zouyu
15 小时以前 6570b36a352edd87532dcf13a124181d4d815a39
src/views/productionManagement/productionOrder/index.vue
@@ -8,7 +8,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 160px;"
                    style="width: 160px"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品名称:">
@@ -16,7 +16,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 160px;"
                    style="width: 160px"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="规格:">
@@ -24,13 +24,13 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 160px;"
                    style="width: 160px"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="状态:">
          <el-select v-model="searchForm.status"
                     placeholder="请选择"
                     style="width: 160px;"
                     style="width: 160px"
                     @change="handleQuery">
            <el-option label="待开始"
                       value="1" />
@@ -38,8 +38,10 @@
                       value="2" />
            <el-option label="已完成"
                       value="3" />
            <el-option label="已取消"
                       value="4" />
            <!--            <el-option label="已取消"-->
            <!--                       value="4" />-->
            <el-option label="已结束"
                       value="5" />
          </el-select>
        </el-form-item>
        <el-form-item>
@@ -65,12 +67,44 @@
                :tableLoading="tableLoading"
                :row-class-name="tableRowClassName"
                :isSelection="true"
                :selectable="(row) => !row.endOrder"
                @selection-change="handleSelectionChange"
                @pagination="pagination">
        <template #completionStatus="{ row }">
          <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
                       :color="progressColor(toProgressPercentage(row?.completionStatus))"
                       :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
                       :status="
              toProgressPercentage(row?.completionStatus) >= 100
                ? 'success'
                : ''
            " />
        </template>
        <template #processRouteStatus="{ row }">
          <div v-if="row.processRouteStatus && row.processRouteStatus.length"
               class="process-progress-container">
            <div v-for="(item, index) in row.processRouteStatus"
                 :key="index"
                 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">{{ item.name }}</div>
              </div>
              <div v-if="index < row.processRouteStatus.length - 1"
                   class="step-line"></div>
            </div>
          </div>
          <span v-else>-</span>
        </template>
      </PIMTable>
    </div>
@@ -81,7 +115,7 @@
        <el-form-item label="工艺路线">
          <el-select v-model="bindForm.routeId"
                     placeholder="请选择工艺路线"
                     style="width: 100%;"
                     style="width: 100%"
                     :loading="bindRouteLoading">
            <el-option v-for="item in routeOptions"
                       :key="item.id"
@@ -102,18 +136,23 @@
    <!-- 来源数据弹窗 -->
    <el-dialog v-model="sourceDataDialogVisible"
               title="来源数据"
               width="1200px">
               width="1200px"
               top="5vh"
               class="source-data-dialog"
               append-to-body>
      <div v-if="sourceRowData"
           class="applyno-summary1">
        <div class="summary-item">
          <span class="summary-label">产品名称:</span>
          <span class="summary-value">
            <el-tag type="primary">{{ sourceRowData.productName || '-' }}</el-tag>
            <el-tag type="primary">{{
              sourceRowData.productName || "-"
            }}</el-tag>
          </span>
        </div>
        <div class="summary-item">
          <span class="summary-label">规格:</span>
          <span class="summary-value">{{ sourceRowData.model || '-' }}</span>
          <span class="summary-value">{{ sourceRowData.model || "-" }}</span>
        </div>
        <div class="summary-item">
          <span class="summary-label">订单需求数量:</span>
@@ -129,39 +168,49 @@
              <div class="info-grid">
                <div class="info-item">
                  <div class="info-label">计划号</div>
                  <div class="info-value">{{ item.mpsNo || '-' }}</div>
                  <div class="info-value">{{ item.mpsNo || "-" }}</div>
                </div>
                <div class="info-item">
                  <div class="info-label">数据来源</div>
                  <div class="info-value">
                    <el-tag :type="item.source === '销售' ? 'primary' : 'warning'">
                      {{ item.source || '未知' }}
                      {{ item.source || "未知" }}
                    </el-tag>
                  </div>
                </div>
                <div class="info-item">
                  <div class="info-label">合同号</div>
                  <div class="info-value">{{ item.salesContractNo || '-' }}</div>
                  <div class="info-value">
                    {{ item.salesContractNo || "-" }}
                  </div>
                </div>
                <div class="info-item">
                  <div class="info-label">客户名称</div>
                  <div class="info-value">{{ item.customerName || '-' }}</div>
                  <div class="info-value">{{ item.customerName || "-" }}</div>
                </div>
                <div class="info-item">
                  <div class="info-label">项目名称</div>
                  <div class="info-value">{{ item.projectName || '-' }}</div>
                  <div class="info-value">{{ item.projectName || "-" }}</div>
                </div>
                <div class="info-item">
                  <div class="info-label">计划需求数量</div>
                  <div class="info-value">{{ item.qtyRequired || 0 }} {{ item.unit || '' }}</div>
                  <div class="info-value">
                    {{ item.qtyRequired || 0 }} {{ item.unit || "" }}
                  </div>
                </div>
                <div class="info-item">
                  <div class="info-label">单位</div>
                  <div class="info-value">{{ item.unit || '-' }}</div>
                  <div class="info-value">{{ item.unit || "-" }}</div>
                </div>
                <div class="info-item">
                  <div class="info-label">需求日期</div>
                  <div class="info-value">{{ item.requiredDate ? dayjs(item.requiredDate).format('YYYY-MM-DD') : '-' }}</div>
                  <div class="info-value">
                    {{
                      item.requiredDate
                        ? dayjs(item.requiredDate).format("YYYY-MM-DD")
                        : "-"
                    }}
                  </div>
                </div>
              </div>
            </div>
@@ -181,6 +230,12 @@
    <new-product-order v-if="isShowNewModal"
                       v-model:visible="isShowNewModal"
                       @completed="handleQuery" />
    <!-- 打印领料单组件 -->
    <div class="print-requisition-wrapper">
      <PrintMaterialRequisition ref="printRef"
                                :order-row="printOrderRow"
                                :material-list="printMaterialList" />
    </div>
  </div>
</template>
@@ -204,13 +259,20 @@
    listProcessBom,
    delProductOrder,
    getProductOrderSource,
    updateProductOrder,
  } from "@/api/productionManagement/productionOrder.js";
  import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
  import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
  import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue";
  import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue";
  import MaterialSupplementDialog from "@/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue";
  import PrintMaterialRequisition from "@/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import { listPage } from "@/api/productionManagement/processRoute.js";
  import {
    listMaterialPickingDetail,
    listMaterialPickingBom,
  } from "@/api/productionManagement/productionOrder.js";
  const NewProductOrder = defineAsyncComponent(() =>
    import("@/views/productionManagement/productionOrder/New.vue")
  );
@@ -226,13 +288,23 @@
    total: 0,
  });
  const tableColumn = ref([
  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`;
  });
  const tableColumn = computed(() => [
    {
      label: "生产订单号",
      prop: "npsNo",
      width: "150px",
    },
    // 1.待开始、2.进行中、3.已完成、4.已取消
    // 1.待开始、2.进行中、3.已完成、4.已取消、5.已结束
    {
      label: "状态",
      prop: "status",
@@ -245,6 +317,8 @@
          ? "进行中"
          : val === 3
          ? "已完成"
          : val === 5
          ? "已结束"
          : "已取消",
      formatType: val =>
        val === 1
@@ -253,7 +327,9 @@
          ? "warning"
          : val === 3
          ? "success"
          : "danger",
          : val === 5
          ? "danger"
          : "info",
    },
    {
      label: "产品名称",
@@ -277,6 +353,13 @@
    {
      label: "完成数量",
      prop: "completeQuantity",
    },
    {
      label: "工序生产进度",
      prop: "processRouteStatus",
      dataType: "slot",
      slot: "processRouteStatus",
      width: processColumnWidth.value,
    },
    {
      dataType: "slot",
@@ -308,7 +391,7 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: 260,
      width: 280,
      operation: [
        {
          name: "工艺路线",
@@ -321,7 +404,8 @@
        {
          name: "绑定工艺路线",
          type: "text",
          showHide: row => !row.processRouteCode,
          showHide: row =>
            !row.processRouteCode && !row.endOrder && row.status !== 3,
          clickFun: row => {
            openBindRouteDialog(row, "add");
          },
@@ -329,7 +413,8 @@
        {
          name: "更换工艺路线",
          type: "text",
          showHide: row => row.processRouteCode,
          showHide: row =>
            row.processRouteCode && !row.endOrder && row.status !== 3,
          clickFun: row => {
            openBindRouteDialog(row, "change");
          },
@@ -345,6 +430,7 @@
          name: "领料",
          type: "text",
          color: "#5EC7AB",
          showHide: row => !row.endOrder && !row.returned,
          clickFun: row => {
            openMaterialDialog(row);
          },
@@ -353,6 +439,7 @@
          name: "补料",
          type: "text",
          color: "#5EC7AB",
          showHide: row => !row.endOrder && !row.returned,
          clickFun: row => {
            openMaterialSupplementDialog(row);
          },
@@ -363,6 +450,39 @@
          color: "#5EC7AB",
          clickFun: row => {
            openMaterialDetailDialog(row);
          },
        },
        {
          name: "打印领料单",
          type: "text",
          color: "#5EC7AB",
          showHide: row => !row.endOrder,
          clickFun: row => {
            handlePrint(row);
          },
        },
        {
          name: "生产追溯",
          type: "text",
          color: "#409eff",
          clickFun: row => {
            router.push({
              path: "/productionManagement/productionTraceability",
              query: {
                npsNo: row.npsNo,
                productName: row.productName,
                model: row.model,
              },
            });
          },
        },
        {
          name: "结束订单",
          type: "text",
          color: "red",
          showHide: row => !row.endOrder,
          clickFun: row => {
            handleEndOrder(row);
          },
        },
      ],
@@ -395,7 +515,7 @@
    if (!Number.isFinite(n)) return 0;
    if (n <= 0) return 0;
    if (n >= 100) return 100;
    return Math.round(n);
    return parseFloat(n.toFixed(2));
  };
  // 30/50/80/100 分段颜色:红/橙/蓝/绿
@@ -440,9 +560,45 @@
  const materialSupplementDialogVisible = ref(false);
  const currentMaterialSupplementOrder = ref(null);
  // 打印相关
  const printOrderRow = ref(null);
  const printMaterialList = ref([]);
  const handlePrint = async row => {
    printOrderRow.value = row;
    proxy.$modal.loading("正在获取领料数据...");
    try {
      printMaterialList.value = [];
      const detailRes = await listMaterialPickingDetail(row.id);
      const detailList = Array.isArray(detailRes?.data)
        ? detailRes.data
        : detailRes?.data?.records || [];
      if (detailList.length > 0) {
        printMaterialList.value = detailList;
      }
      if (printMaterialList.value.length === 0) {
        proxy.$modal.msgWarning("暂无领料数据");
        return;
      }
      // 等待 DOM 更新后执行打印
      proxy.$nextTick(() => {
        setTimeout(() => {
          window.print();
        }, 800);
      });
    } catch (e) {
      console.error("获取领料数据失败:", e);
      proxy.$modal.msgError("获取领料数据失败");
    } finally {
      proxy.$modal.closeLoading();
    }
  };
  const openBindRouteDialog = async (row, type) => {
    bindForm.orderId = row.id;
    bindForm.routeId = type === "add" ? null : row.processRouteCode;
    bindForm.routeId = type === "add" ? null : row.technologyRoutingId;
    bindRouteDialogVisible.value = true;
    routeOptions.value = [];
    if (!row.productModelId) {
@@ -540,10 +696,35 @@
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    productOrderListPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
      .then(async res => {
        const records = res.data.records || [];
        // 为每个订单查询对应的工序进度数据
        const processPromises = records.map(async item => {
          if (item.npsNo) {
            try {
              const workOrderRes = await productWorkOrderPage({
                npsNo: item.npsNo,
                size: 100,
              });
              const workOrders = workOrderRes.data.records || [];
              // 按照工序顺序排序(如果有顺序字段,假设为 orderNum 或按返回顺序)
              // 转换为 processRouteStatus 格式
              const processRouteStatus = workOrders.map(wo => ({
                name: wo.operationName || "未知工序",
                percentage: wo.completionStatus > 100 ? 100 : wo.completionStatus,
              }));
              return { ...item, processRouteStatus };
            } catch (error) {
              console.error(`获取工单 ${item.npsNo} 进度失败:`, error);
              return { ...item, processRouteStatus: [] };
            }
          }
          return { ...item, processRouteStatus: [] };
        });
        tableData.value = await Promise.all(processPromises);
        page.total = res.data.total;
        tableLoading.value = false;
      })
      .catch(() => {
        tableLoading.value = false;
@@ -570,8 +751,10 @@
          bomNo: row.bomNo || "",
          description: data.description || "",
          quantity: row.quantity || 0,
          technologyRoutingId: data.technologyRoutingId,
          orderId,
          type: "order",
          editable: !row.endOrder && row.status !== 3,
        },
      });
    } catch (e) {
@@ -656,14 +839,34 @@
    })
      .then(() => {
        proxy.download(
          "/productOrder/export",
          "/productionOrder/export",
          { ...searchForm.value },
          "生产订单.xlsx"
          "生产订单数据.xlsx"
        );
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // 结束订单
  const handleEndOrder = row => {
    ElMessageBox.confirm(`是否确认结束订单:${row.npsNo}?`, "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        const params = {
          id: row.id,
          endOrder: true,
        };
        updateProductOrder(params).then(() => {
          proxy.$modal.msgSuccess("结束订单成功");
          getList();
        });
      })
      .catch(() => {});
  };
  const handleConfirmRoute = () => {};
@@ -702,6 +905,64 @@
  .table_list {
    margin-top: unset;
  }
  .process-progress-container {
    display: inline-flex;
    align-items: center;
    padding: 10px 0;
    white-space: nowrap;
    .process-step {
      display: flex;
      align-items: center;
      position: relative;
      .step-content {
        display: flex;
        flex-direction: column;
        align-items: center;
        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;
          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;
        }
      }
      .step-line {
        width: 30px;
        height: 1px;
        background-color: #dcdfe6;
        margin: 0 -2px;
        margin-top: -20px; // 向上偏移以对齐圆心
      }
    }
  }
</style>
<style lang="scss">
  .status-cell {
@@ -713,13 +974,19 @@
  .source-table-container {
    margin-top: 20px;
    flex: 1;
    min-height: 0;
    max-height: 500px;
    overflow: auto;
  }
  .source-data-cards-container {
    display: flex;
    flex-direction: column;
    gap: 16px;
    max-height: 500px;
    flex: 1;
    min-height: 0;
    max-height: none;
    overflow-y: auto;
    padding: 10px;
    background-color: #f5f7fa;
@@ -789,4 +1056,20 @@
      }
    }
  }
  .source-data-dialog {
    .el-dialog {
      display: flex;
      flex-direction: column;
      max-height: 90vh;
    }
    .el-dialog__body {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
  }
</style>