2 天以前 4334e4d5f680b4375d7af4ba9e4d8c8baef7266b
feat(production): 添加工序颜色配置和显示功能

- 在工序统计页面为工序名称添加背景色显示
- 在生产订单页面添加当前工序进度列和工序颜色标识
- 在生产工艺页面添加工序颜色选择器和预定义颜色选项
- 在工单管理页面为工序名称添加颜色标识点
- 在数据看板和生产分析报表中添加工序颜色渲染功能
- 新增 getCurrentProcess 方法获取当前工序状态
- 添加工序颜色字段到数据库操作逻辑
- 优化颜色数据提交处理,将 null 值转换为空字符串
已修改7个文件
145 ■■■■■ 文件已修改
src/views/productionManagement/processStatistics/index.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index0.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processStatistics/index.vue
@@ -33,7 +33,8 @@
                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>
@@ -110,6 +111,7 @@
          good: item.goodQuantity || 0,
          bad: item.scrapQty || 0,
          percentage: Number(item.completionStatus || 0),
          color: item.operationColor || "",
        }));
      })
      .finally(() => {
src/views/productionManagement/productionOrder/index.vue
@@ -83,6 +83,20 @@
                       :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">
@@ -95,7 +109,11 @@
                  <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>
@@ -285,6 +303,13 @@
      label: "生产订单号",
      prop: "npsNo",
      width: "150px",
    },
    {
      label: "当前工序进度",
      prop: "currentProcess",
      dataType: "slot",
      slot: "currentProcess",
      width: 160,
    },
    {
      label: "销售单号",
@@ -510,6 +535,19 @@
    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 || "",
    };
  };
  // 添加表行类名方法
@@ -865,6 +903,22 @@
    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;
src/views/productionManagement/productionProcess/index.vue
@@ -21,7 +21,12 @@
               :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"
@@ -138,6 +143,12 @@
            <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">
@@ -347,7 +358,18 @@
    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" }],
@@ -558,6 +580,7 @@
    processForm.remark = "";
    processForm.deviceLedgerId = null;
    processForm.type = 0;
    processForm.color = "";
    processDialogVisible.value = true;
  };
@@ -575,6 +598,7 @@
    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;
  };
@@ -603,8 +627,10 @@
  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;
@@ -614,7 +640,7 @@
              ElMessage.error("编辑失败");
            });
        } else {
          add(processForm)
          add(submitData)
            .then(() => {
              ElMessage.success("新增成功");
              processDialogVisible.value = false;
@@ -877,6 +903,15 @@
        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;
@@ -990,6 +1025,12 @@
    justify-content: center;
  }
  .color-tip {
    margin-left: 10px;
    font-size: 12px;
    color: #909399;
  }
  // 表格样式
  :deep(.el-table) {
    border: none;
src/views/productionManagement/workOrderManagement/index.vue
@@ -44,6 +44,11 @@
                       :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">
@@ -332,6 +337,8 @@
      label: "工序名称",
      prop: "operationName",
      width: "100",
      dataType: "slot",
      slot: "operationName",
    },
    {
      label: "需求数量",
src/views/reportAnalysis/dataDashboard/index0.vue
@@ -820,8 +820,11 @@
        }
        
        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 = []
        }
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
@@ -133,7 +133,10 @@
        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 = []
      }
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -60,7 +60,7 @@
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      color: pieColors[idx % pieColors.length],
      color: d.color || pieColors[idx % pieColors.length],
    },
  }))
@@ -154,11 +154,18 @@
    .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) => {