feat(production): 添加工序颜色配置和显示功能
- 在工序统计页面为工序名称添加背景色显示
- 在生产订单页面添加当前工序进度列和工序颜色标识
- 在生产工艺页面添加工序颜色选择器和预定义颜色选项
- 在工单管理页面为工序名称添加颜色标识点
- 在数据看板和生产分析报表中添加工序颜色渲染功能
- 新增 getCurrentProcess 方法获取当前工序状态
- 添加工序颜色字段到数据库操作逻辑
- 优化颜色数据提交处理,将 null 值转换为空字符串
| | |
| | | class="mb-16"> |
| | | <div class="stats-card"> |
| | | <div class="card-header"> |
| | | <span class="process-name">{{ item.name }}</span> |
| | | <span class="process-name" |
| | | :style="item.color ? { backgroundColor: item.color, color: '#fff' } : {}">{{ item.name }}</span> |
| | | <div class="header-stats"> |
| | | <div class="stat-row"> |
| | | <span class="label">计划数</span> |
| | |
| | | good: item.goodQuantity || 0, |
| | | bad: item.scrapQty || 0, |
| | | percentage: Number(item.completionStatus || 0), |
| | | color: item.operationColor || "", |
| | | })); |
| | | }) |
| | | .finally(() => { |
| | |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | </template> |
| | | <template #currentProcess="{ row }"> |
| | | <template v-if="getCurrentProcess(row)"> |
| | | <el-tag v-if="getCurrentProcess(row).done" |
| | | type="success" |
| | | size="small">全部完成</el-tag> |
| | | <div v-else |
| | | class="current-process-name"> |
| | | <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> |
| | | </div> |
| | | </template> |
| | | <span v-else>-</span> |
| | | </template> |
| | | <template #processRouteStatus="{ row }"> |
| | | <div v-if="row.processRouteStatus && row.processRouteStatus.length" |
| | | class="process-progress-container"> |
| | |
| | | <span class="step-percentage" |
| | | :style="{ color: item.percentage >= 70 ? item.percentage >= 100 ? '#67c23a' : '#f56c6c' : '#000' }">{{ item.percentage }}%</span> |
| | | </div> |
| | | <div class="step-name">{{ item.name }}</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> |
| | | </div> |
| | | <div v-if="index < row.processRouteStatus.length - 1" |
| | | class="step-line"></div> |
| | |
| | | label: "生产订单号", |
| | | prop: "npsNo", |
| | | width: "150px", |
| | | }, |
| | | { |
| | | label: "当前工序进度", |
| | | prop: "currentProcess", |
| | | dataType: "slot", |
| | | slot: "currentProcess", |
| | | width: 160, |
| | | }, |
| | | { |
| | | label: "销售单号", |
| | |
| | | if (p < 50) return "#e6a23c"; |
| | | if (p < 80) return "#409eff"; |
| | | return "#67c23a"; |
| | | }; |
| | | |
| | | // 当前工序:第一道完成度不足100%的工序;全部完成返回 done |
| | | const getCurrentProcess = row => { |
| | | const list = row.processRouteStatus; |
| | | if (!Array.isArray(list) || list.length === 0) return null; |
| | | const current = list.find(item => toProgressPercentage(item.percentage) < 100); |
| | | if (!current) return { done: true }; |
| | | return { |
| | | done: false, |
| | | name: current.name || "未知工序", |
| | | color: current.color || "", |
| | | }; |
| | | }; |
| | | |
| | | // 添加表行类名方法 |
| | |
| | | margin-top: unset; |
| | | } |
| | | |
| | | .current-process-name { |
| | | display: flex; |
| | | align-items: center; |
| | | font-size: 12px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .process-color-dot { |
| | | display: inline-block; |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | margin-right: 4px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .process-progress-container { |
| | | display: inline-flex; |
| | | align-items: center; |
| | |
| | | :class="{ active: selectedProcess?.id === process.id }" |
| | | @click="selectProcess(process)"> |
| | | <div class="card-header"> |
| | | <div class="process-name">{{ process.name }} <span class="process-code">{{ process.no }}</span></div> |
| | | <div class="process-name"> |
| | | <span v-if="process.color" |
| | | class="process-color-dot" |
| | | :style="{ backgroundColor: process.color }"></span> |
| | | {{ process.name }} <span class="process-code">{{ process.no }}</span> |
| | | </div> |
| | | <div class="card-actions"> |
| | | <el-button link |
| | | type="primary" |
| | |
| | | <el-radio :label="0">计时</el-radio> |
| | | <el-radio :label="1">计件</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="工序颜色" |
| | | prop="color"> |
| | | <el-color-picker v-model="processForm.color" |
| | | :predefine="predefineColors" /> |
| | | <span class="color-tip">用于生产模块工序标识颜色渲染</span> |
| | | </el-form-item> |
| | | <el-form-item label="关联设备" |
| | | prop="deviceLedgerId"> |
| | |
| | | remark: "", |
| | | deviceLedgerId: null, |
| | | type: 0, |
| | | color: "", |
| | | }); |
| | | const predefineColors = [ |
| | | "#409eff", |
| | | "#67c23a", |
| | | "#e6a23c", |
| | | "#f56c6c", |
| | | "#909399", |
| | | "#9254de", |
| | | "#13c2c2", |
| | | "#fa8c16", |
| | | ]; |
| | | const processRules = { |
| | | no: [{ required: true, message: "请输入工序编码", trigger: "blur" }], |
| | | name: [{ required: true, message: "请输入工序名称", trigger: "blur" }], |
| | |
| | | processForm.remark = ""; |
| | | processForm.deviceLedgerId = null; |
| | | processForm.type = 0; |
| | | processForm.color = ""; |
| | | processDialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | const hasDevice = deviceOptions.value.some(item => item.id === deviceId); |
| | | processForm.deviceLedgerId = deviceId && hasDevice ? deviceId : null; |
| | | processForm.type = process.type; |
| | | processForm.color = process.color || ""; |
| | | processDialogVisible.value = true; |
| | | }; |
| | | |
| | |
| | | const handleProcessSubmit = () => { |
| | | processFormRef.value.validate(valid => { |
| | | if (valid) { |
| | | // 颜色清空后为 null,后端 updateById 会忽略 null 字段,统一转为空串提交 |
| | | const submitData = { ...processForm, color: processForm.color || "" }; |
| | | if (processForm.id) { |
| | | update(processForm) |
| | | update(submitData) |
| | | .then(() => { |
| | | ElMessage.success("编辑成功"); |
| | | processDialogVisible.value = false; |
| | |
| | | ElMessage.error("编辑失败"); |
| | | }); |
| | | } else { |
| | | add(processForm) |
| | | add(submitData) |
| | | .then(() => { |
| | | ElMessage.success("新增成功"); |
| | | processDialogVisible.value = false; |
| | |
| | | font-family: "Courier New", monospace; |
| | | } |
| | | |
| | | .process-color-dot { |
| | | display: inline-block; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 50%; |
| | | margin-right: 6px; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | .card-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | |
| | | justify-content: center; |
| | | } |
| | | |
| | | .color-tip { |
| | | margin-left: 10px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | // 表格样式 |
| | | :deep(.el-table) { |
| | | border: none; |
| | |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | </template> |
| | | <template #operationName="{ row }"> |
| | | <span v-if="row.operationColor" |
| | | :style="{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', marginRight: '4px', background: row.operationColor }"></span> |
| | | <span>{{ row.operationName }}</span> |
| | | </template> |
| | | <template #picked="{ row }"> |
| | | <el-tag :type="row.picked ? 'success' : 'info'" |
| | | size="small"> |
| | |
| | | label: "工序名称", |
| | | prop: "operationName", |
| | | width: "100", |
| | | dataType: "slot", |
| | | slot: "operationName", |
| | | }, |
| | | { |
| | | label: "需求数量", |
| | |
| | | } |
| | | |
| | | if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) { |
| | | // 设置Y轴数据(在制品数量) |
| | | workInProcessBarSeries.value[0].data = res.data.processQuantityDetails |
| | | // 设置Y轴数据(在制品数量),有工序颜色时按颜色渲染柱子 |
| | | const processColors = Array.isArray(res.data.processColors) ? res.data.processColors : [] |
| | | workInProcessBarSeries.value[0].data = res.data.processQuantityDetails.map((v, i) => |
| | | processColors[i] ? { value: v, itemStyle: { color: processColors[i] } } : v |
| | | ) |
| | | } else { |
| | | workInProcessBarSeries.value[0].data = [] |
| | | } |
| | |
| | | workInProcessXAxis.value[0].data = [] |
| | | } |
| | | if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) { |
| | | workInProcessBarSeries.value[0].data = res.data.processQuantityDetails |
| | | const colors = Array.isArray(res.data.processColors) ? res.data.processColors : [] |
| | | workInProcessBarSeries.value[0].data = res.data.processQuantityDetails.map((v, i) => |
| | | colors[i] ? { value: v, itemStyle: { color: colors[i] } } : v |
| | | ) |
| | | } else { |
| | | workInProcessBarSeries.value[0].data = [] |
| | | } |
| | |
| | | icon: 'circle', |
| | | textStyle: { |
| | | fontSize: 18, |
| | | color: pieColors[idx % pieColors.length], |
| | | color: d.color || pieColors[idx % pieColors.length], |
| | | }, |
| | | })) |
| | | |
| | |
| | | .then((res) => { |
| | | if (res.code === 200 && Array.isArray(res.data)) { |
| | | const items = res.data |
| | | pieDatas.value = items.map((item) => ({ |
| | | name: item.name, |
| | | value: parseFloat(item.value) || 0, |
| | | rate: item.rate, |
| | | })) |
| | | pieDatas.value = items.map((item) => { |
| | | const point = { |
| | | name: item.name, |
| | | value: parseFloat(item.value) || 0, |
| | | rate: item.rate, |
| | | } |
| | | if (item.color) { |
| | | point.color = item.color |
| | | point.itemStyle = { color: item.color } |
| | | } |
| | | return point |
| | | }) |
| | | } |
| | | }) |
| | | .catch((err) => { |