spring
23 小时以前 e8d1d68de1382c44e4ebb6fdda5607bf2c9b2ac0
src/views/productionManagement/productionOrder/index.vue
@@ -8,7 +8,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="合同号:">
@@ -16,7 +16,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品名称:">
@@ -24,7 +24,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="规格:">
@@ -32,7 +32,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item>
@@ -41,6 +41,8 @@
        </el-form-item>
      </el-form>
      <div>
        <el-button type="primary" @click="isShowNewModal = true">新增</el-button>
        <el-button type="danger" @click="handleDelete">删除</el-button>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
@@ -50,56 +52,339 @@
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
                :row-class-name="tableRowClassName"
                :isSelection="true"
                @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' : ''"
          />
        </template>
      </PIMTable>
    </div>
    <process-route-item-form v-if="isShowItemModal"
                             v-model:visible="isShowItemModal"
                             :record="record"
                             @completed="getList" />
    <el-dialog v-model="bindRouteDialogVisible"
               title="绑定工艺路线"
               width="500px">
      <el-form label-width="90px">
        <el-form-item label="工艺路线">
          <el-select v-model="bindForm.routeId"
                     placeholder="请选择工艺路线"
                     style="width: 100%;"
                     :loading="bindRouteLoading">
            <el-option v-for="item in routeOptions"
                       :key="item.id"
                       :label="`${item.processRouteCode || ''}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     :loading="bindRouteSaving"
                     @click="handleBindRouteConfirm">确 认</el-button>
          <el-button @click="bindRouteDialogVisible = false">取 消</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog
      v-model="materialDialogVisible"
      title="领料台账"
      width="1200px"
      @close="handleMaterialDialogClose"
    >
      <div class="material-toolbar">
        <el-button type="primary" @click="handleAddMaterialRow">新增</el-button>
      </div>
      <el-table
        v-loading="materialTableLoading"
        :data="materialTableData"
        border
        row-key="tempId"
      >
        <el-table-column label="工序名称" min-width="180">
          <template #default="{ row }">
            <el-select
              v-model="row.processId"
              placeholder="请选择工序"
              clearable
              filterable
              style="width: 100%;"
              @change="val => handleProcessChange(row, val)"
            >
              <el-option
                v-for="item in processOptions"
                :key="item.id"
                :label="item.name"
                :value="item.id"
              />
            </el-select>
          </template>
        </el-table-column>
        <el-table-column label="原料名称" min-width="160">
          <template #default="{ row }">
            <el-button type="primary" link @click="openMaterialProductSelect(row)">
              {{ row.materialName || "选择原料" }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column label="原料型号" min-width="180">
          <template #default="{ row }">
            {{ row.materialModel || "-" }}
          </template>
        </el-table-column>
        <el-table-column label="需求数量" min-width="120">
          <template #default="{ row }">
            <el-input-number
              v-model="row.requiredQty"
              :min="0"
              :precision="3"
              :step="1"
              controls-position="right"
              style="width: 100%;"
              @change="val => handleRequiredQtyChange(row, val)"
            />
          </template>
        </el-table-column>
        <el-table-column label="计量单位" width="120">
          <template #default="{ row }">
            {{ row.unit || "-" }}
          </template>
        </el-table-column>
        <el-table-column label="领用数量" min-width="120">
          <template #default="{ row }">
            <el-input-number
              v-model="row.pickQty"
              :min="0"
              :precision="3"
              :step="1"
              controls-position="right"
              style="width: 100%;"
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="90" fixed="right">
          <template #default="{ $index }">
            <el-button type="danger" link @click="handleDeleteMaterialRow($index)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" :loading="materialSaving" @click="handleMaterialSave">保存</el-button>
          <el-button @click="materialDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <ProductSelectDialog
      v-model="materialProductDialogVisible"
      @confirm="handleMaterialProductConfirm"
      single
    />
    <el-dialog
      v-model="materialDetailDialogVisible"
      title="领料详情"
      width="1400px"
      @close="handleMaterialDetailDialogClose"
    >
      <el-table
        v-loading="materialDetailLoading"
        :data="materialDetailTableData"
        border
        row-key="id"
      >
        <el-table-column label="工序名称" prop="processName" min-width="180" />
        <el-table-column label="原料名称" prop="materialName" min-width="160" />
        <el-table-column label="原料型号" prop="materialModel" min-width="180" />
        <el-table-column label="需求数量" prop="requiredQty" min-width="110" />
        <el-table-column label="计量单位" prop="unit" width="100" />
        <el-table-column label="领用数量" prop="pickQty" min-width="110" />
        <el-table-column label="补料数量" min-width="120">
          <template #default="{ row }">
            <el-button type="primary" link @click="handleViewSupplementRecord(row)">
              {{ row.supplementQty ?? 0 }}
            </el-button>
          </template>
        </el-table-column>
        <el-table-column label="退料数量" prop="returnQty" min-width="110" />
        <el-table-column label="实际数量" prop="actualQty" min-width="110" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="warning" :loading="materialReturnConfirming" @click="handleReturnConfirm">
            退料确认
          </el-button>
          <el-button @click="materialDetailDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog
      v-model="supplementRecordDialogVisible"
      title="补料记录"
      width="800px"
    >
      <el-table
        v-loading="supplementRecordLoading"
        :data="supplementRecordTableData"
        border
        row-key="id"
      >
        <el-table-column label="补料数量" prop="supplementQty" min-width="120" />
        <el-table-column label="补料时间" prop="supplementTime" min-width="180" />
        <el-table-column label="备注" prop="remark" min-width="200" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="supplementRecordDialogVisible = false">关闭</el-button>
        </span>
      </template>
    </el-dialog>
    <new-product-order v-if="isShowNewModal"
                         v-model:visible="isShowNewModal"
                         @completed="handleQuery" />
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import { computed, defineAsyncComponent, getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
  import { useRouter } from "vue-router";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import {
    productOrderListPage,
    listProcessRoute,
    bindingRoute,
    listProcessBom, delProductOrder,
    listMaterialPickingLedger,
    saveMaterialPickingLedger,
    listMaterialPickingDetail,
    listMaterialSupplementRecord,
    confirmMaterialReturn,
  } from "@/api/productionManagement/productionOrder.js";
  import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
  import { processList } from "@/api/productionManagement/productionProcess.js";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  const NewProductOrder = defineAsyncComponent(() => import("@/views/productionManagement/productionOrder/New.vue"));
  const { proxy } = getCurrentInstance();
  import ProcessRouteItemForm from "@/views/productionManagement/productionOrder/ProcessRouteItemForm.vue";
  const router = useRouter();
  const isShowNewModal = ref(false);
  const tableColumn = ref([
    {
      label: "生产订单号",
      prop: "npsNo",
      width: '120px',
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
      width: '150px',
    },
    {
      label: "客户名称",
      prop: "customerName",
      width: '200px',
    },
    {
      label: "产品名称",
      prop: "productCategory",
      width: '120px',
    },
    {
      label: "规格",
      prop: "specificationModel",
      width: '120px',
    },
    {
      label: "工艺路线编号",
      prop: "processRouteCode",
      width: '200px',
    },
    {
      label: "需求数量",
      prop: "quantity",
    },
    {
      label: "完成数量",
      prop: "completeQuantity",
    },
    {
      dataType: "slot",
      label: "完成进度",
      prop: "completionStatus",
      slot: "completionStatus",
      width: 180,
    },
    {
      label: "开始日期",
      prop: "startTime",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
      width: 120,
    },
    {
      label: "结束日期",
      prop: "endTime",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
      width: 120,
    },
    {
      label: "交付日期",
      prop: "deliveryDate",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
      width: 120,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      width: 340,
      operation: [
        {
          name: "工艺路线",
          type: "text",
          clickFun: row => {
            showRouteItemModal(row);
          },
        },
        {
          name: "绑定工艺路线",
          type: "text",
          showHide: row => !row.processRouteCode,
          clickFun: row => {
            openBindRouteDialog(row);
          },
        },
        {
          name: "产品结构",
          type: "text",
          clickFun: row => {
            showProductStructure(row);
          },
        },
        {
          name: "领料",
          type: "text",
          clickFun: row => {
            openMaterialDialog(row);
          },
        },
        {
          name: "领料详情",
          type: "text",
          clickFun: row => {
            openMaterialDetailDialog(row);
          },
        },
      ],
@@ -112,6 +397,7 @@
    size: 100,
    total: 0,
  });
  const selectedRows = ref([]);
  const data = reactive({
    searchForm: {
@@ -123,6 +409,319 @@
    },
  });
  const { searchForm } = toRefs(data);
  const toProgressPercentage = val => {
    const n = Number(val);
    if (!Number.isFinite(n)) return 0;
    if (n <= 0) return 0;
    if (n >= 100) return 100;
    return Math.round(n);
  };
  // 30/50/80/100 分段颜色:红/橙/蓝/绿
  const progressColor = percentage => {
    const p = toProgressPercentage(percentage);
    if (p < 30) return "#f56c6c";
    if (p < 50) return "#e6a23c";
    if (p < 80) return "#409eff";
    return "#67c23a";
  };
  // 添加表行类名方法
  const tableRowClassName = ({ row }) => {
    if (!row.deliveryDate) return '';
    if (row.isFh) return '';
    const diff = row.deliveryDaysDiff;
    if (diff === 15) {
      return 'yellow';
    } else if (diff === 10) {
      return 'pink';
    } else if (diff === 2) {
      return 'purple';
    } else if (diff < 2) {
      return 'red';
    }
  };
  // 绑定工艺路线弹框
  const bindRouteDialogVisible = ref(false);
  const bindRouteLoading = ref(false);
  const bindRouteSaving = ref(false);
  const routeOptions = ref([]);
  const bindForm = reactive({
    orderId: null,
    routeId: null,
  });
  const materialDialogVisible = ref(false);
  const materialProductDialogVisible = ref(false);
  const materialTableLoading = ref(false);
  const materialSaving = ref(false);
  const materialTableData = ref([]);
  const processOptions = ref([]);
  const currentMaterialOrder = ref(null);
  const currentMaterialSelectRowIndex = ref(-1);
  const materialDetailDialogVisible = ref(false);
  const materialDetailLoading = ref(false);
  const materialDetailTableData = ref([]);
  const materialReturnConfirming = ref(false);
  const currentMaterialDetailOrder = ref(null);
  const supplementRecordDialogVisible = ref(false);
  const supplementRecordLoading = ref(false);
  const supplementRecordTableData = ref([]);
  let materialTempId = 0;
  const createMaterialRow = (row = {}) => ({
    tempId: row.id || `temp_${++materialTempId}`,
    id: row.id,
    processId: row.processId,
    processName: row.processName || "",
    materialModelId: row.materialModelId,
    materialName: row.materialName || "",
    materialModel: row.materialModel || "",
    requiredQty: Number(row.requiredQty ?? 0),
    unit: row.unit || "",
    pickQty: Number(row.pickQty ?? row.requiredQty ?? 0),
  });
  const openBindRouteDialog = async row => {
    bindForm.orderId = row.id;
    bindForm.routeId = null;
    bindRouteDialogVisible.value = true;
    routeOptions.value = [];
    if (!row.productModelId) {
      proxy.$modal.msgWarning("当前订单缺少产品型号,无法查询工艺路线");
      bindRouteDialogVisible.value = false;
      return;
    }
    bindRouteLoading.value = true;
    try {
      const res = await listProcessRoute({ productModelId: row.productModelId });
      routeOptions.value = res.data || [];
    } catch (e) {
      console.error("获取工艺路线列表失败:", e);
      proxy.$modal.msgError("获取工艺路线列表失败");
    } finally {
      bindRouteLoading.value = false;
    }
  };
  const handleBindRouteConfirm = async () => {
    if (!bindForm.routeId) {
      proxy.$modal.msgWarning("请选择工艺路线");
      return;
    }
    bindRouteSaving.value = true;
    try {
      await bindingRoute({
        id: bindForm.orderId,
        routeId: bindForm.routeId,
      });
      proxy.$modal.msgSuccess("绑定成功");
      bindRouteDialogVisible.value = false;
      getList();
    } catch (e) {
      console.error("绑定工艺路线失败:", e);
      proxy.$modal.msgError("绑定工艺路线失败");
    } finally {
      bindRouteSaving.value = false;
    }
  };
  const getProcessOptions = async () => {
    if (processOptions.value.length > 0) return;
    try {
      const res = await processList({});
      processOptions.value = res.data || [];
    } catch (e) {
      console.error("获取工序列表失败:", e);
      proxy.$modal.msgError("获取工序列表失败");
    }
  };
  const openMaterialDialog = async row => {
    currentMaterialOrder.value = row;
    materialDialogVisible.value = true;
    materialTableLoading.value = true;
    materialTableData.value = [];
    await getProcessOptions();
    try {
      const res = await listMaterialPickingLedger({ orderId: row.id });
      const list = res.data || [];
      materialTableData.value = list.map(item => createMaterialRow(item));
    } catch (e) {
      console.error("获取领料台账失败:", e);
      proxy.$modal.msgError("获取领料台账失败");
    } finally {
      materialTableLoading.value = false;
    }
  };
  const handleMaterialDialogClose = () => {
    materialTableData.value = [];
    currentMaterialOrder.value = null;
    currentMaterialSelectRowIndex.value = -1;
  };
  const handleAddMaterialRow = () => {
    materialTableData.value.push(createMaterialRow());
  };
  const handleDeleteMaterialRow = index => {
    materialTableData.value.splice(index, 1);
  };
  const handleProcessChange = (row, processId) => {
    const process = processOptions.value.find(item => item.id === processId);
    row.processName = process?.name || "";
  };
  const handleRequiredQtyChange = (row, val) => {
    const required = Number(val ?? 0);
    row.requiredQty = required;
    if (!row.pickQty || Number(row.pickQty) === 0) {
      row.pickQty = required;
    }
  };
  const openMaterialProductSelect = row => {
    currentMaterialSelectRowIndex.value = materialTableData.value.findIndex(item => item.tempId === row.tempId);
    materialProductDialogVisible.value = true;
  };
  const handleMaterialProductConfirm = products => {
    if (!products || products.length === 0) return;
    const index = currentMaterialSelectRowIndex.value;
    if (index < 0 || !materialTableData.value[index]) return;
    const product = products[0];
    const row = materialTableData.value[index];
    row.materialModelId = product.id;
    row.materialName = product.productName || "";
    row.materialModel = product.model || "";
    row.unit = product.unit || "";
    currentMaterialSelectRowIndex.value = -1;
    materialProductDialogVisible.value = false;
  };
  const validateMaterialRows = () => {
    if (materialTableData.value.length === 0) {
      proxy.$modal.msgWarning("请至少新增一条领料记录");
      return false;
    }
    const invalidRow = materialTableData.value.find(
      item =>
        !item.processId ||
        !item.materialModelId ||
        item.requiredQty === null ||
        item.requiredQty === undefined ||
        item.pickQty === null ||
        item.pickQty === undefined
    );
    if (invalidRow) {
      proxy.$modal.msgWarning("请完善领料台账必填字段");
      return false;
    }
    return true;
  };
  const handleMaterialSave = async () => {
    if (!currentMaterialOrder.value?.id) {
      proxy.$modal.msgWarning("未获取到当前生产订单");
      return;
    }
    if (!validateMaterialRows()) return;
    materialSaving.value = true;
    try {
      await saveMaterialPickingLedger({
        orderId: currentMaterialOrder.value.id,
        items: materialTableData.value.map(item => ({
          id: item.id,
          processId: item.processId,
          processName: item.processName,
          materialModelId: item.materialModelId,
          materialName: item.materialName,
          materialModel: item.materialModel,
          requiredQty: item.requiredQty,
          unit: item.unit,
          pickQty: item.pickQty,
        })),
      });
      proxy.$modal.msgSuccess("保存成功");
      materialDialogVisible.value = false;
    } catch (e) {
      console.error("保存领料台账失败:", e);
      proxy.$modal.msgError("保存领料台账失败");
    } finally {
      materialSaving.value = false;
    }
  };
  const openMaterialDetailDialog = async row => {
    currentMaterialDetailOrder.value = row;
    materialDetailDialogVisible.value = true;
    materialDetailLoading.value = true;
    materialDetailTableData.value = [];
    try {
      const res = await listMaterialPickingDetail({ orderId: row.id });
      materialDetailTableData.value = res.data || [];
    } catch (e) {
      console.error("获取领料详情失败:", e);
      proxy.$modal.msgError("获取领料详情失败");
    } finally {
      materialDetailLoading.value = false;
    }
  };
  const handleMaterialDetailDialogClose = () => {
    materialDetailTableData.value = [];
    currentMaterialDetailOrder.value = null;
  };
  const handleViewSupplementRecord = async row => {
    if (!row?.id) {
      proxy.$modal.msgWarning("缺少领料明细ID,无法查看补料记录");
      return;
    }
    supplementRecordDialogVisible.value = true;
    supplementRecordLoading.value = true;
    supplementRecordTableData.value = [];
    try {
      const res = await listMaterialSupplementRecord({ materialDetailId: row.id });
      supplementRecordTableData.value = res.data || [];
    } catch (e) {
      console.error("获取补料记录失败:", e);
      proxy.$modal.msgError("获取补料记录失败");
    } finally {
      supplementRecordLoading.value = false;
    }
  };
  const handleReturnConfirm = async () => {
    if (!currentMaterialDetailOrder.value?.id) {
      proxy.$modal.msgWarning("未获取到当前生产订单");
      return;
    }
    try {
      await ElMessageBox.confirm("确认执行退料确认?", "提示", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      });
    } catch (e) {
      return;
    }
    materialReturnConfirming.value = true;
    try {
      await confirmMaterialReturn({ orderId: currentMaterialDetailOrder.value.id });
      proxy.$modal.msgSuccess("退料确认成功");
      openMaterialDetailDialog(currentMaterialDetailOrder.value);
    } catch (e) {
      console.error("退料确认失败:", e);
      proxy.$modal.msgError("退料确认失败");
    } finally {
      materialReturnConfirming.value = false;
    }
  };
  // 查询列表
  /** 搜索按钮操作 */
@@ -161,11 +760,73 @@
      });
  };
  const isShowItemModal = ref(false);
  const record = ref({});
  const showRouteItemModal = row => {
    isShowItemModal.value = true;
    record.value = row;
  const showRouteItemModal = async row => {
    const orderId = row.id;
    try {
      const res = await getOrderProcessRouteMain(orderId);
      const data = res.data || {};
      if (!data || !data.id) {
        proxy.$modal.msgWarning("未找到关联的工艺路线");
        return;
      }
      router.push({
        path: "/productionManagement/processRouteItem",
        query: {
          id: data.id,
          processRouteCode: data.processRouteCode || "",
          productName: data.productName || "",
          model: data.model || "",
          bomNo: data.bomNo || "",
          description: data.description || "",
          orderId,
          type: "order",
        },
      });
    } catch (e) {
      console.error("获取工艺路线主信息失败:", e);
      proxy.$modal.msgError("获取工艺路线信息失败");
    }
  };
  const showProductStructure = row => {
    router.push({
      path: "/productionManagement/productStructureDetail",
      query: {
        id: row.id,
        bomNo: row.bomNo || "",
        productName: row.productCategory || "",
        productModelName: row.specificationModel || "",
        orderId: row.id,
        type: "order",
      },
    });
  };
  // 表格选择数据
  const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
  };
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map((item) => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      delProductOrder(ids).then((res) => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    }).catch(() => {
      proxy.$modal.msg("已取消");
    });
  };
  // 导出
@@ -176,7 +837,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/salesLedger/scheduling/export", {}, "生产订单.xlsx");
        proxy.download("/productOrder/export", {...searchForm.value}, "生产订单.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -190,4 +851,32 @@
  });
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.search_form{
  align-items: start;
}
:deep(.yellow) {
  background-color: #FAF0DE;
}
:deep(.pink) {
  background-color: #FAE1DE;
}
:deep(.red) {
  background-color: #f80202;
}
:deep(.purple){
  background-color: #F4DEFA;
}
.table_list {
   margin-top: unset;
}
.material-toolbar {
  margin-bottom: 12px;
  text-align: right;
}
</style>