| | |
| | | :selectable="row => !row.endOrder" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="pagination"> |
| | | <template #planCompleteTime="{ row }"> |
| | | <span :title="deliveryHint(row)"> |
| | | {{ row.planCompleteTime ? dayjs(row.planCompleteTime).format('YYYY-MM-DD') : '' }} |
| | | </span> |
| | | </template> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | |
| | | type="success" |
| | | size="small">全部完成</el-tag> |
| | | <div v-else |
| | | class="current-process-name"> |
| | | class="current-process-name" |
| | | :title="`${getCurrentProcess(row).name} · ${getCurrentProcess(row).pct}%`"> |
| | | <span class="process-color-dot" |
| | | :style="{ background: getCurrentProcess(row).color || '#909399' }"></span> |
| | | <span :style="getCurrentProcess(row).color ? { color: getCurrentProcess(row).color } : {}">{{ getCurrentProcess(row).name }}</span> |
| | | <span :style="readableTextColor(getCurrentProcess(row).color) ? { color: readableTextColor(getCurrentProcess(row).color) } : {}">{{ getCurrentProcess(row).name }}</span> |
| | | </div> |
| | | </template> |
| | | <span v-else>-</span> |
| | |
| | | class="process-step"> |
| | | <div class="step-content"> |
| | | <div class="step-circle" |
| | | :class="{ 'is-completed': item.percentage >= 100 }"> |
| | | <span class="step-percentage" |
| | | :style="{ color: item.percentage >= 70 ? item.percentage >= 100 ? '#67c23a' : '#f56c6c' : '#000' }">{{ item.percentage }}%</span> |
| | | </div> |
| | | <div class="step-name"> |
| | | <span v-if="item.color" |
| | | :style="{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', marginRight: '4px', background: item.color }"></span> |
| | | <span>{{ item.name }}</span> |
| | | </div> |
| | | :title="`${item.name} ${item.percentage}%`" |
| | | :style="{ background: item.color || '#409EFF', opacity: item.percentage >= 100 ? 1 : item.percentage > 0 ? 0.6 : 0.25 }"></div> |
| | | <div class="step-name">{{ item.name }}</div> |
| | | </div> |
| | | <div v-if="index < row.processRouteStatus.length - 1" |
| | | class="step-line"></div> |
| | | class="step-line" |
| | | :style="{ background: item.percentage >= 100 ? '#67c23a' : '#dcdfe6' }"></div> |
| | | </div> |
| | | </div> |
| | | <span v-else>-</span> |
| | |
| | | </div> |
| | | <el-dialog v-model="bindRouteDialogVisible" |
| | | title="绑定工艺路线" |
| | | width="500px"> |
| | | width="500px" |
| | | :close-on-click-modal="false"> |
| | | <el-form label-width="90px"> |
| | | <el-form-item label="工艺路线"> |
| | | <el-select v-model="bindForm.routeId" |
| | |
| | | <!-- 来源数据弹窗 --> |
| | | <el-dialog v-model="sourceDataDialogVisible" |
| | | title="来源数据" |
| | | width="1200px"> |
| | | width="1200px" |
| | | :close-on-click-modal="false"> |
| | | <div v-if="sourceRowData" |
| | | class="applyno-summary1"> |
| | | <div class="summary-item"> |
| | |
| | | total: 0, |
| | | }); |
| | | |
| | | // 每步固定占宽:名称区96px + 连接线34px,长名称换行,保证圆圈-连线节奏一致 |
| | | const PROCESS_STEP_WIDTH = 130; |
| | | const processColumnWidth = computed(() => { |
| | | if (!tableData.value || tableData.value.length === 0) return "200px"; |
| | | const maxProcesses = Math.max( |
| | | ...tableData.value.map(row => row.processRouteStatus?.length || 0) |
| | | ); |
| | | if (maxProcesses === 0) return "100px"; |
| | | // 每个工序圆圈 36px + 线条 30px = 66px,额外加 60px 边距和文字空间 |
| | | return `${maxProcesses * 66 + 60}px`; |
| | | return `${maxProcesses * PROCESS_STEP_WIDTH + 40}px`; |
| | | }); |
| | | |
| | | const tableColumn = computed(() => [ |
| | |
| | | prop: "currentProcess", |
| | | dataType: "slot", |
| | | slot: "currentProcess", |
| | | align: "center", |
| | | width: 160, |
| | | }, |
| | | { |
| | |
| | | : val === 5 |
| | | ? "已结束" |
| | | : "已取消", |
| | | // 已结束为正常关闭(中性灰),已取消才是异常操作(红) |
| | | formatType: val => |
| | | val === 1 |
| | | ? "primary" |
| | |
| | | : val === 3 |
| | | ? "success" |
| | | : val === 5 |
| | | ? "danger" |
| | | : "info", |
| | | ? "info" |
| | | : "danger", |
| | | }, |
| | | { |
| | | label: "产品名称", |
| | |
| | | { |
| | | label: "计划完成时间", |
| | | prop: "planCompleteTime", |
| | | formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""), |
| | | dataType: "slot", |
| | | slot: "planCompleteTime", |
| | | width: 120, |
| | | }, |
| | | { |
| | |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 280, |
| | | width: 420, |
| | | operation: [ |
| | | { |
| | | name: "工艺路线", |
| | |
| | | { |
| | | name: "生产追溯", |
| | | type: "text", |
| | | color: "#409eff", |
| | | color: "var(--el-color-primary)", |
| | | clickFun: row => { |
| | | router.push({ |
| | | path: "/productionManagement/productionTraceability", |
| | |
| | | { |
| | | name: "结束订单", |
| | | type: "text", |
| | | color: "red", |
| | | color: "var(--el-color-danger)", |
| | | showHide: row => !row.endOrder, |
| | | clickFun: row => { |
| | | handleEndOrder(row); |
| | |
| | | return "#67c23a"; |
| | | }; |
| | | |
| | | // hex 色转 rgba(用于光晕/淡底),非 hex 返回空串 |
| | | const hexToRgba = (hex, alpha) => { |
| | | if (typeof hex !== "string") return ""; |
| | | const m = hex.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); |
| | | if (!m) return ""; |
| | | let n = m[1]; |
| | | if (n.length === 3) n = n.split("").map(c => c + c).join(""); |
| | | const v = parseInt(n, 16); |
| | | return `rgba(${(v >> 16) & 255}, ${(v >> 8) & 255}, ${v & 255}, ${alpha})`; |
| | | }; |
| | | |
| | | // 过浅的工序色在白底上文字不可读,回退常规文字色 |
| | | const isLightColor = hex => { |
| | | const rgba = hexToRgba(hex, 1); |
| | | if (!rgba) return false; |
| | | const [r, g, b] = rgba.match(/\d+/g).map(Number); |
| | | return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.72; |
| | | }; |
| | | const readableTextColor = color => |
| | | !color || isLightColor(color) ? "" : color; |
| | | |
| | | // 当前工序:第一道完成度不足100%的工序;全部完成返回 done |
| | | const getCurrentProcess = row => { |
| | | const list = row.processRouteStatus; |
| | |
| | | done: false, |
| | | name: current.name || "未知工序", |
| | | color: current.color || "", |
| | | pct: toProgressPercentage(current.percentage), |
| | | }; |
| | | }; |
| | | |
| | | // 交期预警的文字提示(颜色之外的无障碍冗余指示) |
| | | const deliveryHint = row => { |
| | | if (!row.planCompleteTime || row.isFh) return ""; |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === 15) return "距交期还剩 15 天"; |
| | | if (diff === 10) return "距交期还剩 10 天"; |
| | | if (diff === 2) return "距交期仅剩 2 天,时间紧张"; |
| | | if (diff < 0) return `已逾期 ${-diff} 天,请尽快处理`; |
| | | if (diff < 2) return `距交期仅剩 ${diff} 天,紧急`; |
| | | return ""; |
| | | }; |
| | | |
| | | // 添加表行类名方法 |
| | |
| | | |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === 15) { |
| | | return "yellow"; |
| | | return "due-15"; |
| | | } else if (diff === 10) { |
| | | return "pink"; |
| | | return "due-10"; |
| | | } else if (diff === 2) { |
| | | return "purple"; |
| | | return "due-2"; |
| | | } else if (diff < 2) { |
| | | return "red"; |
| | | return "due-critical"; |
| | | } |
| | | }; |
| | | |
| | |
| | | gap: 8px; |
| | | } |
| | | |
| | | :deep(.yellow) { |
| | | background-color: #faf0de; |
| | | // 交期临近:黄→橙→浅红→红 色相递进,全部保持文字可读 |
| | | :deep(.due-15) { |
| | | background-color: #fdf6ec; |
| | | } |
| | | |
| | | :deep(.pink) { |
| | | background-color: #fae1de; |
| | | :deep(.due-10) { |
| | | background-color: #faecd8; |
| | | } |
| | | |
| | | :deep(.red) { |
| | | background-color: #f80202; |
| | | :deep(.due-2) { |
| | | background-color: #fde2e2; |
| | | } |
| | | |
| | | :deep(.purple) { |
| | | background-color: #f4defa; |
| | | :deep(.due-critical) { |
| | | background-color: #fbc4c4; |
| | | |
| | | td:first-child { |
| | | box-shadow: inset 3px 0 0 #f56c6c; |
| | | } |
| | | } |
| | | .table_list { |
| | | margin-top: unset; |
| | | } |
| | | |
| | | .current-process-name { |
| | | display: flex; |
| | | align-items: center; |
| | | font-size: 12px; |
| | | display: inline-flex; |
| | | flex-direction: column; |
| | | align-items: stretch; // 色条宽度由文字撑开,两者等宽 |
| | | padding: 4px 0; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .process-color-dot { |
| | | display: inline-block; |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | margin-right: 4px; |
| | | flex-shrink: 0; |
| | | .process-color-dot { |
| | | display: block; |
| | | height: 16px; |
| | | border-radius: 4px; |
| | | margin-bottom: 6px; |
| | | } |
| | | } |
| | | |
| | | .process-progress-container { |
| | |
| | | |
| | | .process-step { |
| | | display: flex; |
| | | align-items: center; |
| | | align-items: flex-start; |
| | | position: relative; |
| | | |
| | | .step-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | width: 96px; |
| | | flex-shrink: 0; |
| | | z-index: 1; |
| | | |
| | | .step-circle { |
| | | width: 36px; |
| | | height: 36px; |
| | | border-radius: 50%; |
| | | border: 2px solid #409eff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-color: #fff; |
| | | box-sizing: border-box; |
| | | margin-bottom: 4px; |
| | | |
| | | .step-percentage { |
| | | font-size: 11px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | &.is-completed { |
| | | border-color: #67c23a; |
| | | .step-percentage { |
| | | color: #67c23a; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .step-name { |
| | | font-size: 12px; |
| | | color: #606266; |
| | | white-space: nowrap; |
| | | line-height: 1.4; |
| | | max-height: 34px; // 最多两行 |
| | | overflow: hidden; |
| | | text-align: center; |
| | | word-break: break-all; |
| | | } |
| | | } |
| | | |
| | | .step-line { |
| | | width: 30px; |
| | | height: 1px; |
| | | width: 130px; // 贯穿相邻两圆心,两端被圆圈(z-index更高)遮挡 |
| | | height: 2px; |
| | | border-radius: 1px; |
| | | background-color: #dcdfe6; |
| | | margin: 0 -2px; |
| | | margin-top: -20px; // 向上偏移以对齐圆心 |
| | | flex-shrink: 0; |
| | | margin: 17px -48px 0; // 线在34px槽位居中:(130-34)/2=48;顶边17px对准圆心 |
| | | } |
| | | } |
| | | } |