1. 生产模块按工序拆分为两个一级菜单 - 生产-前缫(落丝记录)、生产-后缫(纤度检验、黑板检验) - 旧的「生产管理」(/prod) 目录及子菜单由种子自动清理,页面组件与权限标识保持不变 - 消息中心跳转路径、首页快捷入口路径同步更新 2. 职工管理独立为一级菜单(原挂在「基础数据」目录下),并清理历史子菜单 3. 修复侧边栏菜单展开卡顿(基础数据等子项较多的目录尤其明显) - 去掉 Element Plus 的 height 过渡动画(会逐帧重排全部子项) - 收起时改用 height:0 + visibility:hidden 而非 display:none,子项提前完成布局 - 菜单区独立滚动并预留滚动条宽度(scrollbar-gutter),消除展开时的横向抖动 - 菜单项增加 contain 布局隔离,淡入动画改为容器级 4. 修复页面首次打开重复请求 - 分页组件挂载时会触发一次 current-change,页码未变化时不再重复加载(18 个页面) - 职工管理去掉 onMounted 与默认选中部门的重复加载 - 工种管理、原料庄口的注册表下拉由按 key 多次请求改为一次全量拉取 5. 后端增加前端静态托管与 SPA 路由回退,支持 IIS 单站点部署(前后端同源) 6. 新增部署脚本 deploy-iis.ps1(首次部署)与 update-iis.ps1(更新发布)
380 lines
17 KiB
Vue
380 lines
17 KiB
Vue
<script setup>
|
|
import { ref, reactive, onMounted, computed } from 'vue';
|
|
import request from '../../../utils/request';
|
|
import { ElMessage } from 'element-plus';
|
|
import { CircleCloseFilled, WarningFilled, InfoFilled } from '@element-plus/icons-vue';
|
|
|
|
// ==================== 列表 ====================
|
|
const list = ref([]);
|
|
const total = ref(0);
|
|
const loading = ref(false);
|
|
const deptOptions = ref([]);
|
|
const query = reactive({ pageIndex: 1, pageSize: 20, date: '', deptId: null });
|
|
|
|
async function loadDepts() {
|
|
try {
|
|
const res = await request.get('/sys/department');
|
|
deptOptions.value = (res.data || []).filter((d) => d.name.includes('前缫'));
|
|
} catch (e) {}
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true;
|
|
try {
|
|
const params = { pageIndex: query.pageIndex, pageSize: query.pageSize };
|
|
if (query.date) params.date = query.date;
|
|
if (query.deptId) params.deptId = query.deptId;
|
|
const res = await request.get('/xdjy', { params });
|
|
list.value = res.data.items;
|
|
total.value = res.data.total;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function handlePageChange(p) {
|
|
// 分页组件挂载时会触发一次 current-change,页码未变化时忽略,避免首次打开重复请求
|
|
if (p === query.pageIndex) return;
|
|
query.pageIndex = p;
|
|
load();
|
|
}
|
|
|
|
// ==================== 编辑对话框 ====================
|
|
const dialogVisible = ref(false);
|
|
const editLoading = ref(false);
|
|
const saving = ref(false);
|
|
const editData = ref(null);
|
|
const gg = computed(() => editData.value?.guigeConfig || null);
|
|
|
|
// 纤度列配置
|
|
const xdColumns = [
|
|
{ key: 'xd1', label: '纤度1' },
|
|
{ key: 'xd2', label: '纤度2' },
|
|
{ key: 'xd3', label: '纤度3' },
|
|
{ key: 'xd4', label: '纤度4' },
|
|
];
|
|
|
|
function num(v) {
|
|
if (typeof v === 'number') return isFinite(v) ? v : 0;
|
|
if (v === null || v === undefined || v === '') return 0;
|
|
const n = Number(v);
|
|
return isFinite(n) ? n : 0;
|
|
}
|
|
function round2(v) { return Math.round(v * 100) / 100; }
|
|
|
|
// 野纤等级:teye(特野) / daye(大野) / xiaoye(小野) / null(正常)
|
|
function xdLevel(xd) {
|
|
const g = gg.value;
|
|
const v = num(xd);
|
|
if (!g || v <= 0) return null;
|
|
if (v <= g.teyeLolimit || v >= g.teyeUplimit) return 'teye';
|
|
if (v <= g.dayeLolimit || v >= g.dayeUplimit) return 'daye';
|
|
if (v <= g.xiaoyeLolimit || v >= g.xiaoyeUplimit) return 'xiaoye';
|
|
return null;
|
|
}
|
|
|
|
// 范围(在中心范围内)
|
|
function isInRange(xd) {
|
|
const g = gg.value;
|
|
const v = num(xd);
|
|
return !!g && v > 0 && g.centerrangeLolimit <= v && v <= g.centerrangeUplimit;
|
|
}
|
|
|
|
// 中心(等于中心纤度)
|
|
function isCenter(xd) {
|
|
const g = gg.value;
|
|
const v = num(xd);
|
|
return !!g && v > 0 && g.centerXiandu === v;
|
|
}
|
|
|
|
// 计算单行指标
|
|
function calcRow(row) {
|
|
const xds = [num(row.xd1), num(row.xd2), num(row.xd3), num(row.xd4)];
|
|
const valid = xds.filter((v) => v > 0);
|
|
row.xdNum = valid.length;
|
|
if (valid.length > 0) {
|
|
row.xdAvg = round2(xds.reduce((a, b) => a + b, 0) / valid.length);
|
|
row.fanweiNum = xds.filter(isInRange).length;
|
|
row.xiaoyeNum = xds.filter((v) => xdLevel(v) === 'xiaoye').length;
|
|
row.dayeNum = xds.filter((v) => xdLevel(v) === 'daye').length;
|
|
row.teyeNum = xds.filter((v) => xdLevel(v) === 'teye').length;
|
|
row.centerNum = xds.filter(isCenter).length;
|
|
} else {
|
|
row.xdAvg = 0; row.fanweiNum = 0; row.xiaoyeNum = 0; row.dayeNum = 0; row.teyeNum = 0; row.centerNum = 0;
|
|
}
|
|
}
|
|
|
|
// 汇总指标
|
|
const summary = computed(() => {
|
|
const items = editData.value?.xdjy || [];
|
|
let jiaoshu = 0, sum = 0, fanweiNum = 0, xiaoyeNum = 0, dayeNum = 0, teyeNum = 0, centerNum = 0;
|
|
const xds = [];
|
|
items.forEach((r) => {
|
|
[num(r.xd1), num(r.xd2), num(r.xd3), num(r.xd4)].forEach((xd) => {
|
|
if (xd > 0) { jiaoshu++; sum += xd; xds.push(xd); }
|
|
});
|
|
fanweiNum += r.fanweiNum || 0; xiaoyeNum += r.xiaoyeNum || 0; dayeNum += r.dayeNum || 0;
|
|
teyeNum += r.teyeNum || 0; centerNum += r.centerNum || 0;
|
|
});
|
|
const avg = jiaoshu > 0 ? sum / jiaoshu : 0;
|
|
let junfangcha = 0;
|
|
if (jiaoshu > 0) {
|
|
let diff = 0;
|
|
xds.forEach((x) => { const d = x - avg; diff += d * d; });
|
|
junfangcha = round2(Math.sqrt(round2(diff / jiaoshu)));
|
|
}
|
|
let zongcha = 0;
|
|
if (jiaoshu > 0) {
|
|
xds.sort((a, b) => a - b);
|
|
const n = Math.max(1, Math.ceil(xds.length * 0.02));
|
|
const minAvg = xds.slice(0, n).reduce((a, b) => a + b, 0) / n;
|
|
const maxAvg = xds.slice(xds.length - n).reduce((a, b) => a + b, 0) / n;
|
|
zongcha = round2(Math.max(Math.abs(minAvg - round2(avg)), Math.abs(maxAvg - round2(avg))));
|
|
}
|
|
const fanxianLv = jiaoshu > 0 ? Math.round(fanweiNum * 100 / jiaoshu) : 0;
|
|
return { jiaoshu, avgxiandu: round2(avg), junfangcha, zongcha, fanxianLv, fanweiNum, xiaoyeNum, dayeNum, teyeNum, centerNum };
|
|
});
|
|
|
|
// 纤度输入:显式赋值 + 重算,避免事件时序问题
|
|
function onXdInput(row, key, v) {
|
|
row[key] = num(v);
|
|
calcRow(row);
|
|
}
|
|
|
|
// 单击选中整个单元格(聚焦时全选,方便直接覆盖输入)
|
|
function selectAll(e) {
|
|
const el = e?.target;
|
|
if (el && typeof el.select === 'function') {
|
|
setTimeout(() => el.select(), 0);
|
|
}
|
|
}
|
|
|
|
async function openEdit(row) {
|
|
editLoading.value = true;
|
|
dialogVisible.value = true;
|
|
try {
|
|
const res = await request.get(`/xdjy/${row.lsjlId}`);
|
|
editData.value = res.data;
|
|
editData.value.xdjy.forEach(calcRow);
|
|
} catch (e) {
|
|
dialogVisible.value = false;
|
|
} finally {
|
|
editLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function save() {
|
|
if (!editData.value) return;
|
|
if (summary.value.jiaoshu === 0) { ElMessage.warning('请至少录入一个纤度值'); return; }
|
|
saving.value = true;
|
|
try {
|
|
await request.post('/xdjy', {
|
|
lsjlId: editData.value.lsjlId,
|
|
dengji: editData.value.dengji || '',
|
|
items: editData.value.xdjy.map((i) => ({
|
|
chetaihao: i.chetaihao,
|
|
xd1: num(i.xd1), xd2: num(i.xd2), xd3: num(i.xd3), xd4: num(i.xd4)
|
|
}))
|
|
});
|
|
ElMessage.success('保存成功');
|
|
dialogVisible.value = false;
|
|
load();
|
|
} catch (e) {} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => { load(); loadDepts(); });
|
|
</script>
|
|
|
|
<template>
|
|
<el-card shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>纤度检验</span>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="search-bar">
|
|
<el-date-picker v-model="query.date" type="date" placeholder="日期" value-format="YYYY-MM-DD" style="width: 160px" />
|
|
<el-select v-model="query.deptId" clearable placeholder="车间" style="width: 180px">
|
|
<el-option v-for="d in deptOptions" :key="d.id" :label="d.name" :value="d.id" />
|
|
</el-select>
|
|
<el-button type="primary" @click="query.pageIndex=1;load()">搜索</el-button>
|
|
<el-button @click="Object.assign(query,{date:'',deptId:null,pageIndex:1});load()">重置</el-button>
|
|
</div>
|
|
|
|
<el-table v-loading="loading" :data="list" border stripe>
|
|
<el-table-column type="index" label="序号" width="55" />
|
|
<el-table-column label="日期" width="100">
|
|
<template #default="{ row }">{{ row.workDate?.slice(0, 10) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="deptName" label="车间" width="110" />
|
|
<el-table-column prop="jizu" label="机组" width="60" />
|
|
<el-table-column prop="huishu" label="回数" width="60" />
|
|
<el-table-column prop="guige" label="规格" width="75" />
|
|
<el-table-column prop="zhuangKouName" label="庄口" min-width="160" show-overflow-tooltip />
|
|
<el-table-column prop="yanse" label="颜色" width="75" />
|
|
<el-table-column prop="isbancheName" label="半车" width="65" />
|
|
<el-table-column prop="chetaishu" label="车台数" width="70" />
|
|
<el-table-column prop="luosiTime" label="落丝时间" width="90" />
|
|
<el-table-column prop="yingchengnum" label="应秤数" width="70" />
|
|
<el-table-column label="已秤数" width="70" align="center">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.yingchengnum > row.jiaoshu ? 'warning' : 'success'" size="small" effect="light">{{ row.jiaoshu }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="avgxiandu" label="平均纤度" width="85" />
|
|
<el-table-column prop="junfangcha" label="均方差" width="75" />
|
|
<el-table-column prop="zongcha" label="总差" width="65" />
|
|
<el-table-column prop="fanxianLv" label="范纤率" width="70" />
|
|
<el-table-column prop="centerNum" label="中心" width="60" />
|
|
<el-table-column prop="xiaoyeNum" label="小野" width="60" />
|
|
<el-table-column prop="dayeNum" label="大野" width="60" />
|
|
<el-table-column prop="teyeNum" label="特野" width="60" />
|
|
<el-table-column prop="dengji" label="等级" width="65" />
|
|
<el-table-column label="操作" width="90" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button size="small" :type="row.isChecked ? 'warning' : 'primary'" @click="openEdit(row)">
|
|
{{ row.isChecked ? '重新检验' : '检验' }}
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<el-pagination class="pagination" :current-page="query.pageIndex" :page-size="query.pageSize" :total="total" layout="total, prev, pager, next" @current-change="handlePageChange" />
|
|
|
|
<!-- 检验对话框 -->
|
|
<el-dialog v-model="dialogVisible" title="纤度检验" width="94%" top="2vh" destroy-on-close>
|
|
<div v-loading="editLoading">
|
|
<template v-if="editData">
|
|
<!-- 落丝信息 -->
|
|
<el-descriptions :column="6" border size="small" class="info-block">
|
|
<el-descriptions-item label="车间">{{ editData.deptName }}</el-descriptions-item>
|
|
<el-descriptions-item label="机组">{{ editData.jizu }}</el-descriptions-item>
|
|
<el-descriptions-item label="回数">{{ editData.huishu }}</el-descriptions-item>
|
|
<el-descriptions-item label="庄口">{{ editData.zhuangKouName }}</el-descriptions-item>
|
|
<el-descriptions-item label="颜色">{{ editData.yanse }}</el-descriptions-item>
|
|
<el-descriptions-item label="半车">{{ editData.isbancheName }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<!-- 规格范围 -->
|
|
<el-descriptions v-if="gg" :column="6" border size="small" class="info-block">
|
|
<el-descriptions-item label="规格">{{ gg.guige }}</el-descriptions-item>
|
|
<el-descriptions-item label="中心范围">[{{ gg.centerrangeLolimit }} - {{ gg.centerrangeUplimit }}]</el-descriptions-item>
|
|
<el-descriptions-item label="中心纤度">{{ gg.centerXiandu }}</el-descriptions-item>
|
|
<el-descriptions-item label="小野范围" class="d-xiaoye">≤{{ gg.xiaoyeLolimit }} / ≥{{ gg.xiaoyeUplimit }}</el-descriptions-item>
|
|
<el-descriptions-item label="大野范围" class="d-daye">≤{{ gg.dayeLolimit }} / ≥{{ gg.dayeUplimit }}</el-descriptions-item>
|
|
<el-descriptions-item label="特野范围" class="d-teye">≤{{ gg.teyeLolimit }} / ≥{{ gg.teyeUplimit }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<!-- 车台明细 -->
|
|
<el-table :data="editData.xdjy" border size="small" max-height="440">
|
|
<el-table-column prop="chetaihao" label="车台号" width="70" align="center" fixed />
|
|
<el-table-column v-for="c in xdColumns" :key="c.key" :label="c.label" width="118" align="center">
|
|
<template #default="{ row }">
|
|
<el-input-number
|
|
v-model="row[c.key]"
|
|
:min="0"
|
|
:precision="1"
|
|
:step="0.5"
|
|
:controls="false"
|
|
:class="['xd-input', 'xd-' + (xdLevel(row[c.key]) || 'normal')]"
|
|
@focus="selectAll"
|
|
@input="(v) => onXdInput(row, c.key, v)"
|
|
>
|
|
<template #suffix>
|
|
<el-icon v-if="xdLevel(row[c.key]) === 'teye'" class="xd-icon icon-teye"><CircleCloseFilled /></el-icon>
|
|
<el-icon v-else-if="xdLevel(row[c.key]) === 'daye'" class="xd-icon icon-daye"><WarningFilled /></el-icon>
|
|
<el-icon v-else-if="xdLevel(row[c.key]) === 'xiaoye'" class="xd-icon icon-xiaoye"><InfoFilled /></el-icon>
|
|
</template>
|
|
</el-input-number>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="xdNum" label="已称绞数" width="80" align="center" />
|
|
<el-table-column prop="xdAvg" label="平均纤度" width="85" align="center" />
|
|
<el-table-column prop="fanweiNum" label="范围" width="60" align="center" />
|
|
<el-table-column prop="xiaoyeNum" label="小野" width="60" align="center" />
|
|
<el-table-column prop="dayeNum" label="大野" width="60" align="center" />
|
|
<el-table-column prop="teyeNum" label="特野" width="60" align="center" />
|
|
<el-table-column prop="centerNum" label="中心" width="60" align="center" />
|
|
</el-table>
|
|
|
|
<!-- 汇总 -->
|
|
<div class="summary-bar">
|
|
<div class="stat"><span class="lbl">已称绞数</span><b>{{ summary.jiaoshu }}</b></div>
|
|
<div class="stat"><span class="lbl">平均纤度</span><b class="c-primary">{{ summary.avgxiandu }}</b></div>
|
|
<div class="stat"><span class="lbl">均方差</span><b>{{ summary.junfangcha }}</b></div>
|
|
<div class="stat"><span class="lbl">总差</span><b>{{ summary.zongcha }}</b></div>
|
|
<div class="stat"><span class="lbl">范纤率</span><b class="c-primary">{{ summary.fanxianLv }}%</b></div>
|
|
<div class="stat"><span class="lbl">范围</span><b class="c-blue">{{ summary.fanweiNum }}</b></div>
|
|
<div class="stat tag-xiaoye"><span class="lbl">小野</span><b>{{ summary.xiaoyeNum }}</b></div>
|
|
<div class="stat tag-daye"><span class="lbl">大野</span><b>{{ summary.dayeNum }}</b></div>
|
|
<div class="stat tag-teye"><span class="lbl">特野</span><b>{{ summary.teyeNum }}</b></div>
|
|
<div class="stat tag-center"><span class="lbl">中心</span><b>{{ summary.centerNum }}</b></div>
|
|
<div class="stat level">
|
|
<span class="lbl">等级</span>
|
|
<el-input v-model="editData.dengji" placeholder="等级" style="width: 90px" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图例 -->
|
|
<div class="legend">
|
|
<span class="lg lg-teye"><el-icon><CircleCloseFilled /></el-icon> 特野</span>
|
|
<span class="lg lg-daye"><el-icon><WarningFilled /></el-icon> 大野</span>
|
|
<span class="lg lg-xiaoye"><el-icon><InfoFilled /></el-icon> 小野</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</el-card>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
.search-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
.pagination { margin-top: 16px; justify-content: flex-end; }
|
|
.info-block { margin-bottom: 12px; }
|
|
|
|
/* 规格范围描述着色 */
|
|
.d-xiaoye :deep(.el-descriptions__content) { color: #b88230; font-weight: 600; }
|
|
.d-daye :deep(.el-descriptions__content) { color: #e6a23c; font-weight: 600; }
|
|
.d-teye :deep(.el-descriptions__content) { color: #f56c6c; font-weight: 600; }
|
|
|
|
/* 纤度输入框:浅色背景 + 图标 */
|
|
.xd-input :deep(.el-input__wrapper) { padding: 0 6px; }
|
|
.xd-input.xd-teye :deep(.el-input__wrapper) { background: #fef0f0; box-shadow: 0 0 0 1px #f56c6c inset; }
|
|
.xd-input.xd-daye :deep(.el-input__wrapper) { background: #fdf6ec; box-shadow: 0 0 0 1px #e6a23c inset; }
|
|
.xd-input.xd-xiaoye :deep(.el-input__wrapper) { background: #fef9e7; box-shadow: 0 0 0 1px #e6b23c inset; }
|
|
|
|
.xd-icon { font-size: 14px; }
|
|
.icon-teye { color: #f56c6c; }
|
|
.icon-daye { color: #e6a23c; }
|
|
.icon-xiaoye { color: #e6b23c; }
|
|
|
|
/* 汇总栏 */
|
|
.summary-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 16px; padding: 14px 16px; background: #f5f7fa; border-radius: 8px; }
|
|
.stat { display: flex; flex-direction: column; align-items: center; min-width: 64px; padding: 6px 12px; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
|
.stat .lbl { font-size: 12px; color: #909399; }
|
|
.stat b { font-size: 18px; color: #303133; }
|
|
.stat .c-primary { color: #409eff; }
|
|
.stat .c-blue { color: #409eff; }
|
|
.stat.tag-xiaoye b { color: #b88230; }
|
|
.stat.tag-daye b { color: #e6a23c; }
|
|
.stat.tag-teye b { color: #f56c6c; }
|
|
.stat.tag-center b { color: #67c23a; }
|
|
.stat.level { flex-direction: row; gap: 6px; align-items: center; }
|
|
|
|
/* 图例 */
|
|
.legend { display: flex; gap: 18px; margin-top: 12px; color: #606266; font-size: 13px; }
|
|
.legend .lg { display: flex; align-items: center; gap: 4px; }
|
|
.lg-teye { color: #f56c6c; }
|
|
.lg-daye { color: #e6a23c; }
|
|
.lg-xiaoye { color: #b88230; }
|
|
</style>
|