工序计划。作业人员推送

This commit is contained in:
tzy 2026-04-12 22:35:17 +08:00
parent 1a454695ed
commit 4b5ee02ba3
3 changed files with 468 additions and 96 deletions

View File

@ -50,3 +50,11 @@ export function cascadeListOperationPlanningMain(query) {
params: query
})
}
export function updateOperationPlanningMainOperators(data) {
return request({
url: '/system/operationPlanningMain/updateOperators',
method: 'put',
data: data
})
}

View File

@ -2,8 +2,8 @@
<div :class="{'hidden':hidden}" class="pagination-container">
<el-pagination
:background="background"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:current-page="currentPage"
:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
@ -85,6 +85,7 @@ export default {
},
methods: {
handleSizeChange(val) {
this.pageSize = val
if (this.currentPage * val > this.total) {
this.currentPage = 1
}
@ -94,6 +95,7 @@ export default {
}
},
handleCurrentChange(val) {
this.currentPage = val
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
scrollTo(0, 800)

View File

@ -123,15 +123,15 @@
<el-tag :type="typeTagType(scope.row)" size="mini" effect="plain">{{ typeText(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="产品主数据" min-width="220">
<el-table-column label="产品主数据" min-width="320">
<template slot-scope="scope">
<div class="cell-main">
<div class="cell-title">{{ scope.row.productCode }}<span v-if="scope.row.productName"> | {{ scope.row.productName }}</span></div>
<div class="cell-title cell-title--wrap">{{ scope.row.productCode }}<span v-if="scope.row.productName"> | {{ scope.row.productName }}</span></div>
<div class="cell-sub">{{ scope.row.proSpecification || '-' }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="产线" min-width="180">
<el-table-column label="产线" width="120">
<template slot-scope="scope">
<div class="cell-main">
<div class="cell-title">{{ scope.row.lineText || '-' }}</div>
@ -159,26 +159,24 @@
<template v-if="!(scope.row.processSteps || []).length">
<span class="cell-sub">-</span>
</template>
<template v-else-if="(scope.row.processSteps || []).length <= 2">
<div class="proc-mini">
<div v-for="(p, idx) in scope.row.processSteps" :key="idx" class="proc-mini-item" @click="openProcessDetail(scope.row, p)">
<el-progress
type="circle"
:percentage="p.percent"
:width="30"
:stroke-width="4"
:color="p.done ? '#16c784' : '#dcdfe6'"
/>
<div class="proc-mini-name">{{ p.processName || '-' }}</div>
</div>
</div>
</template>
<template v-else>
<div class="proc-steps">
<div v-for="(p, idx) in scope.row.processSteps" :key="idx" class="proc-step">
<div class="proc-step-top">
<span class="proc-node" :class="{ done: p.done, clickable: !!p.raw }" @click="openProcessDetail(scope.row, p)">
<i v-if="p.done" class="el-icon-check"></i>
<span class="proc-circle" :class="{ clickable: !!p.raw, ellipsis: !p.raw && p.processName === '…' }" @click="openProcessDetail(scope.row, p)">
<el-progress
type="circle"
:percentage="p.done ? 100 : p.percent"
:width="32"
:stroke-width="4"
:show-text="false"
:color="p.done ? '#16c784' : '#00b4ad'"
/>
<span class="proc-circle__text" :class="{ done: p.done }">
<i v-if="p.done" class="el-icon-check"></i>
<span v-else-if="p.raw">{{ Math.max(0, Math.min(99, Number(p.percent) || 0)) }}%</span>
<span v-else></span>
</span>
</span>
<span v-if="idx < scope.row.processSteps.length - 1" class="proc-line" :class="{ done: p.done }"></span>
</div>
@ -230,18 +228,46 @@
@pagination="getList"
/>
<el-dialog :title="processDialogTitle" :visible.sync="processDialogOpen" width="1200px" top="6vh" append-to-body>
<div v-if="processDialogMeta" class="cell-sub" style="margin-bottom: 10px;">
{{ processDialogMeta }}
<el-drawer
class="process-drawer"
:title="processDialogTitle"
:visible.sync="processDialogOpen"
direction="btt"
size="70%"
append-to-body
>
<template slot="title">
<div class="process-drawer__title-line">
<span class="process-drawer__title-main">工序详情</span>
<span v-if="processDialogBillNo" class="process-drawer__title-kv">工序单号{{ processDialogBillNo }}</span>
<span v-if="processDialogBatchNo" class="process-drawer__title-kv">生产号{{ processDialogBatchNo }}</span>
<span v-if="processDialogProduct" class="process-drawer__title-kv process-drawer__title-product">产品{{ processDialogProduct }}</span>
</div>
</template>
<div class="process-drawer__toolbar">
<div class="process-drawer__toolbar-left">
<span v-if="processDialogDirtyCount" class="process-drawer__dirty">已修改{{ processDialogDirtyCount }}</span>
</div>
<div class="process-drawer__toolbar-right">
<el-button
type="primary"
size="mini"
:loading="processDialogSaving"
:disabled="!processDialogDirtyCount"
@click="submitOperatorUpdates"
>保存</el-button>
</div>
</div>
<el-table
ref="processTable"
:data="processDialogList"
size="mini"
border
highlight-current-row
:row-key="p => p.entityEntryId || p.operNo"
>
<div class="process-drawer__table">
<el-table
ref="processTable"
:data="processDialogList"
size="mini"
border
highlight-current-row
height="100%"
:row-key="p => p.entityEntryId || p.operNo"
>
<el-table-column label="工序号" prop="operNo" width="80" align="center" />
<el-table-column label="工序名称" prop="processName" min-width="140" />
<el-table-column label="工作中心" prop="workCenter" min-width="120" />
@ -249,20 +275,52 @@
<el-table-column label="转入数量" prop="inQty" width="90" align="center" />
<el-table-column label="合格数量" prop="okQty" width="90" align="center" />
<el-table-column label="废品数量" prop="ngQty" width="90" align="center" />
<el-table-column label="状态" prop="status" width="100" align="center" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-tag size="mini" effect="plain" :class="processStatusClass(scope.row)">{{ processStatusText(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="转出数量" prop="transOutQty" width="90" align="center" />
<el-table-column label="损耗数量" prop="wastageQty" width="90" align="center" />
<el-table-column label="加工车间" prop="department" min-width="120" />
<el-table-column label="作业人员" prop="operator" min-width="100" />
<el-table-column label="计划开始" prop="seqPlanStartTime" width="160" />
<el-table-column label="计划完成" prop="seqPlanFinishTime" width="160" />
<el-table-column label="作业人员" min-width="220">
<template slot-scope="scope">
<div class="operator-cell">
<el-input
v-model="scope.row.__operatorDraft"
size="mini"
clearable
placeholder="填写作业人员"
@input="markOperatorDirty(scope.row)"
/>
<el-button
v-if="scope.row.__operatorDirty"
type="text"
size="mini"
:disabled="scope.row.__operatorSaving"
@click="resetOperatorDraft(scope.row)"
>取消</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="计划开始" width="160">
<template slot-scope="scope">
<span>{{ getProcessPlanStartTime(scope.row) || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="计划完成" width="160">
<template slot-scope="scope">
<span>{{ getProcessPlanFinishTime(scope.row) || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="控制码" prop="optCtrlCode" min-width="120" />
<el-table-column label="关键工序" prop="keyOper" width="90" align="center" />
<el-table-column label="活动一" prop="activity1Name" min-width="120" />
<el-table-column label="工序说明" prop="operDescription" min-width="140" />
<el-table-column label="单据体内码" prop="entityEntryId" width="120" align="center" />
</el-table>
</el-dialog>
</el-table>
</div>
</el-drawer>
<!-- 添加或修改工序计划主对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
@ -314,7 +372,7 @@
</template>
<script>
import { listOperationPlanningMain, cascadeListOperationPlanningMain, getOperationPlanningMain, delOperationPlanningMain, addOperationPlanningMain, updateOperationPlanningMain } from "@/api/system/operationPlanningMain";
import { listOperationPlanningMain, cascadeListOperationPlanningMain, getOperationPlanningMain, delOperationPlanningMain, addOperationPlanningMain, updateOperationPlanningMain, updateOperationPlanningMainOperators } from "@/api/system/operationPlanningMain";
export default {
name: "OperationPlanningMain",
@ -331,6 +389,8 @@ export default {
processDialogTitle: "工序详情",
processDialogMeta: "",
processDialogList: [],
processDialogOrder: null,
processDialogSaving: false,
//
single: true,
//
@ -399,6 +459,23 @@ export default {
created() {
this.getList();
},
computed: {
processDialogBillNo() {
const o = this.processDialogOrder || {};
return o.billNo || o.moNumber || '';
},
processDialogBatchNo() {
const o = this.processDialogOrder || {};
return o.batchNo || o.hbytSclh || '';
},
processDialogProduct() {
const o = this.processDialogOrder || {};
return [o.productCode, o.productName].filter(Boolean).join(' | ');
},
processDialogDirtyCount() {
return (this.processDialogList || []).filter(p => p && p.__operatorDirty).length;
}
},
methods: {
/** 查询工序计划主列表 */
getList() {
@ -470,8 +547,12 @@ export default {
const processSteps = processList.map(p => {
const planQty = this.normalizeNumber(p.planQty || overallPlanQty);
const okQty = this.normalizeNumber(p.okQty);
const percent = planQty > 0 ? Math.min(100, Math.round((okQty / planQty) * 100)) : 0;
const done = this.isProcessDone(p, planQty, okQty);
const inQty = this.normalizeNumber(p.inQty);
const backendProgress = this.normalizePercent(p.progress);
const baseQty = p.inQty != null ? inQty : okQty;
const fallbackProgress = planQty > 0 ? (baseQty / planQty) * 100 : 0;
const percent = backendProgress != null ? backendProgress : this.normalizePercent(fallbackProgress) || 0;
const done = this.isProcessDone(p, planQty, okQty, inQty, percent);
return {
operNo: p.operNo,
@ -499,12 +580,25 @@ export default {
lineText
};
},
isProcessDone(process, planQty, okQty) {
isProcessDone(process, planQty, okQty, inQty, percent) {
const derived = this.calcProcessStatus(process);
if (derived === '工序完成' || derived === '已关闭') return true;
const status = (process && process.status) || '';
if (String(status).includes('完工')) return true;
if (percent != null && Number(percent) >= 100) return true;
if (planQty > 0 && okQty >= planQty) return true;
if (planQty > 0 && inQty >= planQty) return true;
return false;
},
normalizePercent(val) {
if (val == null || val === '') return null;
const n = Number(val);
if (!Number.isFinite(n)) return null;
if (n < 0) return 0;
if (n > 100) return 100;
return n;
},
buildLineText(processList) {
const p0 = Array.isArray(processList) && processList.length ? processList[0] : undefined;
if (p0 && p0.workCenter) return p0.workCenter;
@ -538,14 +632,76 @@ export default {
},
statusText(row) {
if (!row) return '-';
if (row.overallProgress >= 100) return '完工';
if (row.overallProgress > 0) return '已开工';
return '备料中';
return this.calcOrderStatus(row) || '-';
},
statusTagClass(row) {
const text = this.statusText(row);
if (text === '完工' || text === '已开工') return 'tag-purple';
return 'tag-green';
if (text === '未开始') return 'tag-gray';
if (text === '进行中') return 'tag-green';
if (text === '预警') return 'tag-orange';
if (text === '延期') return 'tag-red';
if (text === '已完成') return 'tag-blue';
return 'tag-gray';
},
calcOrderStatus(row) {
if (!row) return '';
const overallProgress = Number(row.overallProgress) || 0;
if (overallProgress >= 100) return '已完成';
const risk = this.calcOrderRisk(row);
if (risk) return risk;
if (overallProgress > 0) return '进行中';
return '未开始';
},
calcOrderRisk(row) {
if (!row) return '';
if (Number(row.overallProgress) >= 100) return '';
const warnDays = 1;
const now = Date.now();
const orderDue = this.toTimestamp(row.planFinishTime);
const orderRisk = this.calcRiskByDue(orderDue, now, warnDays);
let processRisk = '';
const list = Array.isArray(row.processList) ? row.processList : [];
for (const p of list) {
const base = this.calcProcessStatus(p);
if (base === '工序完成' || base === '已转出' || base === '已关闭') continue;
const due = this.toTimestamp(this.getProcessPlanFinishTime(p));
const r = this.calcRiskByDue(due, now, warnDays);
if (r === '延期') {
processRisk = '延期';
break;
}
if (r === '预警') processRisk = '预警';
}
if (orderRisk === '延期' || processRisk === '延期') return '延期';
if (orderRisk === '预警' || processRisk === '预警') return '预警';
return '';
},
calcRiskByDue(dueTs, nowTs, warnDays) {
if (!dueTs) return '';
const diffDays = (dueTs - nowTs) / 86400000;
if (diffDays < 0) return '延期';
if (diffDays <= warnDays) return '预警';
return '';
},
toTimestamp(val) {
if (val == null || val === '') return null;
if (typeof val === 'number' && Number.isFinite(val)) return val;
const s = String(val);
const ts = new Date(s.replace(/-/g, '/')).getTime();
return Number.isFinite(ts) ? ts : null;
},
getProcessPlanStartTime(process) {
if (!process) return '';
return process.operPlanStartTime || process.fOperPlanStartTime || process.seqPlanStartTime || process.fSeqPlanStartTime || '';
},
getProcessPlanFinishTime(process) {
if (!process) return '';
return process.operPlanFinishTime || process.fOperPlanFinishTime || process.seqPlanFinishTime || process.fSeqPlanFinishTime || '';
},
typeText(row) {
return (row && row.typeText) || '正常';
@ -617,16 +773,149 @@ export default {
this.processDialogTitle = code ? `工序详情 - ${code}` : '工序详情';
this.processDialogMeta = batchNo ? `生产令号:${batchNo}` : '';
this.processDialogList = list;
this.processDialogOrder = orderRow || null;
this.processDialogList = this.prepareProcessDialogList(list);
this.processDialogOpen = true;
this.$nextTick(() => {
if (!this.$refs.processTable || !step || !step.raw) return;
const target = this.processDialogList.find(p => p.entityEntryId === step.raw.entityEntryId)
|| this.processDialogList.find(p => p.operNo === step.operNo);
const entryId = step.raw.entityEntryId != null ? String(step.raw.entityEntryId) : '';
const operNo = step.operNo != null ? String(step.operNo) : '';
const target = this.processDialogList.find(p => p.entityEntryId != null && String(p.entityEntryId) === entryId)
|| this.processDialogList.find(p => p.operNo != null && String(p.operNo) === operNo);
if (target) this.$refs.processTable.setCurrentRow(target);
});
},
prepareProcessDialogList(list) {
const safeList = Array.isArray(list) ? list : [];
return safeList.map(p => {
const operator = p && p.operator != null ? String(p.operator) : '';
return {
...p,
__operatorDraft: operator,
__operatorDirty: false,
__operatorSaving: false
};
});
},
processStatusText(process) {
return this.calcProcessDisplayStatus(process) || '-';
},
processStatusClass(process) {
const text = this.calcProcessDisplayStatus(process);
if (text === '未开工') return 'tag-gray';
if (text === '生产中') return 'tag-green';
if (text === '质检中') return 'tag-blue';
if (text === '预警') return 'tag-orange';
if (text === '延期') return 'tag-red';
if (text === '工序完成' || text === '已转出') return 'tag-blue';
if (text === '已关闭') return 'tag-gray';
return 'tag-gray';
},
calcProcessDisplayStatus(process) {
const base = this.calcProcessStatus(process);
if (base === '工序完成' || base === '已转出' || base === '已关闭') return base;
const warnDays = 1;
const now = Date.now();
const due = this.toTimestamp(this.getProcessPlanFinishTime(process));
const risk = this.calcRiskByDue(due, now, warnDays);
if (risk) return risk;
if (base === '未开工' || base === '生产中' || base === '质检中') return base;
return base || '';
},
calcProcessStatus(process) {
if (!process) return '';
const rawK3 = process.k3Status ?? process.operStatus ?? process.k3OperStatus;
const k3 = rawK3 == null || rawK3 === '' ? 0 : Number(rawK3);
if (k3 === 5 || k3 === 6) return '已关闭';
const inQty = this.normalizeNumber(process.inQty);
const okQty = this.normalizeNumber(process.okQty);
const ngQty = this.normalizeNumber(process.ngQty);
const transOutQty = this.normalizeNumber(process.transOutQty);
if (inQty === 0) return '未开工';
if (transOutQty > 0) return '已转出';
if (okQty + ngQty >= inQty) return '工序完成';
if (okQty + ngQty > 0) return '质检中';
return '生产中';
},
markOperatorDirty(row) {
if (!row) return;
const origin = row.operator != null ? String(row.operator) : '';
const draft = row.__operatorDraft != null ? String(row.__operatorDraft) : '';
row.__operatorDirty = origin !== draft;
},
resetOperatorDraft(row) {
if (!row) return;
row.__operatorDraft = row.operator != null ? String(row.operator) : '';
row.__operatorDirty = false;
},
buildOperatorUpdatePayload(rows) {
const order = this.processDialogOrder || {};
const list = Array.isArray(rows) ? rows : [];
return {
orderId: order.id ?? null,
fid: order.fid ?? null,
billNo: order.billNo ?? null,
moNumber: order.moNumber ?? null,
batchNo: (order.batchNo || order.hbytSclh) ?? null,
updates: list.map(p => ({
k3DetailId: p.k3DetailId ?? null,
entityEntryId: p.entityEntryId ?? null,
operNo: p.operNo ?? null,
processName: p.processName ?? null,
operator: p.__operatorDraft != null ? String(p.__operatorDraft).trim() : ''
}))
};
},
submitOperatorUpdates(targetRows) {
if (this.processDialogSaving) return;
const rows = Array.isArray(targetRows)
? targetRows
: (this.processDialogList || []).filter(p => p && p.__operatorDirty);
const realRows = rows.filter(r => {
const origin = r && r.operator != null ? String(r.operator) : '';
const draft = r && r.__operatorDraft != null ? String(r.__operatorDraft).trim() : '';
return origin !== draft;
});
if (!realRows.length) return;
this.processDialogSaving = true;
realRows.forEach(r => {
r.__operatorSaving = true;
});
const payload = this.buildOperatorUpdatePayload(realRows);
updateOperationPlanningMainOperators(payload)
.then(() => {
realRows.forEach(r => {
const next = r.__operatorDraft != null ? String(r.__operatorDraft).trim() : '';
r.operator = next;
r.__operatorDraft = next;
r.__operatorDirty = false;
});
this.$modal.msgSuccess('保存成功');
})
.finally(() => {
realRows.forEach(r => {
r.__operatorSaving = false;
});
this.processDialogSaving = false;
});
},
saveOperator(row) {
if (!row || row.__operatorSaving) return;
this.markOperatorDirty(row);
if (!row.__operatorDirty) return;
this.$modal.msgWarning('已暂存,点击上方“保存”统一提交');
},
formatProcessText(row) {
const list = (row && Array.isArray(row.processList) ? row.processList : []).map(p => {
const operNo = p.operNo != null ? String(p.operNo) : '-';
@ -742,6 +1031,30 @@ export default {
color: #ffffff;
}
.tag-gray {
background: #909399;
border-color: #909399;
color: #ffffff;
}
.tag-orange {
background: #f59e0b;
border-color: #f59e0b;
color: #ffffff;
}
.tag-red {
background: #ef4444;
border-color: #ef4444;
color: #ffffff;
}
.tag-blue {
background: #3b82f6;
border-color: #3b82f6;
color: #ffffff;
}
.cell-main {
line-height: 18px;
}
@ -754,6 +1067,14 @@ export default {
text-overflow: ellipsis;
}
.cell-title--wrap {
white-space: normal;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.cell-sub {
color: #909399;
font-size: 12px;
@ -768,36 +1089,6 @@ export default {
min-height: 46px;
}
.proc-mini {
display: flex;
align-items: center;
gap: 10px;
}
.proc-mini-item {
display: flex;
flex-direction: column;
align-items: center;
width: 56px;
cursor: pointer;
}
::v-deep .proc-mini .el-progress-circle__text {
font-size: 16px;
font-weight: 600;
}
.proc-mini-name {
margin-top: 4px;
font-size: 12px;
color: #303133;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 56px;
text-align: center;
}
.proc-steps {
display: flex;
align-items: flex-start;
@ -814,33 +1105,46 @@ export default {
align-items: center;
}
.proc-node {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #dcdfe6;
.proc-circle {
position: relative;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #ffffff;
background: #ffffff;
}
.proc-node.done {
width: 28px;
height: 28px;
border-color: #16c784;
color: #16c784;
}
.proc-node.clickable {
.proc-circle.clickable {
cursor: pointer;
}
.proc-node.done i {
.proc-circle__text {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #00b4ad;
font-size: 10px;
font-weight: 600;
user-select: none;
}
.proc-circle__text.done {
color: #16c784;
font-size: 16px;
}
.proc-circle.ellipsis .proc-circle__text {
font-size: 18px;
line-height: 1;
color: #c0c4cc;
}
.proc-line {
width: 26px;
height: 2px;
@ -883,4 +1187,62 @@ export default {
.is-row-checked td {
background: #e8faf8 !important;
}
.process-drawer ::v-deep .el-drawer__body {
padding: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.process-drawer__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.process-drawer__title-main {
font-size: 20px;
font-weight: 800;
color: #303133;
}
.process-drawer__title-line {
display: flex;
align-items: baseline;
gap: 14px;
width: 100%;
overflow: hidden;
white-space: nowrap;
}
.process-drawer__title-kv {
font-size: 14px;
font-weight: 600;
color: #606266;
}
.process-drawer__title-product {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.process-drawer__dirty {
color: #606266;
font-size: 12px;
}
.process-drawer__table {
flex: 1;
min-height: 0;
}
.operator-cell {
display: flex;
align-items: center;
gap: 6px;
}
</style>