gaoluyang
9 小时以前 cca940a6460bc4ec4266df4e413b05921d1f2e1d
src/views/productionManagement/productionOrder/index.vue
@@ -1,193 +1,673 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="客户名称:">
          <el-input v-model="searchForm.customerName"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="客户名称">
          <el-input
            v-model="searchForm.customerName"
            placeholder="请输入"
            clearable
            :prefix-icon="Search"
            style="width: 160px;"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="合同号:">
          <el-input v-model="searchForm.salesContractNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        <el-form-item label="销售合同号">
          <el-input
            v-model="searchForm.salesContractNo"
            placeholder="请输入"
            clearable
            :prefix-icon="Search"
            style="width: 160px;"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="产品名称:">
          <el-input v-model="searchForm.productCategory"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        <el-form-item label="产品名称">
          <el-input
            v-model="searchForm.productCategory"
            placeholder="请输入"
            clearable
            :prefix-icon="Search"
            style="width: 160px;"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="规格:">
          <el-input v-model="searchForm.specificationModel"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        <el-form-item label="规格型号">
          <el-input
            v-model="searchForm.specificationModel"
            placeholder="请输入"
            clearable
            :prefix-icon="Search"
            style="width: 160px;"
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
          <el-button type="primary" @click="handleQuery">查询</el-button>
        </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>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :tableLoading="tableLoading"
        :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="700px"
    >
      <el-form label-width="90px">
        <el-form-item label="工艺路线">
          <el-select
            v-model="bindForm.routeId"
            placeholder="请选择工艺路线"
            style="width: 100%;"
            :loading="bindRouteLoading"
            @change="handleBindRouteChange"
          >
            <el-option
              v-for="item in routeOptions"
              :key="item.id"
              :label="item.processRouteCode || ''"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item v-if="bindProcessList.length" label="报工人员">
          <div class="process-user-list">
            <div
              v-for="(item, index) in bindProcessList"
              :key="item.id || `${item.processId}-${index}`"
              class="process-user-item"
            >
              <div class="process-user-header">
                <div class="process-user-name">
                  {{ item.name || item.processName || item.no || `工序${index + 1}` }}
                </div>
                <el-button
                  type="danger"
                  link
                  class="process-user-remove"
                  @click="removeBindProcessItem(index)"
                >
                  删除
                </el-button>
              </div>
              <el-select
                v-model="bindForm.processUserList[index].userIds"
                class="process-user-select"
                placeholder="请选择报工人员"
                filterable
                clearable
                multiple
                collapse-tags
                collapse-tags-tooltip
                :max-collapse-tags="3"
                @change="handleBindProcessUserChange(index, $event)"
              >
                <el-option
                  v-for="user in userOptions"
                  :key="user.userId"
                  :label="user.nickName"
                  :value="user.userId"
                />
              </el-select>
            </div>
          </div>
        </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>
    <new-product-order
      v-if="isShowNewModal"
      v-model:visible="isShowNewModal"
      @completed="handleQuery"
    />
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
  const { proxy } = getCurrentInstance();
  import ProcessRouteItemForm from "@/views/productionManagement/productionOrder/ProcessRouteItemForm.vue";
import { defineAsyncComponent, getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
import { ElMessageBox } from "element-plus";
import { Search } from "@element-plus/icons-vue";
import dayjs from "dayjs";
import { useRouter } from "vue-router";
import {
  bindingRoute,
  delProductOrder,
  listProcessRoute,
  productOrderListPage,
} from "@/api/productionManagement/productionOrder.js";
import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
import { processList } from "@/api/productionManagement/productionProcess.js";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import {findProcessRouteItemList} from "@/api/productionManagement/processRouteItem.js";
  const tableColumn = ref([
    {
      label: "生产订单号",
      prop: "npsNo",
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
    },
    {
      label: "客户名称",
      prop: "customerName",
    },
    {
      label: "产品名称",
      prop: "productCategory",
    },
    {
      label: "规格",
      prop: "specificationModel",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      operation: [
        {
          name: "工艺路线",
          type: "text",
          clickFun: row => {
            showRouteItemModal(row);
          },
const NewProductOrder = defineAsyncComponent(() => import("@/views/productionManagement/productionOrder/New.vue"));
const { proxy } = getCurrentInstance();
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,
    operation: [
      {
        name: "工艺路线",
        type: "text",
        clickFun: row => {
          showRouteItemModal(row);
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
      },
      {
        name: "绑定工艺路线",
        type: "text",
        showHide: row => !row.processRouteCode,
        clickFun: row => {
          openBindRouteDialog(row);
        },
      },
      {
        name: "产品结构",
        type: "text",
        clickFun: row => {
          showProductStructure(row);
        },
      },
    ],
  },
]);
  const data = reactive({
    searchForm: {
      customerName: "",
      salesContractNo: "",
      projectName: "",
      productCategory: "",
      specificationModel: "",
    },
  });
  const { searchForm } = toRefs(data);
const tableData = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const selectedRows = ref([]);
  // 查询列表
  /** 搜索按钮操作 */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const changeDaterange = value => {
    if (value) {
      searchForm.value.entryDateStart = value[0];
      searchForm.value.entryDateEnd = value[1];
    } else {
      searchForm.value.entryDateStart = undefined;
      searchForm.value.entryDateEnd = undefined;
    }
    handleQuery();
  };
  const getList = () => {
    tableLoading.value = true;
    // 构造一个新的对象,不包含entryDate字段
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    productOrderListPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
const data = reactive({
  searchForm: {
    customerName: "",
    salesContractNo: "",
    projectName: "",
    productCategory: "",
    specificationModel: "",
  },
});
const { searchForm } = toRefs(data);
  const isShowItemModal = ref(false);
  const record = ref({});
  const showRouteItemModal = row => {
    isShowItemModal.value = true;
    record.value = row;
  };
const toProgressPercentage = val => {
  const n = Number(val);
  if (!Number.isFinite(n) || n <= 0) return 0;
  if (n >= 100) return 100;
  return Math.round(n);
};
  // 导出
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
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 || row.isFh) return "";
  const diff = row.deliveryDaysDiff;
  if (diff === 15) return "yellow";
  if (diff === 10) return "pink";
  if (diff === 2) return "purple";
  if (diff < 2) return "red";
  return "";
};
const bindRouteDialogVisible = ref(false);
const bindRouteLoading = ref(false);
const bindRouteSaving = ref(false);
const routeOptions = ref([]);
const bindProcessList = ref([]);
const userOptions = ref([]);
const userLoading = ref(false);
const bindForm = reactive({
  orderId: null,
  productId: null,
  productModelId: null,
  routeId: null,
  processUserList: [],
});
const resetBindProcessUsers = () => {
  bindProcessList.value = [];
  bindForm.processUserList = [];
};
const ensureUserOptions = () => {
  if (userOptions.value.length || userLoading.value) return;
  userLoading.value = true;
  userListNoPageByTenantId()
    .then(res => {
      userOptions.value = res.data || [];
    })
      .then(() => {
        proxy.download("/salesLedger/scheduling/export", {}, "生产订单.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
    .finally(() => {
      userLoading.value = false;
    });
};
  const handleConfirmRoute = () => {};
const createBindProcessUserList = list =>
  list.map(item => ({
    processId: item.id,
    processName: item.name || item.processName || item.no || "",
    userIds: [],
    userNames: "",
  }));
  onMounted(() => {
    getList();
const buildBindProcessRouteItems = () =>
  bindProcessList.value.map((item, index) => {
    const processUser = bindForm.processUserList[index] || {};
    return {
      productOrderId: bindForm.orderId,
      productRouteId: bindForm.routeId,
      processId: item.id,
      productModelId: bindForm.productModelId,
      dragSort: item.dragSort ?? index + 1,
      isQuality: item.isQuality ?? false,
      reportUserIds: Array.isArray(processUser.userIds) ? processUser.userIds.join(",") : "",
    };
  });
const fetchBindProcessList = async routeId => {
  if (!routeId) {
    resetBindProcessUsers();
    return;
  }
  try {
    const res = await findProcessRouteItemList({
      routeId,
         productModelId: bindForm.productModelId,
    });
    bindProcessList.value = res.data || [];
    bindForm.processUserList = createBindProcessUserList(bindProcessList.value);
    ensureUserOptions();
  } catch (error) {
    console.error("获取工序列表失败", error);
    proxy.$modal.msgError("获取工序列表失败");
    resetBindProcessUsers();
  }
};
const handleBindRouteChange = routeId => {
  fetchBindProcessList(routeId);
};
const handleBindProcessUserChange = (index, userIds) => {
  const selectedUsers = userOptions.value.filter(user => Array.isArray(userIds) && userIds.includes(user.userId));
  bindForm.processUserList[index].userNames = selectedUsers.map(user => user.nickName).join(",");
};
const removeBindProcessItem = index => {
  bindProcessList.value.splice(index, 1);
  bindForm.processUserList.splice(index, 1);
};
const openBindRouteDialog = async row => {
  bindForm.orderId = row.id;
  bindForm.productId = row.id ?? null;
  bindForm.productModelId = row.productModelId ?? null;
  bindForm.routeId = null;
  bindForm.processUserList = [];
  bindRouteDialogVisible.value = true;
  routeOptions.value = [];
  resetBindProcessUsers();
  if (!row.productModelId) {
    proxy.$modal.msgWarning("当前订单缺少产品型号,无法查询工艺路线");
    bindRouteDialogVisible.value = false;
    return;
  }
  bindRouteLoading.value = true;
  try {
    const res = await listProcessRoute({
      productId: bindForm.productId,
      productModelId: row.productModelId,
    });
    routeOptions.value = res.data || [];
  } catch (error) {
    console.error("获取工艺路线列表失败", error);
    proxy.$modal.msgError("获取工艺路线列表失败");
  } finally {
    bindRouteLoading.value = false;
  }
};
const handleBindRouteConfirm = async () => {
  if (!bindForm.routeId) {
    proxy.$modal.msgWarning("请选择工艺路线");
    return;
  }
  if (!bindForm.processUserList.length) {
    proxy.$modal.msgWarning("当前工艺路线下没有工序");
    return;
  }
  if (bindForm.processUserList.some(item => !Array.isArray(item.userIds) || item.userIds.length === 0)) {
    proxy.$modal.msgWarning("请为每道工序选择报工人员");
    return;
  }
  bindRouteSaving.value = true;
  try {
    await bindingRoute({
      id: bindForm.orderId,
      routeId: bindForm.routeId,
      processRouteItems: buildBindProcessRouteItems(),
      processUserList: bindForm.processUserList.map(item => ({
        ...item,
        userIds: item.userIds.join(","),
      })),
    });
    proxy.$modal.msgSuccess("绑定成功");
    bindRouteDialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("绑定工艺路线失败", error);
    proxy.$modal.msgError("绑定工艺路线失败");
  } finally {
    bindRouteSaving.value = false;
  }
};
const handleQuery = () => {
  page.current = 1;
  getList();
};
const pagination = obj => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  const params = { ...searchForm.value, ...page };
  params.entryDate = undefined;
  productOrderListPage(params)
    .then(res => {
      tableData.value = res.data.records;
      page.total = res.data.total;
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const showRouteItemModal = async row => {
  const orderId = row.id;
  try {
    const res = await getOrderProcessRouteMain(orderId);
    const detail = res.data || {};
    if (!detail.id) {
      proxy.$modal.msgWarning("未找到关联的工艺路线");
      return;
    }
    router.push({
      path: "/productionManagement/processRouteItem",
      query: {
        id: detail.id,
        processRouteCode: detail.processRouteCode || "",
        productName: detail.productName || "",
        model: detail.model || "",
        bomNo: detail.bomNo || "",
        description: detail.description || "",
        orderId,
        type: "order",
      },
    });
  } catch (error) {
    console.error("获取工艺路线信息失败", error);
    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 = () => {
  if (!selectedRows.value.length) {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  const ids = selectedRows.value.map(item => item.id);
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => delProductOrder(ids))
    .then(() => {
      proxy.$modal.msgSuccess("删除成功");
      getList();
    })
    .catch(() => {
      proxy.$modal.msg("已取消删除");
    });
};
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/productOrder/export", { ...searchForm.value }, "生产订单.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消导出");
    });
};
onMounted(() => {
  getList();
});
</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;
}
.process-user-list {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px;
  border-radius: 12px;
  background: #f7f9fc;
  border: 1px solid #e8eef5;
}
.process-user-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  padding: 12px 14px;
  border-radius: 10px;
  background: #fff;
  border: 1px solid #edf2f7;
}
.process-user-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.process-user-name {
  color: #1f2d3d;
  font-weight: 500;
  line-height: 1.4;
}
.process-user-remove {
  flex-shrink: 0;
  padding: 0;
}
.process-user-select {
  width: 100%;
}
@media (max-width: 768px) {
  .process-user-item {
    grid-template-columns: 1fr;
    gap: 10px;
  }
}
</style>