Files
f9-web-ai-test/web/src/views/prod/xdjy/index.vue
T
yanpeng200212 c3e2665257 生产模块拆分为前缫/后缫;修复菜单卡顿与重复请求;支持 IIS 单站点部署
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(更新发布)
2026-10-12 14:09:59 +08:00

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>