eae12ffeaa667f271a2c548b5e16557cac3686e3..58916670fe54367e9c9350596fb293787b73b425
2026-03-23 zhangwencui
Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventor...
589166 对比 | 目录
2026-03-23 zhangwencui
报工模块接口对接(还需调试)
9b9542 对比 | 目录
2026-03-23 zss
Merge remote-tracking branch 'origin/dev_银川_中盛建材' into dev_银川_中盛建材
3dee44 对比 | 目录
2026-03-23 zss
bom的产品结构添加单价
5448c9 对比 | 目录
2026-03-23 yyb
移除原材料和辅助费用显示,增加了每订单平均成本,更新计算公式
30c87e 对比 | 目录
2026-03-23 yyb
Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventor...
80adf0 对比 | 目录
2026-03-23 yyb
添加生成逻辑以支持标准和实际成本对比分析,优化数据结构和模板下载功能
b1d7de 对比 | 目录
2026-03-23 zhangwencui
新增报工产量信息计算,报工表格白班夜班区分
da7c76 对比 | 目录
2026-03-23 zhangwencui
生产报工模块修改
2b2bc2 对比 | 目录
2026-03-23 yyb
Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventor...
bd1012 对比 | 目录
2026-03-23 yyb
优化标准/实际成本对比分析页面的样式和功能,增加背景元素,改进KPI展示,支持自定义排序功能
fa281e 对比 | 目录
2026-03-23 zhangwencui
Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventor...
313285 对比 | 目录
2026-03-23 zhangwencui
工艺路线默认卡片视图
8293aa 对比 | 目录
2026-03-23 yyb
生产成本核算的明细样式优化
d69bd8 对比 | 目录
2026-03-23 yyb
标准/实际成本对比分析图表功能优化
3bc464 对比 | 目录
2026-03-23 yyb
标准/实际成本对比分析
9147b2 对比 | 目录
2026-03-23 yyb
Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventor...
384fb9 对比 | 目录
2026-03-23 yyb
生产成本核算
132c41 对比 | 目录
2026-03-23 zss
页面调整
83626a 对比 | 目录
2026-03-20 zhangwencui
生产报工页面更改
70703e 对比 | 目录
2026-03-20 zhangwencui
Merge branch 'dev_银川_中盛建材' of http://114.132.189.42:9002/r/product-inventor...
31baed 对比 | 目录
2026-03-20 zhangwencui
change
ed3604 对比 | 目录
2026-03-20 gongchunyi
fix: 点击取消时将表单子元素全部销毁
06d627 对比 | 目录
2026-03-20 zss
bom的规格型号非必填
7682db 对比 | 目录
2026-03-19 zhangwencui
8ba792 对比 | 目录
2026-03-19 zhangwencui
工艺路线查询功能
8448cb 对比 | 目录
2026-03-19 zhangwencui
工艺路线和BOM修改
ff1e38 对比 | 目录
2026-03-19 zhangwencui
BOM逻辑修改
de73a8 对比 | 目录
已添加3个文件
已修改25个文件
已删除3个文件
8660 ■■■■ 文件已修改
src/api/personnelManagement/class.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteItem.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productProcessRoute.js 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productStructure.js 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue 211 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 449 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/costAccounting/productionCostAccounting/index.vue 837 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/costAccounting/stdVsActCostAnalysis/index.vue 1440 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/officeEnergyConsumption/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/productionEnergyConsumption/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/classsSheduling/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/Edit.vue 174 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 315 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index2.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 799 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/StructureEdit.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 979 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 71 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/Input.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/Output.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/components/formDia.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 755 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/reportingDialog.vue 1706 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 142 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/class.js
@@ -71,6 +71,7 @@
    url: "/personalShift/export",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
src/api/productionManagement/processRouteItem.js
@@ -60,7 +60,7 @@
    data: data,
  });
}
// å·¥è‰ºè·¯çº¿å‚数修改
// å·¥è‰ºè·¯çº¿å‚数删除
export function delProcessRouteItemParam(id) {
  return request({
    url: `/ProcessRouteItemParam/remove/${id}`,
src/api/productionManagement/productProcessRoute.js
@@ -4,16 +4,17 @@
// åˆ—表查询
export function findProductProcessRouteItemList(query) {
  return request({
    url: "/productProcessRoute/list",
    url: `/productionOrderRouteItem/list/${query.orderId}`,
    method: "get",
    params: query,
  });
}
export function addOrUpdateProductProcessRouteItem(data) {
  return request({
    url: "/productProcessRoute/updateRouteItem",
    method: "post",
    url: "/productionOrderRouteItem/update",
    method: "put",
    data: data,
  });
}
@@ -21,7 +22,7 @@
// ç”Ÿäº§è®¢å•下:新增工艺路线项目
export function addRouteItem(data) {
  return request({
    url: "/productProcessRoute/addRouteItem",
    url: "/productionOrderRouteItem/add",
    method: "post",
    data,
  });
@@ -39,7 +40,7 @@
// åˆ é™¤å·¥è‰ºè·¯çº¿é¡¹ç›®ï¼ˆè·¯ç”±åŽæ‹¼æŽ¥ id)
export function deleteRouteItem(id) {
  return request({
    url: `/productProcessRoute/deleteRouteItem/${id}`,
    url: `/productionOrderRouteItem/delete/${id}`,
    method: "delete",
  });
}
@@ -47,8 +48,54 @@
// ç”Ÿäº§è®¢å•下:排序工艺路线项目
export function sortRouteItem(data) {
  return request({
    url: "/productProcessRoute/sortRouteItem",
    url: "/productionOrderRouteItem/sort",
    method: "post",
    data,
  });
}
// èŽ·å–å·¥åºå‚æ•°åˆ—è¡¨-生产订单
export function findProcessParamListOrder(query) {
  return request({
    url: `/productionOrderRouteItemParam/list`,
    method: "get",
    params: query,
  });
}
// å·¥è‰ºè·¯çº¿å‚数新增-生产订单
export function addProcessRouteItemParamOrder(data) {
  return request({
    url: "/productionOrderRouteItemParam/add",
    method: "post",
    data: data,
  });
}
// å·¥è‰ºè·¯çº¿å‚数修改-生产订单
export function editProcessRouteItemParamOrder(data) {
  return request({
    url: "/productionOrderRouteItemParam/update",
    method: "put",
    data: data,
  });
}
// å·¥è‰ºè·¯çº¿å‚数删除-生产订单
export function delProcessRouteItemParamOrder(id) {
  return request({
    url: `/productionOrderRouteItemParam/delete/${id}`,
    method: "delete",
  });
}
// ç”Ÿäº§æŠ¥å·¥-新增时查询
export function productionRecordAdd(id) {
  return request({
    url: "/productionRecord/add/" + id,
    method: "get",
  });
}
// ç”Ÿäº§æŠ¥å·¥-新增
export function productionRecordAddSubmit(data) {
  return request({
    url: "/productionRecord/add",
    method: "post",
    data: data,
  });
}
src/api/productionManagement/productStructure.js
@@ -11,8 +11,23 @@
export function add(data) {
  return request({
    url: "/productStructure",
    url: "/productStructure/"+data.bomId,
    method: "post",
    data: data,
    data: data.children,
  });
}
// åˆ†é¡µæŸ¥è¯¢-产品订单
export function queryList2(id) {
  return request({
    url: "/productionOrderStructure/getBomStructs/" + id,
    method: "get",
  });
}
export function add2(data) {
  return request({
    url: "/productionOrderStructure/addOrUpdateBomStructs/"+data.orderId,
    method: "put",
    data: data.children,
  });
}
src/api/productionManagement/productionOrder.js
@@ -30,7 +30,7 @@
// ç”Ÿäº§è®¢å•-绑定工艺路线
export function bindingRoute(data) {
  return request({
    url: "/productOrder/bindingRoute",
    url: "/appendix/bindingRoute",
    method: "post",
    data,
  });
@@ -44,14 +44,21 @@
    data: data,
  });
}
//生产订单删除
export function delProductOrder(ids) {
  return request({
    url: `/productOrder/${ids}`,
    method: "delete",
  });
}
//生产订单退回
export function revokeProductOrder(data) {
  return request({
    url: "/productOrder/revoke",
    method: "post",
    data: data,
  });
}
// ç”Ÿäº§è®¢å•-查询产品结构列表
export function listProcessBom(query) {
  return request({
@@ -129,4 +136,11 @@
    method: "post",
    data: data,
  });
}
export function getProductOrderSource(id) {
  return request({
    url: `/productOrder/productOrderSource/${id}`,
    method: "get",
  });
}
src/components/PIMTable/PIMTable.vue
@@ -22,7 +22,8 @@
    <el-table-column align="center"
                     type="selection"
                     width="55"
                     v-if="isSelection" />
                     v-if="isSelection"
                     :selectable="selectable" />
    <el-table-column align="center"
                     label="序号"
                     type="index"
@@ -311,6 +312,10 @@
      type: [String, Object],
      default: () => ({ width: "100%" }),
    },
    selectable: {
      type: Function,
      default: () => true,
    },
  });
  // Data
src/components/ProcessParamListDialog.vue
@@ -152,7 +152,9 @@
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired" />
              <el-switch :active-value="true"
                         :inactive-value="false"
                         v-model="selectedParam.isRequired" />
            </el-form-item>
          </el-form>
          <el-empty v-else
@@ -235,6 +237,12 @@
    editProcessRouteItemParam,
    addProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import {
    addProcessRouteItemParamOrder,
    delProcessRouteItemParamOrder,
    editProcessRouteItemParamOrder,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  const props = defineProps({
@@ -261,6 +269,14 @@
    editable: {
      type: Boolean,
      default: true,
    },
    orderId: {
      type: Number,
      default: 0,
    },
    pageType: {
      type: String,
      default: "route",
    },
  });
@@ -292,7 +308,7 @@
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
    isRequired: false,
    paramType: null,
    paramFormat: "",
    unit: "",
@@ -326,7 +342,7 @@
      minValue: param.minValue,
      maxValue: param.maxValue,
      sort: param.sort || 1,
      isRequired: param.isRequired || 0,
      isRequired: param.isRequired || false,
      paramType: param.parameterType || param.paramType,
      paramFormat: param.parameterFormat || param.paramFormat,
      unit: param.unit || param.unit,
@@ -343,15 +359,27 @@
    })
      .then(() => {
        // è°ƒç”¨API删除参数
        delProcessRouteItemParam(param.id)
          .then(res => {
            ElMessage.success("删除成功");
            emit("refresh");
          })
          .catch(err => {
            ElMessage.error("删除参数失败");
            console.error("删除参数失败:", err);
          });
        if (props.pageType === "order") {
          delProcessRouteItemParamOrder(param.id)
            .then(res => {
              ElMessage.success("删除成功");
              emit("refresh");
            })
            .catch(err => {
              ElMessage.error("删除参数失败");
              console.error("删除参数失败:", err);
            });
        } else {
          delProcessRouteItemParam(param.id)
            .then(res => {
              ElMessage.success("删除成功");
              emit("refresh");
            })
            .catch(err => {
              ElMessage.error("删除参数失败");
              console.error("删除参数失败:", err);
            });
        }
      })
      .catch(() => {});
  };
@@ -390,31 +418,63 @@
    }
    // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
    const isNumericMode = selectedParam.value.valueMode === 1;
    const isNumericMode = selectedParam.value.paramType == 1;
    console.log(isNumericMode, "isNumericMode");
    // è°ƒç”¨API新增参数
    addProcessRouteItemParam({
      routeItemId: props.process.id,
      paramId: selectedParam.value.id,
      standardValue: isNumericMode ? selectedParam.value.standardValue || "" : "",
      minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
      maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
      isRequired: selectedParam.value.isRequired || 0,
      sort: selectedParam.value.sort || 1,
    })
      .then(res => {
        if (res.code === 200) {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          emit("refresh");
        } else {
          ElMessage.error(res.msg || "添加参数失败");
        }
    if (props.pageType === "order") {
      addProcessRouteItemParamOrder({
        orderId: Number(props.orderId),
        // processId: props.process.id,
        routeItemId: props.process.id,
        // routeItemId: Number(props.routeId),
        paramId: selectedParam.value.id,
        standardValue: isNumericMode
          ? selectedParam.value.standardValue || ""
          : "",
        minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
        maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
        isRequired: selectedParam.value.isRequired || false,
        sort: selectedParam.value.sort || 1,
      })
      .catch(err => {
        ElMessage.error("添加参数失败");
        console.error("添加参数失败:", err);
      });
        .then(res => {
          if (res.code === 200) {
            ElMessage.success("添加参数成功");
            selectParamDialogVisible.value = false;
            emit("refresh");
          } else {
            ElMessage.error(res.msg || "添加参数失败");
          }
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    } else {
      addProcessRouteItemParam({
        routeItemId: props.process.id,
        paramId: selectedParam.value.id,
        standardValue: isNumericMode
          ? selectedParam.value.standardValue || ""
          : "",
        minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
        maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
        isRequired: selectedParam.value.isRequired || false,
        sort: selectedParam.value.sort || 1,
      })
        .then(res => {
          if (res.code === 200) {
            ElMessage.success("添加参数成功");
            selectParamDialogVisible.value = false;
            emit("refresh");
          } else {
            ElMessage.error(res.msg || "添加参数失败");
          }
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    }
  };
  // æäº¤ç¼–辑参数
@@ -423,33 +483,60 @@
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.value.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.value.id,
          routeItemId: props.process.id,
          paramId: editParamForm.value.paramId,
          standardValue: isNumericMode
            ? editParamForm.value.standardValue || ""
            : "",
          minValue: isNumericMode ? editParamForm.value.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.value.maxValue || 0 : null,
          isRequired: editParamForm.value.isRequired || 0,
        })
          .then(res => {
            if (res.code === 200) {
              ElMessage.success("编辑成功");
              editParamDialogVisible.value = false;
              emit("refresh");
            } else {
              ElMessage.error(res.msg || "编辑失败");
            }
        const isNumericMode = editParamForm.value.paramType == 1;
        console.log(isNumericMode, "isNumericMode");
        if (props.pageType === "order") {
          editProcessRouteItemParamOrder({
            id: editParamForm.value.id,
            // routeItemId: props.process.id,
            // paramId: editParamForm.value.paramId,
            standardValue: isNumericMode
              ? editParamForm.value.standardValue || ""
              : "",
            minValue: isNumericMode ? editParamForm.value.minValue || 0 : null,
            maxValue: isNumericMode ? editParamForm.value.maxValue || 0 : null,
            isRequired: editParamForm.value.isRequired || false,
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
            .then(res => {
              if (res.code === 200) {
                ElMessage.success("编辑成功");
                editParamDialogVisible.value = false;
                emit("refresh");
              } else {
                ElMessage.error(res.msg || "编辑失败");
              }
            })
            .catch(err => {
              ElMessage.error("编辑参数失败");
              console.error("编辑参数失败:", err);
            });
        } else {
          // è°ƒç”¨API修改参数
          editProcessRouteItemParam({
            id: editParamForm.value.id,
            routeItemId: props.process.id,
            paramId: editParamForm.value.paramId,
            standardValue: isNumericMode
              ? editParamForm.value.standardValue || ""
              : "",
            minValue: isNumericMode ? editParamForm.value.minValue || 0 : null,
            maxValue: isNumericMode ? editParamForm.value.maxValue || 0 : null,
            isRequired: editParamForm.value.isRequired || false,
          })
            .then(res => {
              if (res.code === 200) {
                ElMessage.success("编辑成功");
                editParamDialogVisible.value = false;
                emit("refresh");
              } else {
                ElMessage.error(res.msg || "编辑失败");
              }
            })
            .catch(err => {
              ElMessage.error("编辑参数失败");
              console.error("编辑参数失败:", err);
            });
        }
      }
    });
  };
@@ -497,7 +584,7 @@
          minValue: null,
          maxValue: null,
          sort: 1,
          isRequired: 0,
          isRequired: false,
          paramType: null,
          paramFormat: "",
          unit: "",
src/layout/components/Navbar.vue
@@ -1,39 +1,45 @@
<template>
  <div class="navbar">
    <div>
      <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container"
        @toggleClick="toggleSideBar" />
      <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
      <hamburger id="hamburger-container"
                 :is-active="appStore.sidebar.opened"
                 class="hamburger-container"
                 @toggleClick="toggleSideBar" />
      <breadcrumb v-if="!settingsStore.topNav"
                  id="breadcrumb-container"
                  class="breadcrumb-container" />
    </div>
    <!--    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />-->
    <div class="right-menu">
      <!-- æ¶ˆæ¯é€šçŸ¥ -->
      <el-popover
        v-model:visible="notificationVisible"
        :width="500"
        placement="bottom-end"
        trigger="click"
        :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
        popper-class="notification-popover"
      >
      <el-popover v-model:visible="notificationVisible"
                  :width="500"
                  placement="bottom-end"
                  trigger="click"
                  :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
                  popper-class="notification-popover">
        <template #reference>
          <div class="notification-container right-menu-item hover-effect">
            <el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notification-badge">
              <el-icon :size="20" style="cursor: pointer;">
            <el-badge :value="unreadCount"
                      :hidden="unreadCount === 0"
                      class="notification-badge">
              <el-icon :size="20"
                       style="cursor: pointer;">
                <Bell />
              </el-icon>
            </el-badge>
          </div>
        </template>
        <NotificationCenter
          @unreadCountChange="handleUnreadCountChange"
          ref="notificationCenterRef"
        />
        <NotificationCenter @unreadCountChange="handleUnreadCountChange"
                            ref="notificationCenterRef" />
      </el-popover>
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
        <el-dropdown @command="handleCommand"
                     class="right-menu-item hover-effect"
                     trigger="click">
          <div class="avatar-wrapper">
            <img :src="userStore.avatar" class="user-avatar" />
            <img :src="userStore.avatar"
                 class="user-avatar" />
            <el-icon><caret-bottom /></el-icon>
          </div>
          <template #dropdown>
@@ -41,10 +47,12 @@
              <router-link to="/user/profile">
                <el-dropdown-item>个人中心</el-dropdown-item>
              </router-link>
              <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
              <el-dropdown-item command="setLayout"
                                v-if="settingsStore.showSettings">
                <span>布局设置</span>
              </el-dropdown-item>
              <el-dropdown-item divided command="logout">
              <el-dropdown-item divided
                                command="logout">
                <span>退出登录</span>
              </el-dropdown-item>
            </el-dropdown-menu>
@@ -56,237 +64,238 @@
</template>
<script setup>
import { ElMessageBox } from 'element-plus'
import { Bell } from '@element-plus/icons-vue'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import NotificationCenter from './NotificationCenter/index.vue'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
  import { ElMessageBox } from "element-plus";
  import { Bell } from "@element-plus/icons-vue";
  import Breadcrumb from "@/components/Breadcrumb";
  import TopNav from "@/components/TopNav";
  import Hamburger from "@/components/Hamburger";
  import Screenfull from "@/components/Screenfull";
  import SizeSelect from "@/components/SizeSelect";
  import HeaderSearch from "@/components/HeaderSearch";
  import RuoYiGit from "@/components/RuoYi/Git";
  import RuoYiDoc from "@/components/RuoYi/Doc";
  import NotificationCenter from "./NotificationCenter/index.vue";
  import useAppStore from "@/store/modules/app";
  import useUserStore from "@/store/modules/user";
  import useSettingsStore from "@/store/modules/settings";
const appStore = useAppStore()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const notificationVisible = ref(false)
const notificationCenterRef = ref(null)
const unreadCount = ref(0)
function toggleSideBar() {
  appStore.toggleSideBar()
}
// const redirect = ref(undefined)
// watch(route, (newRoute) => {
//   redirect.value = newRoute.query && newRoute.query.redirect
// }, { immediate: true })
function handleCommand(command) {
  switch (command) {
    case "setLayout":
      setLayout()
      break
    case "logout":
      logout()
      break
    default:
      break
  const appStore = useAppStore();
  const userStore = useUserStore();
  const settingsStore = useSettingsStore();
  const notificationVisible = ref(false);
  const notificationCenterRef = ref(null);
  const unreadCount = ref(0);
  function toggleSideBar() {
    appStore.toggleSideBar();
  }
}
  // const redirect = ref(undefined)
  // watch(route, (newRoute) => {
  //   redirect.value = newRoute.query && newRoute.query.redirect
  // }, { immediate: true })
function logout() {
  ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    userStore.logOut().then(() => {
      location.href = '/index'
  function handleCommand(command) {
    switch (command) {
      case "setLayout":
        setLayout();
        break;
      case "logout":
        logout();
        break;
      default:
        break;
    }
  }
  function logout() {
    ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
  }).catch(() => { })
}
      .then(() => {
        userStore.logOut().then(() => {
          location.href = "/index";
        });
      })
      .catch(() => {});
  }
const emits = defineEmits(['setLayout'])
function setLayout() {
  emits('setLayout')
}
  const emits = defineEmits(["setLayout"]);
  function setLayout() {
    emits("setLayout");
  }
function toggleTheme() {
  settingsStore.toggleTheme()
}
  function toggleTheme() {
    settingsStore.toggleTheme();
  }
// æ¶ˆæ¯é€šçŸ¥ç›¸å…³
function handleUnreadCountChange(count) {
  unreadCount.value = count
}
  // æ¶ˆæ¯é€šçŸ¥ç›¸å…³
  function handleUnreadCountChange(count) {
    unreadCount.value = count;
  }
// ç»„件挂载时加载未读数量和定时刷新
let unreadCountTimer = null
onMounted(() => {
  // å»¶è¿ŸåŠ è½½ï¼Œç¡®ä¿ç»„ä»¶å·²æ¸²æŸ“
  nextTick(() => {
    if (notificationCenterRef.value) {
      notificationCenterRef.value.loadUnreadCount()
    }
  })
  // å®šæ—¶åˆ·æ–°æœªè¯»æ•°é‡ï¼ˆæ¯30秒)
  unreadCountTimer = setInterval(() => {
    if (notificationCenterRef.value) {
      notificationCenterRef.value.loadUnreadCount()
    }
  }, 30000)
})
// ç›‘听 popover æ˜¾ç¤ºçŠ¶æ€ï¼Œæ‰“å¼€æ—¶åŠ è½½æ¶ˆæ¯åˆ—è¡¨
watch(notificationVisible, (val) => {
  if (val && notificationCenterRef.value) {
  // ç»„件挂载时加载未读数量和定时刷新
  let unreadCountTimer = null;
  onMounted(() => {
    // å»¶è¿ŸåŠ è½½ï¼Œç¡®ä¿ç»„ä»¶å·²æ¸²æŸ“
    nextTick(() => {
      notificationCenterRef.value.loadMessages()
    })
  }
})
      if (notificationCenterRef.value) {
        notificationCenterRef.value.loadUnreadCount();
      }
    });
    // å®šæ—¶åˆ·æ–°æœªè¯»æ•°é‡ï¼ˆæ¯30秒)
    // unreadCountTimer = setInterval(() => {
    //   if (notificationCenterRef.value) {
    //     notificationCenterRef.value.loadUnreadCount()
    //   }
    // }, 30000)
  });
onUnmounted(() => {
  if (unreadCountTimer) {
    clearInterval(unreadCountTimer)
  }
})
  // ç›‘听 popover æ˜¾ç¤ºçŠ¶æ€ï¼Œæ‰“å¼€æ—¶åŠ è½½æ¶ˆæ¯åˆ—è¡¨
  watch(notificationVisible, val => {
    if (val && notificationCenterRef.value) {
      nextTick(() => {
        notificationCenterRef.value.loadMessages();
      });
    }
  });
  onUnmounted(() => {
    if (unreadCountTimer) {
      clearInterval(unreadCountTimer);
    }
  });
</script>
<style lang='scss' scoped>
.navbar {
  height: 50px;
  overflow: hidden;
  position: relative;
  background: var(--navbar-bg);
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  .navbar {
    height: 50px;
    overflow: hidden;
    position: relative;
    background: var(--navbar-bg);
    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background 0.3s;
    -webkit-tap-highlight-color: transparent;
    &:hover {
      background: rgba(0, 0, 0, 0.025);
    }
  }
  .breadcrumb-container {
    float: left;
  }
  .topmenu-container {
    position: absolute;
    left: 50px;
  }
  .errLog-container {
    display: inline-block;
    vertical-align: top;
  }
  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;
    display: flex;
    &:focus {
      outline: none;
    }
    .right-menu-item {
      display: inline-block;
      padding: 0 8px;
    .hamburger-container {
      line-height: 46px;
      height: 100%;
      font-size: 18px;
      color: var(--navbar-text);
      vertical-align: text-bottom;
      float: left;
      cursor: pointer;
      transition: background 0.3s;
      -webkit-tap-highlight-color: transparent;
      &.hover-effect {
        cursor: pointer;
        transition: background 0.3s;
      &:hover {
        background: rgba(0, 0, 0, 0.025);
      }
    }
        &:hover {
          background: rgba(0, 0, 0, 0.025);
    .breadcrumb-container {
      float: left;
    }
    .topmenu-container {
      position: absolute;
      left: 50px;
    }
    .errLog-container {
      display: inline-block;
      vertical-align: top;
    }
    .right-menu {
      float: right;
      height: 100%;
      line-height: 50px;
      display: flex;
      &:focus {
        outline: none;
      }
      .right-menu-item {
        display: inline-block;
        padding: 0 8px;
        height: 100%;
        font-size: 18px;
        color: var(--navbar-text);
        vertical-align: text-bottom;
        &.hover-effect {
          cursor: pointer;
          transition: background 0.3s;
          &:hover {
            background: rgba(0, 0, 0, 0.025);
          }
        }
        &.theme-switch-wrapper {
          display: flex;
          align-items: center;
          svg {
            transition: transform 0.3s;
            &:hover {
              transform: scale(1.15);
            }
          }
        }
      }
      &.theme-switch-wrapper {
      .notification-container {
        margin-right: 20px;
        display: flex;
        align-items: center;
        cursor: pointer;
        svg {
          transition: transform 0.3s;
        .notification-badge {
          :deep(.el-badge__content) {
            border: none;
          }
        }
      }
          &:hover {
            transform: scale(1.15);
      .avatar-container {
        margin-right: 40px;
        .avatar-wrapper {
          margin-top: 5px;
          position: relative;
          .user-avatar {
            cursor: pointer;
            width: 40px;
            height: 40px;
            border-radius: 50px;
          }
          i {
            cursor: pointer;
            position: absolute;
            right: -20px;
            top: 14px;
            font-size: 12px;
          }
        }
      }
    }
    .notification-container {
      margin-right: 20px;
      display: flex;
      align-items: center;
      cursor: pointer;
      .notification-badge {
        :deep(.el-badge__content) {
          border: none;
        }
      }
    }
    .avatar-container {
      margin-right: 40px;
      .avatar-wrapper {
        margin-top: 5px;
        position: relative;
        .user-avatar {
          cursor: pointer;
          width: 40px;
          height: 40px;
          border-radius: 50px;
        }
        i {
          cursor: pointer;
          position: absolute;
          right: -20px;
          top: 14px;
          font-size: 12px;
        }
      }
    }
  }
}
</style>
<style lang="scss">
.notification-popover {
  padding: 0 !important;
  .el-popover__title {
    display: none;
  }
  .el-popover__body {
  .notification-popover {
    padding: 0 !important;
    .el-popover__title {
      display: none;
    }
    .el-popover__body {
      padding: 0 !important;
    }
  }
}
.el-badge__content.is-fixed{
  top: 12px;
}
  .el-badge__content.is-fixed {
    top: 12px;
  }
</style>
src/views/basicData/customerFile/index.vue
@@ -784,7 +784,7 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: 250,
      width: 150,
      operation: [
        {
          name: "编辑",
@@ -800,20 +800,20 @@
            openDetailDialog(row);
          },
        },
        {
          name: "回访提醒",
          type: "text",
          clickFun: row => {
            openReminderDialog(row);
          },
        },
        {
          name: "添加洽谈进度",
          type: "text",
          clickFun: row => {
            openNegotiationDialog(row);
          },
        },
        // {
        //   name: "回访提醒",
        //   type: "text",
        //   clickFun: row => {
        //     openReminderDialog(row);
        //   },
        // },
        // {
        //   name: "添加洽谈进度",
        //   type: "text",
        //   clickFun: row => {
        //     openNegotiationDialog(row);
        //   },
        // },
      ],
    },
  ]);
src/views/basicData/product/index.vue
@@ -913,8 +913,8 @@
    gap: 20px;
  }
  .left {
    width: 465px;
    min-width: 465px;
    width: 35%;
    min-width: 35%;
    background: #ffffff;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
src/views/costAccounting/productionCostAccounting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,837 @@
<template>
  <div class="production-cost-page">
    <el-card class="filter-card" shadow="never">
      <template #header>
        <div class="card-head">
          <div class="card-head-left">
            <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
            <span class="card-title">生产成本核算</span>
            <span class="subtle">成本 = Î£ æŠ•入量 Ã— å¯¹åº”单价</span>
          </div>
          <div class="card-head-right">
            <el-radio-group
              v-model="statisticsType"
              size="small"
              @change="handleTypeChange"
            >
              <el-radio-button label="day">按日</el-radio-button>
              <el-radio-button label="month">按月</el-radio-button>
            </el-radio-group>
          </div>
        </div>
      </template>
      <div class="filter-layout">
        <el-form :model="searchForm" :inline="true" class="filter-form">
          <el-form-item label="时间范围">
            <el-date-picker
              v-if="statisticsType === 'day'"
              v-model="searchForm.dateRange"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="YYYY-MM-DD"
              class="w-260"
              @change="handleQuery"
            />
            <el-date-picker
              v-else
              v-model="searchForm.monthRange"
              type="monthrange"
              range-separator="至"
              start-placeholder="开始月份"
              end-placeholder="结束月份"
              value-format="YYYY-MM"
              class="w-260"
              @change="handleQuery"
            />
          </el-form-item>
          <el-form-item label="产品类别">
            <el-select
              v-model="searchForm.category"
              clearable
              filterable
              placeholder="全部类别"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in categoryOptions"
                :key="item"
                :label="item"
                :value="item"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="生产订单">
            <el-select
              v-model="searchForm.orderNo"
              clearable
              filterable
              placeholder="全部订单"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in orderOptions"
                :key="item"
                :label="item"
                :value="item"
              />
            </el-select>
          </el-form-item>
        </el-form>
        <div class="filter-actions">
          <el-button class="lux-btn" type="primary" @click="handleQuery">
            åˆ·æ–°
          </el-button>
          <el-button class="lux-btn" @click="handleReset">重置</el-button>
          <el-button class="lux-btn" type="success" plain @click="handleExport">
            å¯¼å‡º
          </el-button>
        </div>
      </div>
    </el-card>
    <el-card class="panel-card" shadow="never">
      <div class="kpi-strip">
        <div class="kpi-item kpi-total">
          <div class="kpi-label">总生产成本</div>
          <div class="kpi-value">Â¥{{ formatMoney(overview.totalCost) }}</div>
        </div>
        <div class="kpi-item kpi-avg">
          <div class="kpi-label">每订单平均成本</div>
          <div class="kpi-value">Â¥{{ formatMoney(overview.avgCostPerOrder) }}</div>
        </div>
        <div class="kpi-item kpi-order">
          <div class="kpi-label">订单数量</div>
          <div class="kpi-value">{{ overview.orderCount }}</div>
        </div>
      </div>
    </el-card>
    <el-row :gutter="14" class="summary-row">
      <el-col :span="12">
        <el-card class="table-card" shadow="never">
          <template #header>
            <div class="panel-head">
              <span class="card-title">按产品类别汇总</span>
            </div>
          </template>
          <el-table :data="categorySummary" stripe class="lux-table" height="260">
            <el-table-column prop="category" label="产品类别" min-width="140" />
            <el-table-column prop="totalCost" label="成本(元)" align="right">
              <template #default="scope">
                <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card class="table-card" shadow="never">
          <template #header>
            <div class="panel-head">
              <span class="card-title">按生产订单汇总</span>
            </div>
          </template>
          <el-table :data="orderSummary" stripe class="lux-table" height="260">
            <el-table-column prop="orderNo" label="生产订单" min-width="150" />
            <el-table-column prop="category" label="产品类别" min-width="120" />
            <el-table-column prop="totalCost" label="总成本(元)" align="right">
              <template #default="scope">
                <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>
    <el-card class="table-card" shadow="never">
      <template #header>
        <div class="panel-head">
          <span class="card-title">多维度汇总明细</span>
          <span class="subtle">{{ timeColumnLabel }} + äº§å“ç±»åˆ« + ç”Ÿäº§è®¢å•</span>
        </div>
      </template>
      <el-table :data="pagedTableData" stripe class="lux-table">
        <el-table-column prop="timeLabel" :label="timeColumnLabel" min-width="110" />
        <el-table-column prop="category" label="产品类别" min-width="120" />
        <el-table-column prop="orderNo" label="生产订单" min-width="150" />
        <el-table-column prop="totalCost" label="成本(元)" align="right">
          <template #default="scope">
            <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="拆分明细" width="92" fixed="right">
          <template #default="scope">
            <el-button link type="primary" @click="openDetail(scope.row)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="page.current"
          v-model:page-size="page.size"
          :page-sizes="[10, 20, 50, 100]"
          :total="tableData.length"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    <el-drawer
      v-model="detailVisible"
      :with-header="false"
      class="detail-drawer"
      size="760px"
      :close-on-click-modal="true"
      :close-on-press-escape="true"
      destroy-on-close
    >
      <div v-if="detailRow" class="drawer-head">
        <div class="meta-item">
          <span class="meta-label">{{ timeColumnLabel }}</span>
          <span class="meta-value">{{ detailRow.timeLabel }}</span>
        </div>
        <div class="meta-item">
          <span class="meta-label">产品类别</span>
          <span class="meta-value">{{ detailRow.category }}</span>
        </div>
        <div class="meta-item">
          <span class="meta-label">生产订单</span>
          <span class="meta-value">{{ detailRow.orderNo }}</span>
        </div>
      </div>
      <el-table :data="detailMaterials" class="lux-table" stripe>
        <el-table-column prop="materialName" label="物料名称" min-width="120" />
        <el-table-column prop="quantity" label="投入量" align="right" min-width="140">
          <template #default="scope">
            <span class="quantity-cell">
              <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span>
              <span class="quantity-unit">{{ scope.row.unit }}</span>
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="unitPrice" label="单价(元)" align="right">
          <template #default="scope">
            {{ formatNumber(scope.row.unitPrice, 2) }}
          </template>
        </el-table-column>
        <el-table-column prop="cost" label="成本(元)" align="right" min-width="132">
          <template #default="scope">
            <span class="cost-value no-wrap-money">Â¥{{ formatMoney(scope.row.cost) }}</span>
          </template>
        </el-table-column>
      </el-table>
      <div class="drawer-foot">
        <div class="foot-item total">
          <span class="foot-label">成本合计</span>
          <span class="foot-value no-wrap-money">Â¥{{ formatMoney(detailTotalCost) }}</span>
        </div>
      </div>
    </el-drawer>
  </div>
</template>
<script setup>
import { computed, reactive, ref, watch } from "vue";
import { DataLine } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
const statisticsType = ref("day");
const getDefaultDateRange = () => {
  const end = new Date();
  const start = new Date();
  start.setDate(start.getDate() - 6);
  return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)];
};
const getDefaultMonthRange = () => {
  const end = new Date();
  const start = new Date();
  start.setMonth(start.getMonth() - 2);
  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
};
const searchForm = reactive({
  dateRange: getDefaultDateRange(),
  monthRange: getDefaultMonthRange(),
  category: "",
  orderNo: "",
});
const sourceRecords = ref([
  { date: "2026-03-17", category: "瓷砖", orderNo: "PO-260317-01", materialName: "陶瓷粉", materialType: "原料", quantity: 1200, unit: "kg", unitPrice: 2.8 },
  { date: "2026-03-17", category: "瓷砖", orderNo: "PO-260317-01", materialName: "釉料", materialType: "辅料", quantity: 180, unit: "kg", unitPrice: 8.6 },
  { date: "2026-03-17", category: "æ°´æ³¥", orderNo: "PO-260317-02", materialName: "熟料", materialType: "原料", quantity: 2200, unit: "kg", unitPrice: 1.36 },
  { date: "2026-03-17", category: "æ°´æ³¥", orderNo: "PO-260317-02", materialName: "石膏", materialType: "辅料", quantity: 260, unit: "kg", unitPrice: 0.92 },
  { date: "2026-03-18", category: "砂浆", orderNo: "PO-260318-01", materialName: "机制砂", materialType: "原料", quantity: 1600, unit: "kg", unitPrice: 0.58 },
  { date: "2026-03-18", category: "砂浆", orderNo: "PO-260318-01", materialName: "保水剂", materialType: "辅料", quantity: 65, unit: "kg", unitPrice: 11.4 },
  { date: "2026-03-19", category: "瓷砖", orderNo: "PO-260319-01", materialName: "陶瓷粉", materialType: "原料", quantity: 980, unit: "kg", unitPrice: 2.9 },
  { date: "2026-03-19", category: "瓷砖", orderNo: "PO-260319-01", materialName: "色料", materialType: "辅料", quantity: 42, unit: "kg", unitPrice: 15.8 },
  { date: "2026-03-19", category: "砂浆", orderNo: "PO-260319-03", materialName: "机制砂", materialType: "原料", quantity: 1400, unit: "kg", unitPrice: 0.56 },
  { date: "2026-03-19", category: "砂浆", orderNo: "PO-260319-03", materialName: "减水剂", materialType: "辅料", quantity: 74, unit: "kg", unitPrice: 7.2 },
  { date: "2026-03-20", category: "æ°´æ³¥", orderNo: "PO-260320-02", materialName: "熟料", materialType: "原料", quantity: 2400, unit: "kg", unitPrice: 1.33 },
  { date: "2026-03-20", category: "æ°´æ³¥", orderNo: "PO-260320-02", materialName: "矿粉", materialType: "辅料", quantity: 380, unit: "kg", unitPrice: 1.08 },
]);
const normalizedRecords = computed(() =>
  sourceRecords.value.map((item) => {
    const month = item.date.slice(0, 7);
    const cost = Number(item.quantity) * Number(item.unitPrice);
    return { ...item, month, cost };
  })
);
const categoryOptions = computed(() =>
  Array.from(new Set(normalizedRecords.value.map((item) => item.category)))
);
const orderOptions = computed(() =>
  Array.from(new Set(normalizedRecords.value.map((item) => item.orderNo)))
);
const inRange = (value, range) => {
  if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true;
  return value >= range[0] && value <= range[1];
};
const getMonthRangeDays = (monthRange) => {
  if (!Array.isArray(monthRange) || monthRange.length !== 2 || !monthRange[0] || !monthRange[1]) {
    return 0;
  }
  const [startMonth, endMonth] = monthRange;
  const startDate = new Date(`${startMonth}-01T00:00:00`);
  const endDate = new Date(`${endMonth}-01T00:00:00`);
  if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) {
    return 0;
  }
  const endMonthLastDay = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0);
  const diffMs = endMonthLastDay.getTime() - startDate.getTime();
  return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1;
};
const buildQueryParams = () => {
  const isDay = statisticsType.value === "day";
  const params = {
    statisticsType: statisticsType.value,
    category: searchForm.category || undefined,
    orderNo: searchForm.orderNo || undefined,
  };
  if (isDay) {
    const [startDate, endDate] = searchForm.dateRange || [];
    params.startDate = startDate;
    params.endDate = endDate;
  } else {
    const [startMonth, endMonth] = searchForm.monthRange || [];
    params.startMonth = startMonth;
    params.endMonth = endMonth;
    params.days = getMonthRangeDays(searchForm.monthRange);
  }
  return params;
};
const filteredRecords = computed(() =>
  normalizedRecords.value.filter((item) => {
    const hitTime =
      statisticsType.value === "day"
        ? inRange(item.date, searchForm.dateRange)
        : inRange(item.month, searchForm.monthRange);
    const hitCategory = !searchForm.category || item.category === searchForm.category;
    const hitOrder = !searchForm.orderNo || item.orderNo === searchForm.orderNo;
    return hitTime && hitCategory && hitOrder;
  })
);
const timeColumnLabel = computed(() => (statisticsType.value === "day" ? "日期" : "月份"));
const aggregateBy = (list, keyFn) => {
  const map = new Map();
  for (const item of list) {
    const key = keyFn(item);
    if (!map.has(key)) {
      map.set(key, {
        totalCost: 0,
        materials: [],
      });
    }
    const bucket = map.get(key);
    bucket.totalCost += item.cost;
    bucket.materials.push(item);
  }
  return map;
};
const groupedMap = computed(() =>
  aggregateBy(filteredRecords.value, (item) => {
    const timeKey = statisticsType.value === "day" ? item.date : item.month;
    return `${timeKey}__${item.category}__${item.orderNo}`;
  })
);
const tableData = computed(() => {
  const rows = [];
  for (const [key, val] of groupedMap.value) {
    const [timeLabel, category, orderNo] = key.split("__");
    rows.push({
      key,
      timeLabel,
      category,
      orderNo,
      totalCost: val.totalCost,
      materials: val.materials,
    });
  }
  return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1));
});
const page = reactive({
  current: 1,
  size: 10,
});
const pagedTableData = computed(() => {
  const start = (page.current - 1) * page.size;
  return tableData.value.slice(start, start + page.size);
});
const categorySummary = computed(() => {
  const map = aggregateBy(filteredRecords.value, (item) => item.category);
  const rows = [];
  for (const [category, val] of map) {
    rows.push({
      category,
      totalCost: val.totalCost,
    });
  }
  return rows.sort((a, b) => b.totalCost - a.totalCost);
});
const orderSummary = computed(() => {
  const map = aggregateBy(filteredRecords.value, (item) => item.orderNo);
  const rows = [];
  for (const [orderNo, val] of map) {
    rows.push({
      orderNo,
      category: val.materials[0]?.category || "-",
      totalCost: val.totalCost,
    });
  }
  return rows.sort((a, b) => b.totalCost - a.totalCost);
});
const overview = computed(() => {
  const orderCount = new Set(filteredRecords.value.map((item) => item.orderNo)).size;
  const totalCost = filteredRecords.value.reduce((sum, item) => sum + item.cost, 0);
  return {
    totalCost,
    orderCount,
    avgCostPerOrder: orderCount === 0 ? 0 : totalCost / orderCount,
  };
});
const detailVisible = ref(false);
const detailRow = ref(null);
const detailMaterials = computed(() => detailRow.value?.materials || []);
const detailTotalCost = computed(() =>
  detailMaterials.value.reduce((sum, item) => sum + item.cost, 0)
);
const openDetail = (row) => {
  detailRow.value = row;
  detailVisible.value = true;
};
const handleTypeChange = () => {
  handleQuery();
};
const handleQuery = () => {
  page.current = 1;
  const queryParams = buildQueryParams();
  console.log("[productionCostAccounting] query params:", queryParams);
  ElMessage.success("已按条件完成汇总");
};
const handleReset = () => {
  searchForm.dateRange = getDefaultDateRange();
  searchForm.monthRange = getDefaultMonthRange();
  searchForm.category = "";
  searchForm.orderNo = "";
  handleQuery();
};
const handleSizeChange = (val) => {
  page.size = val;
  page.current = 1;
};
const handleCurrentChange = (val) => {
  page.current = val;
};
const handleExport = () => {
  const headers = [timeColumnLabel.value, "产品类别", "生产订单", "成本(元)"];
  const lines = tableData.value.map((row) =>
    [
      row.timeLabel,
      row.category,
      row.orderNo,
      row.totalCost.toFixed(2),
    ].join(",")
  );
  const csv = [headers.join(","), ...lines].join("\n");
  const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = `生产成本汇总_${statisticsType.value}_${Date.now()}.csv`;
  link.click();
  URL.revokeObjectURL(url);
  ElMessage.success("导出成功");
};
const formatMoney = (v) => {
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  return value.toLocaleString("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
};
const formatNumber = (v, digits = 2) => {
  const n = Number.parseFloat(v);
  if (!Number.isFinite(n)) return "--";
  return n.toLocaleString("zh-CN", {
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  });
};
watch(tableData, () => {
  const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
  if (page.current > maxPage) page.current = maxPage;
});
</script>
<style scoped lang="scss">
.production-cost-page {
  --lux-bg: #f6f7fb;
  --lux-card: rgba(255, 255, 255, 0.86);
  --lux-border: rgba(15, 23, 42, 0.08);
  --lux-text: rgba(15, 23, 42, 0.92);
  --lux-subtle: rgba(15, 23, 42, 0.58);
  --lux-muted: rgba(15, 23, 42, 0.38);
  --lux-primary: #2f6fed;
  --lux-success: #16a34a;
  --lux-warning: #f59e0b;
  --lux-danger: #ef4444;
  --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
  --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
  --lux-radius: 14px;
  padding: 18px 22px 24px;
  background: radial-gradient(
      1200px 420px at 20% 0%,
      rgba(47, 111, 237, 0.1),
      transparent 55%
    ),
    linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
}
.filter-card,
.panel-card,
.table-card {
  border-radius: var(--lux-radius);
  border-color: var(--lux-border);
  background: var(--lux-card);
  box-shadow: var(--lux-shadow-soft);
}
.filter-card {
  margin-bottom: 16px;
}
.panel-card,
.summary-row {
  margin-bottom: 14px;
}
.filter-layout {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}
.filter-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
}
.filter-form :deep(.el-form-item) {
  margin: 0;
}
.filter-actions {
  display: flex;
  gap: 10px;
}
.card-head,
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.card-head-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
.card-head-right {
  display: flex;
  align-items: center;
}
.card-icon {
  color: var(--lux-primary);
}
.card-title {
  font-weight: 760;
  color: var(--lux-text);
}
.subtle {
  color: var(--lux-subtle);
  font-size: 12px;
}
.kpi-strip {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
.kpi-item {
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid rgba(15, 23, 42, 0.08);
}
.kpi-total {
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86));
}
.kpi-raw {
  background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86));
}
.kpi-avg {
  background: linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(255, 255, 255, 0.86));
}
.kpi-order {
  background: linear-gradient(135deg, rgba(100, 116, 139, 0.1), rgba(255, 255, 255, 0.86));
}
.kpi-label {
  font-size: 12px;
  color: var(--lux-subtle);
}
.kpi-value {
  font-size: 22px;
  margin-top: 6px;
  font-weight: 780;
  color: var(--lux-text);
}
.price-value {
  font-weight: 700;
  color: var(--lux-success);
}
.cost-value {
  font-weight: 700;
  color: var(--lux-danger);
}
.no-wrap-money {
  display: inline-block;
  white-space: nowrap;
}
.drawer-head {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 12px;
}
.meta-item {
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--lux-border);
  background: rgba(15, 23, 42, 0.03);
  display: grid;
  gap: 4px;
}
.meta-label {
  font-size: 12px;
  color: var(--lux-subtle);
}
.meta-value {
  color: var(--lux-text);
  font-size: 16px;
  font-weight: 700;
}
.material-type-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 46px;
  height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
}
.material-type-tag.is-raw {
  color: #15803d;
  background: rgba(22, 163, 74, 0.12);
}
.material-type-tag.is-aux {
  color: #b45309;
  background: rgba(245, 158, 11, 0.16);
}
.quantity-value {
  font-weight: 700;
  color: var(--lux-text);
  margin-right: 6px;
}
.quantity-cell {
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
}
.quantity-unit {
  color: var(--lux-subtle);
}
.drawer-foot {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed var(--lux-border);
}
.foot-item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 16px;
  border-radius: 999px;
  border: 1px solid var(--lux-border);
  background: #fff;
}
.foot-label {
  color: var(--lux-subtle);
  font-size: 14px;
}
.foot-value {
  color: var(--lux-text);
  font-weight: 700;
  font-size: 16px;
}
.foot-item.total {
  border-color: rgba(47, 111, 237, 0.26);
  background: rgba(47, 111, 237, 0.08);
}
.foot-item.total .foot-value {
  color: #1e3a8a;
  font-size: 18px;
  font-weight: 800;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  padding-top: 12px;
}
.strong {
  font-weight: 800;
}
.w-260 {
  width: 260px;
}
.w-180 {
  width: 180px;
}
::deep(.lux-table) {
  border-radius: 12px;
  overflow: hidden;
}
::deep(.lux-table th.el-table__cell) {
  background: rgba(15, 23, 42, 0.03);
}
::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
  background-color: rgba(47, 111, 237, 0.06) !important;
}
@media (max-width: 1100px) {
  .kpi-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .filter-layout {
    flex-direction: column;
  }
  .drawer-head {
    grid-template-columns: 1fr;
  }
  .drawer-foot {
    justify-content: flex-start;
    flex-wrap: wrap;
  }
}
</style>
src/views/costAccounting/stdVsActCostAnalysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1440 @@
<template>
  <div class="std-cost-page">
    <div class="page-bg" aria-hidden="true">
      <div class="bg-mesh" />
      <div class="bg-orb bg-orb--a" />
      <div class="bg-orb bg-orb--b" />
      <div class="bg-orb bg-orb--c" />
      <div class="bg-grid" />
    </div>
    <div class="page-inner">
    <el-card class="filter-card glass-card" shadow="never">
      <template #header>
        <div class="card-head">
          <div class="card-head-left">
            <div class="title-badge">
              <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
            </div>
            <div class="title-block">
              <div class="title-row">
                <span class="card-title shimmer-text">标准/实际成本对比分析</span>
                <span class="live-pill">实时分析</span>
              </div>
              <span class="subtle">差异 = å®žé™…成本 âˆ’ æ ‡å‡†æˆæœ¬</span>
            </div>
          </div>
        </div>
      </template>
      <div class="filter-layout">
        <el-form :model="searchForm" :inline="true" class="filter-form">
          <el-form-item label="月份范围">
            <el-date-picker
              v-model="searchForm.monthRange"
              type="monthrange"
              range-separator="至"
              start-placeholder="开始月份"
              end-placeholder="结束月份"
              value-format="YYYY-MM"
              class="w-260"
              @change="handleQuery"
            />
          </el-form-item>
          <el-form-item label="产品类别">
            <el-select
              v-model="searchForm.category"
              clearable
              filterable
              placeholder="全部类别"
              class="w-180"
              @change="handleQuery"
            >
              <el-option
                v-for="item in categoryOptions"
                :key="item"
                :label="item"
                :value="item"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="成本类型">
            <el-select
              v-model="searchForm.costType"
              clearable
              placeholder="全部类型"
              class="w-180"
              @change="handleQuery"
            >
              <el-option label="能耗成本" value="能耗成本" />
              <el-option label="生产成本" value="生产成本" />
            </el-select>
          </el-form-item>
        </el-form>
        <div class="filter-actions">
          <div class="action-group">
            <el-button class="lux-btn" type="primary" @click="handleQuery">刷新</el-button>
            <el-button class="lux-btn" @click="handleReset">重置</el-button>
          </div>
          <div class="action-group">
            <el-dropdown trigger="click" @command="handleImportCommand">
              <el-button class="lux-btn" type="success" plain>
                æ ‡å‡†æˆæœ¬å¯¼å…¥
                <el-icon class="el-icon--right"><ArrowDown /></el-icon>
              </el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="template">下载导入模板</el-dropdown-item>
                  <el-dropdown-item command="upload">Excel å¯¼å…¥</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
            <el-upload
              ref="uploadRef"
              class="hidden-upload"
              :auto-upload="false"
              :show-file-list="false"
              accept=".xlsx,.xls"
              :on-change="handleFileChange"
            />
          </div>
        </div>
      </div>
    </el-card>
    <el-card class="panel-card glass-card kpi-card" shadow="never">
      <div class="kpi-strip">
        <div class="kpi-item kpi-std">
          <div class="kpi-top">
            <el-icon class="kpi-ico"><Histogram /></el-icon>
            <div class="kpi-label">标准成本合计</div>
          </div>
          <div class="kpi-value">Â¥{{ formatMoney(overview.standardCost) }}</div>
          <div class="kpi-glow" />
        </div>
        <div class="kpi-item kpi-act">
          <div class="kpi-top">
            <el-icon class="kpi-ico"><TrendCharts /></el-icon>
            <div class="kpi-label">实际成本合计</div>
          </div>
          <div class="kpi-value">Â¥{{ formatMoney(overview.actualCost) }}</div>
          <div class="kpi-glow" />
        </div>
        <div class="kpi-item kpi-diff">
          <div class="kpi-top">
            <el-icon class="kpi-ico"><Switch /></el-icon>
            <div class="kpi-label">差异合计</div>
          </div>
          <div class="kpi-value" :class="overview.diff >= 0 ? 'cost-value' : 'ok-value'">
            Â¥{{ formatMoney(overview.diff) }}
          </div>
          <div class="kpi-glow" />
        </div>
        <div class="kpi-item kpi-rate">
          <div class="kpi-top">
            <el-icon class="kpi-ico"><PieChart /></el-icon>
            <div class="kpi-label">差异率</div>
          </div>
          <div class="kpi-value">{{ formatPercent(overview.diffRate) }}</div>
          <div class="kpi-glow" />
        </div>
      </div>
    </el-card>
    <el-card class="table-card glass-card chart-section" shadow="never">
      <template #header>
        <div class="panel-head">
          <div class="panel-head-main">
            <span class="panel-accent" />
            <div>
              <span class="card-title">标准/实际成本可视化</span>
              <span class="chart-tag">柱状 Â· æŠ˜çº¿</span>
            </div>
          </div>
          <span class="subtle">支持按月份、产品类别、成本类型筛选</span>
        </div>
      </template>
      <div class="chart-wrap">
        <div class="chart-tools chart-tools-inline" @click.stop>
          <button class="chart-tool chart-tool--primary" type="button" @click="openLargeChart">
            <el-icon><ZoomIn /></el-icon>
            æŸ¥çœ‹å¤§å›¾
          </button>
          <button class="chart-tool" type="button" @click="downloadChartImage">
            <el-icon><Download /></el-icon>
            ä¸‹è½½å›¾è¡¨
          </button>
        </div>
        <div ref="chartRef" class="chart-content"></div>
      </div>
    </el-card>
    <el-dialog
      v-model="largeChartVisible"
      title="标准/实际成本对比大图"
      width="88%"
      top="6vh"
      append-to-body
      destroy-on-close
      @opened="initLargeChart"
      @closed="disposeLargeChart"
    >
      <div ref="largeChartRef" class="large-chart-content"></div>
    </el-dialog>
    <el-card class="table-card glass-card" shadow="never">
      <template #header>
        <div class="panel-head">
          <div class="panel-head-main">
            <span class="panel-accent panel-accent--emerald" />
            <span class="card-title">对比明细</span>
          </div>
          <span class="count-chip">共 {{ tableData.length }} æ¡</span>
        </div>
      </template>
      <el-table :data="pagedTableData" stripe class="lux-table" @sort-change="handleSortChange">
        <el-table-column prop="month" label="月份" width="110" />
        <el-table-column prop="category" label="产品类别" min-width="140" />
        <el-table-column prop="costType" label="成本类型" min-width="120" />
        <el-table-column prop="standardCost" label="标准成本(元)" sortable="custom" align="right">
          <template #default="scope">Â¥{{ formatMoney(scope.row.standardCost) }}</template>
        </el-table-column>
        <el-table-column prop="actualCost" label="实际成本(元)" sortable="custom" align="right">
          <template #default="scope">Â¥{{ formatMoney(scope.row.actualCost) }}</template>
        </el-table-column>
        <el-table-column prop="diff" label="差异(元)" sortable="custom" align="right">
          <template #default="scope">
            <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'">
              {{ formatSignedMoney(scope.row.diff) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="diffRate" label="差异率" sortable="custom" align="right">
          <template #default="scope">
            <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'">
              {{ formatPercent(scope.row.diffRate) }}
            </span>
          </template>
        </el-table-column>
      </el-table>
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="page.current"
          v-model:page-size="page.size"
          :page-sizes="[10, 20, 50, 100]"
          :total="tableData.length"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </el-card>
    </div>
  </div>
</template>
<script setup>
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import {
  ArrowDown,
  DataLine,
  Download,
  Histogram,
  PieChart,
  Switch,
  TrendCharts,
  ZoomIn,
} from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import * as echarts from "echarts";
import * as XLSX from "xlsx";
const getDefaultMonthRange = () => {
  const end = new Date();
  const start = new Date();
  start.setMonth(start.getMonth() - 2);
  return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
};
const searchForm = reactive({
  monthRange: getDefaultMonthRange(),
  category: "",
  costType: "",
});
const uploadRef = ref();
const chartRef = ref(null);
const largeChartRef = ref(null);
let chartInstance = null;
let largeChartInstance = null;
const largeChartVisible = ref(false);
const currentChartOption = ref(null);
// ------------------------------
// å‡æ•°æ®ï¼šç”¨äºŽå…ˆè”调页面渲染
// ------------------------------
const actualCostSource = ref([]);
const standardCostSource = ref([]);
const fakeMonths = ["2026-01", "2026-02", "2026-03"];
const fakeCategories = [
  "粉煤灰",
  "石灰",
  "æ°´æ³¥",
  "铝粉膏",
  "脱模剂",
  "石膏",
  "打包带",
  "防腐剂(板材用)",
  "氧化镁(板材用)",
  "冷挤丝(板材用)",
  "卡扣(板材用)",
  "材料小计",
  "æ°´",
  "电",
  "蒸汽",
];
const fakeCostType = (category) => (["æ°´", "电", "蒸汽"].includes(category) ? "能耗成本" : "生产成本");
// æ¯ä¸ªç±»åˆ«çš„æ ‡å‡†æˆæœ¬åŸºå‡†å€¼ï¼ˆä»…用于假数据)
const baseStandardCostByCategory = {
  ç²‰ç…¤ç°: 98000,
  çŸ³ç°: 52000,
  æ°´æ³¥: 175000,
  é“ç²‰è†: 32000,
  è„±æ¨¡å‰‚: 21000,
  çŸ³è†: 41000,
  æ‰“包带: 14500,
  "防腐剂(板材用)": 12500,
  "氧化镁(板材用)": 22000,
  "冷挤丝(板材用)": 9800,
  "卡扣(板材用)": 8600,
  ææ–™å°è®¡: 420000,
  æ°´: 6800,
  ç”µ: 26000,
  è’¸æ±½: 52000,
};
// æœˆä»½æ³¢åŠ¨ç³»æ•°ï¼ˆè®©å›¾è¡¨çœ‹èµ·æ¥æ›´â€œçœŸå®žâ€ä¸€äº›ï¼‰
const monthFactorByMonth = {
  "2026-01": 1.0,
  "2026-02": 1.06,
  "2026-03": 0.97,
};
// å®žé™…成本相对标准成本的偏移比例(用于测试正负差异展示)
const diffRatioByCategory = {
  ç²‰ç…¤ç°: 0.05,
  çŸ³ç°: -0.01,
  æ°´æ³¥: 0.03,
  é“ç²‰è†: 0.0,
  è„±æ¨¡å‰‚: -0.04,
  çŸ³è†: 0.02,
  æ‰“包带: -0.03,
  "防腐剂(板材用)": 0.06,
  "氧化镁(板材用)": -0.02,
  "冷挤丝(板材用)": 0.01,
  "卡扣(板材用)": -0.05,
  ææ–™å°è®¡: 0.02,
  æ°´: -0.01,
  ç”µ: 0.04,
  è’¸æ±½: -0.03,
};
const buildFakeSources = () => {
  const stdRows = [];
  const actRows = [];
  for (const month of fakeMonths) {
    const monthFactor = monthFactorByMonth[month] ?? 1;
    const monthAdj = month === "2026-02" ? 0.005 : month === "2026-03" ? -0.006 : 0;
    for (const category of fakeCategories) {
      const costType = fakeCostType(category);
      const base = baseStandardCostByCategory[category] ?? 0;
      const standardCost = Math.round(base * monthFactor);
      const diffRatio = (diffRatioByCategory[category] ?? 0) + monthAdj;
      const actualCost = Math.round(standardCost * (1 + diffRatio));
      stdRows.push({ month, category, costType, standardCost });
      actRows.push({ month, category, costType, actualCost });
    }
  }
  standardCostSource.value = stdRows;
  actualCostSource.value = actRows;
};
buildFakeSources();
const categoryOptions = computed(() => {
  const all = [...actualCostSource.value, ...standardCostSource.value];
  return Array.from(new Set(all.map((item) => item.category)));
});
const inRange = (value, range) => {
  if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true;
  return value >= range[0] && value <= range[1];
};
const mergedRows = computed(() => {
  const key = (item) => `${item.month}__${item.category}__${item.costType}`;
  const stdMap = new Map(standardCostSource.value.map((item) => [key(item), item]));
  const actMap = new Map(actualCostSource.value.map((item) => [key(item), item]));
  const keySet = new Set([...stdMap.keys(), ...actMap.keys()]);
  const rows = [];
  for (const k of keySet) {
    const std = stdMap.get(k);
    const act = actMap.get(k);
    const month = std?.month || act?.month || "";
    const category = std?.category || act?.category || "";
    const costType = std?.costType || act?.costType || "";
    const standardCost = Number(std?.standardCost || 0);
    const actualCost = Number(act?.actualCost || 0);
    const diff = actualCost - standardCost;
    const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
    rows.push({ month, category, costType, standardCost, actualCost, diff, diffRate });
  }
  return rows.sort((a, b) => {
    if (a.month !== b.month) return a.month > b.month ? 1 : -1;
    if (a.category !== b.category) return a.category.localeCompare(b.category, "zh-Hans-CN");
    return a.costType.localeCompare(b.costType, "zh-Hans-CN");
  });
});
const tableData = computed(() =>
  mergedRows.value.filter((item) => {
    const hitMonth = inRange(item.month, searchForm.monthRange);
    const hitCategory = !searchForm.category || item.category === searchForm.category;
    const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
    return hitMonth && hitCategory && hitCostType;
  })
);
const page = reactive({
  current: 1,
  size: 10,
});
/** sortable="custom" éœ€åœ¨ sort-change é‡Œè‡ªè¡ŒæŽ’序,再分页 */
const tableSort = reactive({
  prop: "",
  order: "",
});
const handleSortChange = ({ prop, order }) => {
  tableSort.prop = prop || "";
  tableSort.order = order || "";
  page.current = 1;
};
const sortedTableData = computed(() => {
  const rows = [...tableData.value];
  if (!tableSort.prop || !tableSort.order) return rows;
  const dir = tableSort.order === "ascending" ? 1 : -1;
  const key = tableSort.prop;
  rows.sort((a, b) => {
    const na = Number(a[key]);
    const nb = Number(b[key]);
    const va = Number.isFinite(na) ? na : 0;
    const vb = Number.isFinite(nb) ? nb : 0;
    if (va === vb) return 0;
    return va < vb ? -dir : dir;
  });
  return rows;
});
const pagedTableData = computed(() => {
  const start = (page.current - 1) * page.size;
  return sortedTableData.value.slice(start, start + page.size);
});
const overview = computed(() => {
  const standardCost = tableData.value.reduce((sum, item) => sum + item.standardCost, 0);
  const actualCost = tableData.value.reduce((sum, item) => sum + item.actualCost, 0);
  const diff = actualCost - standardCost;
  const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
  return { standardCost, actualCost, diff, diffRate };
});
const getChartData = () => {
  const xAxis = tableData.value.map(
    (item) => `${item.month}\n${item.category}-${item.costType.replace("成本", "")}`
  );
  const standard = tableData.value.map((item) => item.standardCost);
  const actual = tableData.value.map((item) => item.actualCost);
  const diffRate = tableData.value.map((item) => Number(item.diffRate.toFixed(2)));
  return { xAxis, standard, actual, diffRate };
};
const buildChartOption = () => {
  const { xAxis, standard, actual, diffRate } = getChartData();
  return {
    animation: true,
    animationDuration: 920,
    animationEasing: "cubicOut",
    textStyle: { fontFamily: "inherit" },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "cross",
        crossStyle: { color: "rgba(47, 111, 237, 0.35)" },
        lineStyle: { type: "dashed" },
      },
      backgroundColor: "rgba(255, 255, 255, 0.94)",
      borderColor: "rgba(47, 111, 237, 0.22)",
      borderWidth: 1,
      padding: [12, 14],
      textStyle: { color: "rgba(15, 23, 42, 0.88)" },
      extraCssText: "box-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); border-radius: 12px;",
      formatter: (params) => {
        const row = tableData.value[params[0]?.dataIndex] || {};
        return [
          `${row.month || ""} ${row.category || ""} ${row.costType || ""}`,
          `标准成本:¥${formatMoney(row.standardCost || 0)}`,
          `实际成本:¥${formatMoney(row.actualCost || 0)}`,
          `差异:${formatSignedMoney(row.diff || 0)}`,
          `差异率:${formatPercent(row.diffRate || 0)}`,
        ].join("<br/>");
      },
    },
    legend: {
      data: ["标准成本", "实际成本", "差异率"],
      top: 6,
      itemGap: 18,
      textStyle: { color: "rgba(15, 23, 42, 0.72)" },
    },
    grid: { left: "3%", right: "3%", top: "18%", bottom: "14%", containLabel: true },
    xAxis: {
      type: "category",
      data: xAxis,
      axisLabel: { color: "rgba(15, 23, 42, 0.62)", fontSize: 11 },
      axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.1)" } },
      axisTick: { show: false },
    },
    yAxis: [
      {
        type: "value",
        name: "成本(元)",
        nameTextStyle: { color: "rgba(15, 23, 42, 0.5)", fontSize: 11 },
        axisLabel: { color: "rgba(15, 23, 42, 0.55)" },
        splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)", type: "dashed" } },
      },
      {
        type: "value",
        name: "差异率(%)",
        nameTextStyle: { color: "rgba(15, 23, 42, 0.5)", fontSize: 11 },
        axisLabel: { color: "rgba(15, 23, 42, 0.55)" },
        splitLine: { show: false },
      },
    ],
    series: [
      {
        name: "标准成本",
        type: "bar",
        barMaxWidth: 26,
        data: standard,
        itemStyle: {
          borderRadius: [6, 6, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#8eb4ff" },
            { offset: 1, color: "#3d74f5" },
          ]),
        },
        emphasis: {
          itemStyle: {
            shadowBlur: 12,
            shadowColor: "rgba(61, 116, 245, 0.45)",
          },
        },
      },
      {
        name: "实际成本",
        type: "bar",
        barMaxWidth: 26,
        data: actual,
        itemStyle: {
          borderRadius: [6, 6, 0, 0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#fcd34d" },
            { offset: 1, color: "#ea580c" },
          ]),
        },
        emphasis: {
          itemStyle: {
            shadowBlur: 12,
            shadowColor: "rgba(234, 88, 12, 0.4)",
          },
        },
      },
      {
        name: "差异率",
        type: "line",
        yAxisIndex: 1,
        smooth: true,
        symbol: "circle",
        symbolSize: 7,
        showSymbol: true,
        data: diffRate,
        lineStyle: { width: 3, shadowBlur: 8, shadowColor: "rgba(239, 68, 68, 0.35)" },
        itemStyle: {
          color: "#ef4444",
          borderColor: "#fff",
          borderWidth: 2,
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(239, 68, 68, 0.28)" },
            { offset: 1, color: "rgba(239, 68, 68, 0.02)" },
          ]),
        },
      },
    ],
  };
};
const updateChart = () => {
  const option = buildChartOption();
  currentChartOption.value = option;
  chartInstance?.setOption(option);
  largeChartInstance?.setOption(option);
};
const normalizeCostType = (value) => {
  const text = String(value || "").trim();
  if (!text) return "";
  if (text.includes("能耗")) return "能耗成本";
  if (text.includes("生产")) return "生产成本";
  return text;
};
const parseImportedRows = (rows) => {
  const normalized = rows
    .map((item) => {
      const month = String(item["月份"] || item.month || "").slice(0, 7);
      const category = String(item["产品类别"] || item.category || "").trim();
      const costType = normalizeCostType(item["成本类型"] || item.costType);
      const standardCost = Number(item["标准成本"] ?? item.standardCost ?? 0);
      return { month, category, costType, standardCost };
    })
    .filter((item) => item.month && item.category && item.costType && Number.isFinite(item.standardCost));
  return normalized;
};
const replaceStandardSourceByImport = (importRows) => {
  const map = new Map();
  for (const item of importRows) {
    const k = `${item.month}__${item.category}__${item.costType}`;
    map.set(k, item);
  }
  standardCostSource.value = Array.from(map.values());
};
const handleFileChange = async (uploadFile) => {
  try {
    const file = uploadFile.raw;
    if (!file) return;
    const data = await file.arrayBuffer();
    const workbook = XLSX.read(data, { type: "array" });
    const sheetName = workbook.SheetNames[0];
    const sheet = workbook.Sheets[sheetName];
    const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" });
    const parsed = parseImportedRows(rows);
    if (!parsed.length) {
      ElMessage.warning("导入失败:模板内容为空或字段不匹配");
      return;
    }
    replaceStandardSourceByImport(parsed);
    ElMessage.success(`导入成功:${parsed.length} æ¡æ ‡å‡†æˆæœ¬è®°å½•`);
    handleQuery();
  } catch (error) {
    console.error(error);
    ElMessage.error("导入失败,请检查 Excel æ ¼å¼");
  } finally {
    uploadRef.value?.clearFiles?.();
  }
};
const openUploadSelector = () => {
  const input = uploadRef.value?.$el?.querySelector?.("input[type='file']");
  if (!input) {
    ElMessage.warning("上传组件尚未就绪,请稍后重试");
    return;
  }
  input.click();
};
const handleImportCommand = (command) => {
  if (command === "template") {
    downloadTemplate();
    return;
  }
  if (command === "upload") {
    openUploadSelector();
  }
};
const downloadTemplate = () => {
  const sample = [
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "粉煤灰", æˆæœ¬ç±»åž‹: "标准生产成本", æ ‡å‡†æˆæœ¬: 98000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "æ°´æ³¥", æˆæœ¬ç±»åž‹: "标准生产成本", æ ‡å‡†æˆæœ¬: 175000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "电", æˆæœ¬ç±»åž‹: "标准能耗成本", æ ‡å‡†æˆæœ¬: 26000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "蒸汽", æˆæœ¬ç±»åž‹: "标准能耗成本", æ ‡å‡†æˆæœ¬: 52000 },
    { æœˆä»½: "2026-03", äº§å“ç±»åˆ«: "æ°´", æˆæœ¬ç±»åž‹: "标准能耗成本", æ ‡å‡†æˆæœ¬: 6800 },
  ];
  const ws = XLSX.utils.json_to_sheet(sample);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, "标准成本模板");
  XLSX.writeFile(wb, "标准成本按月导入模板.xlsx");
  ElMessage.success("模板已下载");
};
const handleQuery = () => {
  updateChart();
};
const handleReset = () => {
  searchForm.monthRange = getDefaultMonthRange();
  searchForm.category = "";
  searchForm.costType = "";
  tableSort.prop = "";
  tableSort.order = "";
  page.current = 1;
  handleQuery();
};
const handleSizeChange = (val) => {
  page.size = val;
  page.current = 1;
};
const handleCurrentChange = (val) => {
  page.current = val;
};
const formatMoney = (v) => {
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  return value.toLocaleString("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
};
const formatSignedMoney = (v) => {
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  const sign = value >= 0 ? "+" : "";
  return `${sign}Â¥${formatMoney(value)}`;
};
const formatPercent = (v) => {
  const n = Number.parseFloat(v);
  const value = Number.isFinite(n) ? n : 0;
  const sign = value >= 0 ? "+" : "";
  return `${sign}${value.toFixed(2)}%`;
};
const handleResize = () => {
  chartInstance?.resize?.();
  largeChartInstance?.resize?.();
};
const openLargeChart = () => {
  if (!tableData.value.length) {
    ElMessage.warning("暂无图表数据可查看");
    return;
  }
  largeChartVisible.value = true;
};
const initLargeChart = () => {
  nextTick(() => {
    if (!largeChartRef.value) return;
    if (!largeChartInstance) {
      largeChartInstance = echarts.init(largeChartRef.value);
    }
    if (currentChartOption.value) {
      largeChartInstance.setOption(currentChartOption.value);
    } else {
      updateChart();
    }
    // å¼¹çª—出现后容器尺寸会变化,强制 resize é˜²æ­¢ canvas æº¢å‡ºé®æŒ¡è¡¨å¤´/关闭按钮
    largeChartInstance?.resize?.();
  });
};
const disposeLargeChart = () => {
  largeChartInstance?.dispose?.();
  largeChartInstance = null;
};
const downloadChartImage = () => {
  const sourceChart = chartInstance || largeChartInstance;
  if (!sourceChart) {
    ElMessage.warning("图表尚未加载完成");
    return;
  }
  const url = sourceChart.getDataURL({
    type: "png",
    pixelRatio: 2,
    backgroundColor: "#ffffff",
  });
  const link = document.createElement("a");
  link.href = url;
  link.download = `标准实际成本对比图_${new Date().toISOString().slice(0, 10)}.png`;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  ElMessage.success("图表下载成功");
};
onMounted(() => {
  nextTick(() => {
    if (chartRef.value && !chartInstance) {
      chartInstance = echarts.init(chartRef.value);
    }
    updateChart();
  });
  window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
  window.removeEventListener("resize", handleResize);
  chartInstance?.dispose?.();
  chartInstance = null;
  disposeLargeChart();
});
watch(tableData, () => {
  const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
  if (page.current > maxPage) page.current = maxPage;
  nextTick(updateChart);
});
</script>
<style scoped lang="scss">
@keyframes mesh-shift {
  0%,
  100% {
    opacity: 1;
    transform: scale(1) translate(0, 0);
  }
  50% {
    opacity: 0.85;
    transform: scale(1.02) translate(-1%, 1%);
  }
}
@keyframes orb-float {
  0%,
  100% {
    transform: translate(0, 0) scale(1);
  }
  33% {
    transform: translate(12px, -18px) scale(1.05);
  }
  66% {
    transform: translate(-8px, 10px) scale(0.98);
  }
}
@keyframes shimmer {
  0% {
    background-position: 200% center;
  }
  100% {
    background-position: -200% center;
  }
}
.std-cost-page {
  --lux-bg: #eef1f8;
  --lux-card: rgba(255, 255, 255, 0.72);
  --lux-border: rgba(15, 23, 42, 0.1);
  --lux-text: rgba(15, 23, 42, 0.92);
  --lux-subtle: rgba(15, 23, 42, 0.58);
  --lux-primary: #2f6fed;
  --lux-success: #16a34a;
  --lux-warning: #f59e0b;
  --lux-danger: #ef4444;
  --lux-shadow-soft: 0 12px 40px rgba(15, 23, 42, 0.08);
  --lux-shadow-lift: 0 20px 50px rgba(47, 111, 237, 0.12);
  --lux-radius: 16px;
  position: relative;
  min-height: 100%;
  padding: 20px 22px 28px;
  overflow: hidden;
  background: linear-gradient(165deg, #e8ecf7 0%, #f4f6fb 42%, #fafbfd 100%);
}
.page-bg {
  pointer-events: none;
  position: absolute;
  inset: 0;
  z-index: 0;
}
.bg-mesh {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(ellipse 80% 50% at 15% 0%, rgba(47, 111, 237, 0.18), transparent 50%),
    radial-gradient(ellipse 60% 45% at 85% 15%, rgba(245, 158, 11, 0.12), transparent 45%),
    radial-gradient(ellipse 50% 40% at 50% 100%, rgba(22, 163, 74, 0.08), transparent 50%);
  animation: mesh-shift 14s ease-in-out infinite;
}
.bg-orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.55;
  animation: orb-float 18s ease-in-out infinite;
}
.bg-orb--a {
  width: 320px;
  height: 320px;
  top: -80px;
  right: 5%;
  background: rgba(47, 111, 237, 0.35);
}
.bg-orb--b {
  width: 260px;
  height: 260px;
  bottom: 10%;
  left: -40px;
  background: rgba(99, 102, 241, 0.28);
  animation-delay: -6s;
}
.bg-orb--c {
  width: 200px;
  height: 200px;
  top: 40%;
  right: 25%;
  background: rgba(245, 158, 11, 0.22);
  animation-delay: -12s;
}
.bg-grid {
  position: absolute;
  inset: 0;
  opacity: 0.35;
  background-image:
    linear-gradient(rgba(15, 23, 42, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(15, 23, 42, 0.04) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, black 20%, transparent 75%);
}
.page-inner {
  position: relative;
  z-index: 1;
}
.glass-card {
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(255, 255, 255, 0.65);
  box-shadow: var(--lux-shadow-soft), inset 0 1px 0 rgba(255, 255, 255, 0.85);
  transition: box-shadow 0.35s ease, transform 0.35s ease;
}
.glass-card:hover {
  box-shadow: var(--lux-shadow-lift), inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.filter-card,
.panel-card,
.table-card {
  border-radius: var(--lux-radius);
  border-color: var(--lux-border);
  background: var(--lux-card);
}
.filter-card,
.panel-card,
.table-card {
  margin-bottom: 16px;
}
.title-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 14px;
  background: linear-gradient(145deg, rgba(47, 111, 237, 0.2), rgba(47, 111, 237, 0.06));
  box-shadow: 0 8px 24px rgba(47, 111, 237, 0.15);
}
.card-icon {
  font-size: 22px;
  color: var(--lux-primary);
}
.title-block {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.title-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}
.shimmer-text {
  background: linear-gradient(
    90deg,
    var(--lux-text) 0%,
    var(--lux-text) 40%,
    rgba(47, 111, 237, 0.95) 50%,
    var(--lux-text) 60%,
    var(--lux-text) 100%
  );
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shimmer 5s linear infinite;
}
.live-pill {
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.04em;
  padding: 4px 10px;
  border-radius: 999px;
  color: #0d47a1;
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.18), rgba(47, 111, 237, 0.06));
  border: 1px solid rgba(47, 111, 237, 0.22);
  box-shadow: 0 2px 10px rgba(47, 111, 237, 0.12);
}
.filter-layout {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.filter-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  align-items: center;
}
.filter-form :deep(.el-form-item) {
  margin: 0;
}
.filter-form :deep(.el-input__wrapper),
.filter-form :deep(.el-select .el-input__wrapper) {
  border-radius: 10px;
  box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
  transition: box-shadow 0.2s ease;
}
.filter-form :deep(.el-input__wrapper:hover) {
  box-shadow: 0 4px 14px rgba(47, 111, 237, 0.1);
}
.filter-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  padding-top: 12px;
  border-top: 1px dashed rgba(15, 23, 42, 0.12);
}
.filter-actions :deep(.lux-btn) {
  border-radius: 10px;
  font-weight: 600;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.filter-actions :deep(.lux-btn:hover) {
  transform: translateY(-1px);
  box-shadow: 0 8px 20px rgba(47, 111, 237, 0.18);
}
.filter-actions :deep(.el-button--success.is-plain) {
  border-color: rgba(22, 163, 74, 0.35);
}
.action-group {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.filter-actions :deep(.el-upload) {
  display: inline-flex;
}
.hidden-upload {
  width: 0;
  height: 0;
  overflow: hidden;
}
.card-head,
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.card-head-left {
  display: flex;
  align-items: center;
  gap: 14px;
}
.panel-head-main {
  display: flex;
  align-items: center;
  gap: 12px;
}
.panel-accent {
  width: 4px;
  height: 22px;
  border-radius: 4px;
  background: linear-gradient(180deg, #5b8cff, #2f6fed);
  box-shadow: 0 2px 8px rgba(47, 111, 237, 0.35);
}
.panel-accent--emerald {
  background: linear-gradient(180deg, #34d399, #059669);
  box-shadow: 0 2px 8px rgba(5, 150, 105, 0.35);
}
.chart-tag {
  margin-left: 10px;
  font-size: 11px;
  font-weight: 650;
  color: rgba(15, 23, 42, 0.55);
  padding: 2px 8px;
  border-radius: 6px;
  background: rgba(15, 23, 42, 0.05);
}
.count-chip {
  font-size: 12px;
  font-weight: 650;
  color: var(--lux-primary);
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(47, 111, 237, 0.1);
  border: 1px solid rgba(47, 111, 237, 0.15);
}
.card-title {
  font-weight: 760;
  color: var(--lux-text);
  letter-spacing: -0.02em;
}
.subtle {
  color: var(--lux-subtle);
  font-size: 12px;
}
.kpi-card {
  padding: 2px;
}
.kpi-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
}
.kpi-item {
  position: relative;
  padding: 16px 16px 14px;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.7);
  overflow: hidden;
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease;
}
.kpi-item:hover {
  transform: translateY(-4px);
  box-shadow: 0 16px 36px rgba(15, 23, 42, 0.1);
}
.kpi-glow {
  position: absolute;
  right: -20%;
  bottom: -40%;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  filter: blur(40px);
  opacity: 0.45;
  pointer-events: none;
}
.kpi-std .kpi-glow {
  background: rgba(47, 111, 237, 0.55);
}
.kpi-act .kpi-glow {
  background: rgba(245, 158, 11, 0.5);
}
.kpi-diff .kpi-glow {
  background: rgba(239, 68, 68, 0.45);
}
.kpi-rate .kpi-glow {
  background: rgba(22, 163, 74, 0.5);
}
.kpi-top {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.kpi-ico {
  font-size: 18px;
  opacity: 0.88;
}
.kpi-std {
  background: linear-gradient(145deg, rgba(47, 111, 237, 0.14), rgba(255, 255, 255, 0.92));
}
.kpi-std .kpi-ico {
  color: #2563eb;
}
.kpi-act {
  background: linear-gradient(145deg, rgba(245, 158, 11, 0.16), rgba(255, 255, 255, 0.92));
}
.kpi-act .kpi-ico {
  color: #d97706;
}
.kpi-diff {
  background: linear-gradient(145deg, rgba(239, 68, 68, 0.12), rgba(255, 255, 255, 0.92));
}
.kpi-diff .kpi-ico {
  color: #dc2626;
}
.kpi-rate {
  background: linear-gradient(145deg, rgba(22, 163, 74, 0.12), rgba(255, 255, 255, 0.92));
}
.kpi-rate .kpi-ico {
  color: #059669;
}
.kpi-label {
  font-size: 12px;
  color: var(--lux-subtle);
  font-weight: 600;
}
.kpi-value {
  position: relative;
  z-index: 1;
  font-size: 22px;
  font-weight: 780;
  color: var(--lux-text);
  font-variant-numeric: tabular-nums;
}
.cost-value {
  color: var(--lux-danger);
  font-weight: 700;
}
.ok-value {
  color: var(--lux-success);
  font-weight: 700;
}
.chart-section :deep(.el-card__header) {
  border-bottom: 1px solid rgba(15, 23, 42, 0.06);
}
.chart-wrap {
  position: relative;
  padding-top: 40px;
  border-radius: 14px;
  overflow: hidden;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(248, 250, 252, 0.95) 100%);
  border: 1px solid rgba(15, 23, 42, 0.06);
}
.chart-content {
  height: 380px;
}
.chart-tools {
  display: flex;
  align-items: center;
  gap: 8px;
}
.chart-tools-inline {
  position: absolute;
  top: 6px;
  right: 8px;
  z-index: 2;
}
.chart-tool {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 650;
  line-height: 1;
  padding: 8px 12px;
  border-radius: 10px;
  border: 1px solid rgba(15, 23, 42, 0.1);
  background: rgba(255, 255, 255, 0.88);
  color: rgba(15, 23, 42, 0.75);
  cursor: pointer;
  transition:
    background-color 0.2s ease,
    border-color 0.2s ease,
    transform 0.2s ease,
    box-shadow 0.2s ease;
}
.chart-tool--primary {
  border-color: rgba(47, 111, 237, 0.28);
  background: linear-gradient(135deg, rgba(47, 111, 237, 0.12), rgba(255, 255, 255, 0.95));
  color: #1e40af;
}
.chart-tool:hover {
  background: rgba(47, 111, 237, 0.1);
  border-color: rgba(47, 111, 237, 0.28);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(47, 111, 237, 0.12);
}
.large-chart-content {
  height: 70vh;
  min-height: 520px;
  width: 100%;
  overflow: hidden; // é˜²æ­¢ ECharts ç”»å¸ƒæº¢å‡ºé®æŒ¡å¼¹çª—标题栏
}
.std-cost-page :deep(.el-dialog__header) {
  position: relative;
  z-index: 3;
}
.std-cost-page :deep(.el-dialog__headerbtn) {
  position: relative;
  z-index: 4;
}
.std-cost-page :deep(.el-dialog__body) {
  position: relative;
  z-index: 1;
}
.pagination-container {
  display: flex;
  justify-content: flex-end;
  padding-top: 14px;
}
.w-260 {
  width: 260px;
}
.w-180 {
  width: 180px;
}
::deep(.lux-table) {
  border-radius: 12px;
  overflow: hidden;
  --el-table-border-color: rgba(15, 23, 42, 0.06);
}
::deep(.lux-table th.el-table__cell) {
  background: linear-gradient(180deg, rgba(15, 23, 42, 0.04), rgba(15, 23, 42, 0.02));
  font-weight: 700;
  color: rgba(15, 23, 42, 0.75);
}
::deep(.lux-table .el-table__row) {
  transition: background-color 0.2s ease;
}
::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
  background-color: rgba(47, 111, 237, 0.07) !important;
}
@media (max-width: 1100px) {
  .kpi-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .filter-actions {
    justify-content: flex-start;
    padding-top: 8px;
  }
  .shimmer-text {
    animation: none;
    color: var(--lux-text);
    background: none;
    -webkit-background-clip: unset;
    background-clip: unset;
  }
}
@media (prefers-reduced-motion: reduce) {
  .bg-mesh,
  .bg-orb,
  .shimmer-text {
    animation: none;
  }
  .shimmer-text {
    color: var(--lux-text);
    background: none;
    -webkit-background-clip: unset;
    background-clip: unset;
  }
  .kpi-item:hover {
    transform: none;
  }
}
</style>
src/views/energyManagement/officeEnergyConsumption/index.vue
@@ -1049,7 +1049,6 @@
  }
  .search_form {
    :deep(.el-form-item) {
      margin-bottom: 0px !important;
    }
  }
</style>
src/views/energyManagement/productionEnergyConsumption/index.vue
@@ -1049,7 +1049,6 @@
  }
  .search_form {
    :deep(.el-form-item) {
      margin-bottom: 0px !important;
    }
  }
</style>
src/views/personnelManagement/classsSheduling/index.vue
@@ -974,7 +974,7 @@
  .user-stats {
    /* display: flex; */
    /* flex-wrap: wrap;
                                                                                                                                                                                                                                                                                      gap: 10px; */
                                                                                                                                                                                                                                                                                        gap: 10px; */
    margin-bottom: 4px;
  }
src/views/productionManagement/processRoute/Edit.vue
@@ -8,21 +8,25 @@
               :model="formState"
               label-position="top"
               ref="formRef">
        <el-form-item label="产品名称"
                      prop="productModelId"
        <el-form-item label="产品类型"
                      prop="dictCode"
                      :rules="[
                {
                required: true,
                message: '请选择产品',
                message: '请选择产品类型',
                trigger: 'change',
              }
            ]">
          <el-button type="primary"
                     @click="showProductSelectDialog = true">
            {{ formState.productName && formState.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
          <el-select v-model="formState.dictCode"
                     placeholder="请选择产品类型"
                     clearable
                     style="width: 100%"
                     @change="handleProductTypeChange">
            <el-option v-for="item in productTypeOptions"
                       :key="item.dictCode"
                       :label="item.dictLabel"
                       :value="item.dictCode" />
          </el-select>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId"
@@ -36,7 +40,7 @@
          <el-select v-model="formState.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!formState.productModelId || bomOptions.length === 0"
                     :disabled="!formState.dictCode || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
@@ -50,10 +54,7 @@
                    type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog v-model="showProductSelectDialog"
                           @confirm="handleProductSelect"
                           single />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
@@ -75,8 +76,8 @@
    watch,
  } from "vue";
  import { update } from "@/api/productionManagement/processRoute.js";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import { listPage } from "@/api/productionManagement/productBom.js";
  import { getDicts } from "@/api/system/dict/data.js";
  const props = defineProps({
    visible: {
@@ -94,10 +95,8 @@
  // å“åº”式数据(替代选项式的 data)
  const formState = ref({
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    dictCode: undefined,
    dictLabel: "",
    bomId: undefined,
    description: "",
  });
@@ -111,10 +110,48 @@
    },
  });
  const showProductSelectDialog = ref(false);
  const productTypeOptions = ref([]);
  const bomOptions = ref([]);
  let { proxy } = getCurrentInstance();
  // èŽ·å–äº§å“ç±»åž‹å­—å…¸
  const getProductTypeOptions = () => {
    getDicts("product_type")
      .then(res => {
        if (res.code === 200) {
          productTypeOptions.value = res.data;
        }
      })
      .catch(err => {
        console.error("获取产品类型字典失败:", err);
      });
  };
  // æ ¹æ®äº§å“ç±»åž‹èŽ·å–BOM列表
  const getBomListByProductType = async dictCode => {
    if (!dictCode) {
      bomOptions.value = [];
      return;
    }
    try {
      // ä½¿ç”¨listPage接口,根据dictCode查询BOM
      const res = await listPage({ dictCode });
      // å¤„理返回的BOM数据
      let bomList = [];
      if (res.data && res.data.records) {
        bomList = res.data.records;
      }
      bomOptions.value = bomList;
      if (bomList.length === 0) {
        proxy.$modal.msgError("该产品类型没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品类型没有BOM,请先创建BOM");
      bomOptions.value = [];
    }
  };
  const closeModal = () => {
    isShow.value = false;
@@ -125,94 +162,42 @@
    if (props.record) {
      formState.value = {
        ...props.record,
        productId: props.record.productId,
        productModelId: props.record.productModelId,
        productName: props.record.productName || "",
        // æ³¨æ„ï¼šrecord中的字段是model,需要映射到productModelName
        productModelName:
          props.record.model || props.record.productModelName || "",
        dictCode: props.record.dictCode,
        dictLabel: props.record.dictLabel || "",
        bomId: props.record.bomId,
        description: props.record.description || "",
      };
      // å¦‚果有产品型号ID,加载BOM列表
      if (props.record.productModelId) {
        loadBomList(props.record.productModelId);
      // å¦‚果有产品类型,加载BOM列表
      if (props.record.dictCode) {
        getBomListByProductType(props.record.dictCode);
      }
    }
  };
  // åŠ è½½BOM列表
  const loadBomList = async productModelId => {
    if (!productModelId) {
      bomOptions.value = [];
      return;
    }
    try {
      const res = await getByModel(productModelId);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === "object") {
        bomList = [res];
  // äº§å“ç±»åž‹é€‰æ‹©å¤„理
  const handleProductTypeChange = async dictCode => {
    if (dictCode) {
      const selectedType = productTypeOptions.value.find(item => item.dictCode === dictCode);
      if (selectedType) {
        formState.value.dictLabel = selectedType.dictLabel;
      }
      bomOptions.value = bomList;
    } catch (error) {
      console.error("加载BOM列表失败:", error);
      // å¦‚果当前选择的BOM不在新列表中,则重置BOM选择
      formState.value.bomId = undefined;
      await getBomListByProductType(dictCode);
      // è§¦å‘表单验证更新
      proxy.$refs["formRef"]?.validateField("dictCode");
    } else {
      formState.value.dictLabel = "";
      bomOptions.value = [];
    }
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      // å…ˆæŸ¥è¯¢BOM列表(必选)
      try {
        const res = await getByModel(product.id);
        // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
        let bomList = [];
        if (Array.isArray(res)) {
          bomList = res;
        } else if (res && res.data) {
          bomList = Array.isArray(res.data) ? res.data : [res.data];
        } else if (res && typeof res === "object") {
          bomList = [res];
        }
        if (bomList.length > 0) {
          formState.value.productModelId = product.id;
          formState.value.productName = product.productName;
          formState.value.productModelName = product.model;
          // å¦‚果当前选择的BOM不在新列表中,则重置BOM选择
          const currentBomExists = bomList.some(
            bom => bom.id === formState.value.bomId
          );
          if (!currentBomExists) {
            formState.value.bomId = undefined;
          }
          bomOptions.value = bomList;
          showProductSelectDialog.value = false;
          // è§¦å‘表单验证更新
          proxy.$refs["formRef"]?.validateField("productModelId");
        } else {
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      } catch (error) {
        // å¦‚果接口返回404或其他错误,说明没有BOM
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    }
  };
  const handleSubmit = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
        if (!formState.value.productModelId) {
          proxy.$modal.msgError("请选择产品");
        // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“ç±»åž‹å’ŒBOM
        if (!formState.value.dictCode) {
          proxy.$modal.msgError("请选择产品类型");
          return;
        }
        if (!formState.value.bomId) {
@@ -250,6 +235,7 @@
  );
  onMounted(() => {
    getProductTypeOptions();
    if (props.visible && props.record) {
      setFormData();
    }
src/views/productionManagement/processRoute/New.vue
@@ -1,71 +1,67 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="新增工艺路线"
        width="400"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="产品名称"
            prop="productModelId"
            :rules="[
    <el-dialog v-model="isShow"
               title="新增工艺路线"
               width="400"
               @close="closeModal">
      <el-form label-width="140px"
               :model="formState"
               label-position="top"
               ref="formRef">
        <el-form-item label="产品类型"
                      prop="dictCode"
                      :rules="[
                {
                required: true,
                message: '请选择产品',
                message: '请选择产品类型',
                trigger: 'change',
              }
            ]"
        >
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ formState.productName && formState.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
            ]">
          <el-select v-model="formState.dictCode"
                     placeholder="请选择产品类型"
                     clearable
                     style="width: 100%"
                     @change="handleProductTypeChange">
            <el-option v-for="item in productTypeOptions"
                       :key="item.dictCode"
                       :label="item.dictLabel"
                       :value="item.dictCode" />
          </el-select>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
        <el-form-item label="BOM"
                      prop="bomId"
                      :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
          <el-select
              v-model="formState.bomId"
              placeholder="请选择BOM"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
              style="width: 100%"
          >
            <el-option
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
            />
            ]">
          <el-select v-model="formState.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!formState.dictCode || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="formState.description"
                    type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog
          v-model="showProductSelectDialog"
          @confirm="handleProductSelect"
          single
      />
      <ProductSelectDialog v-model="showProductSelectDialog"
                           @confirm="handleProductSelect"
                           single />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button type="primary"
                     @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
@@ -74,121 +70,142 @@
</template>
<script setup>
import {ref, computed, getCurrentInstance} from "vue";
import {add} from "@/api/productionManagement/processRoute.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import { ref, computed, getCurrentInstance, onMounted } from "vue";
  import { add } from "@/api/productionManagement/processRoute.js";
  import { listPage } from "@/api/productionManagement/productBom.js";
  import { getDicts } from "@/api/system/dict/data.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
});
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
  });
const emit = defineEmits(['update:visible', 'completed']);
  const emit = defineEmits(["update:visible", "completed"]);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
let { proxy } = getCurrentInstance()
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  formState.value = {
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
  // å“åº”式数据(替代选项式的 data)
  const formState = ref({
    dictCode: undefined,
    dictLabel: "",
    bomId: undefined,
    description: '',
  };
  bomOptions.value = [];
  isShow.value = false;
};
    description: "",
  });
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const productTypeOptions = ref([]);
  const bomOptions = ref([]);
  let { proxy } = getCurrentInstance();
  // èŽ·å–äº§å“ç±»åž‹å­—å…¸
  const getProductTypeOptions = () => {
    getDicts("product_type")
      .then(res => {
        if (res.code === 200) {
          productTypeOptions.value = res.data;
        }
      })
      .catch(err => {
        console.error("获取产品类型字典失败:", err);
      });
  };
  // æ ¹æ®äº§å“ç±»åž‹èŽ·å–BOM列表
  const getBomListByProductType = async dictCode => {
    if (!dictCode) {
      bomOptions.value = [];
      return;
    }
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      // ä½¿ç”¨listPage接口,根据dictCode查询BOM
      const res = await listPage({ dictCode });
      // å¤„理返回的BOM数据
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
      if (res.data && res.data.records) {
        bomList = res.data.records;
      }
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        formState.value.bomId = undefined; // é‡ç½®BOM选择
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      bomOptions.value = bomList;
      if (bomList.length === 0) {
        proxy.$modal.msgError("该产品类型没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      proxy.$modal.msgError("该产品类型没有BOM,请先创建BOM");
      bomOptions.value = [];
    }
  }
};
  };
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
  const closeModal = () => {
    // é‡ç½®è¡¨å•数据
    formState.value = {
      dictCode: undefined,
      dictLabel: "",
      bomId: undefined,
      description: "",
    };
    bomOptions.value = [];
    isShow.value = false;
  };
  // äº§å“ç±»åž‹é€‰æ‹©å¤„理
  const handleProductTypeChange = async dictCode => {
    if (dictCode) {
      const selectedType = productTypeOptions.value.find(
        item => item.dictCode === dictCode
      );
      if (selectedType) {
        formState.value.dictLabel = selectedType.dictLabel;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
        return;
      }
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
      formState.value.bomId = undefined; // é‡ç½®BOM选择
      await getBomListByProductType(dictCode);
      // è§¦å‘表单验证更新
      proxy.$refs["formRef"]?.validateField("dictCode");
    } else {
      formState.value.dictLabel = "";
      bomOptions.value = [];
    }
  })
};
  };
  const handleSubmit = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“ç±»åž‹å’ŒBOM
        if (!formState.value.dictCode) {
          proxy.$modal.msgError("请选择产品类型");
          return;
        }
        if (!formState.value.bomId) {
          proxy.$modal.msgError("请选择BOM");
          return;
        }
        add(formState.value).then(res => {
          // å…³é—­æ¨¡æ€æ¡†
          isShow.value = false;
          // å‘ŠçŸ¥çˆ¶ç»„件已完成
          emit("completed");
          proxy.$modal.msgSuccess("提交成功");
        });
      }
    });
  };
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
  // ç»„件挂载时获取产品类型字典
  onMounted(() => {
    getProductTypeOptions();
  });
  defineExpose({
    closeModal,
    handleSubmit,
    isShow,
  });
</script>
src/views/productionManagement/processRoute/index.vue
@@ -3,13 +3,29 @@
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="规格名称:">
          <el-input v-model="searchForm.model"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        <el-form-item label="产品类型:">
          <el-select v-model="searchForm.dictCode"
                     style="width: 200px;"
                     placeholder="请选择产品类型"
                     clearable
                     @change="handleQuery">
            <el-option v-for="option in productTypeOptions"
                       :key="option.dictCode"
                       :label="option.dictLabel"
                       :value="option.dictCode" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态:">
          <el-select v-model="searchForm.status"
                     style="width: 200px;"
                     placeholder="请选择状态"
                     clearable
                     @change="handleQuery">
            <el-option label="已批准"
                       :value="true" />
            <el-option label="草稿"
                       :value="false" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
@@ -63,11 +79,13 @@
  } from "@/api/productionManagement/processRoute.js";
  import { useRouter } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
  import { getDicts } from "@/api/system/dict/data";
  const router = useRouter();
  const data = reactive({
    searchForm: {
      model: "",
      dictCode: "",
      status: "",
    },
  });
  const { searchForm } = toRefs(data);
@@ -75,14 +93,33 @@
    {
      label: "工艺路线编号",
      prop: "processRouteCode",
      width: "200px",
      className: "status-cell",
    },
    {
      label: "产品名称",
      prop: "productName",
      label: "状态",
      prop: "status",
      dataType: "tag",
      formatData: params => {
        if (params) {
          return "已批准";
        } else {
          return "草稿";
        }
      },
      formatType: params => {
        if (params) {
          return "success";
        } else {
          return "info";
        }
      },
    },
    {
      label: "规格名称",
      prop: "model",
      label: "产品类型",
      prop: "dictLabel",
      dataType: "tag",
    },
    {
      label: "BOM编号",
@@ -145,6 +182,7 @@
  const isShowEditModal = ref(false);
  const isShowItemModal = ref(false);
  const record = ref({});
  const productTypeOptions = ref([]);
  const page = reactive({
    current: 1,
    size: 100,
@@ -204,6 +242,8 @@
        productName: row.productName || "",
        model: row.model || "",
        bomNo: row.bomNo || "",
        dictLabel: row.dictLabel || "",
        orderId: row.id || "",
        bomId: row.bomId || null,
        description: row.description || "",
        type: "route",
@@ -264,9 +304,31 @@
    });
  };
  // èŽ·å–äº§å“ç±»åž‹å­—å…¸
  const getProductTypeOptions = () => {
    getDicts("product_type")
      .then(res => {
        if (res.code === 200) {
          productTypeOptions.value = res.data;
        }
      })
      .catch(err => {
        console.error("获取产品类型字典失败:", err);
      });
  };
  onMounted(() => {
    getProductTypeOptions();
    getList();
  });
</script>
<style scoped></style>
<style lang="scss">
  .status-cell {
    font-weight: 600;
    color: #409eff;
    font-family: "Courier New", monospace;
    text-shadow: 0 1px 2px rgba(64, 158, 255, 0.2);
  }
</style>
src/views/productionManagement/processRoute/index2.vue
@@ -397,8 +397,8 @@
            </el-form-item>
            <el-form-item label="是否质检">
              <el-tag size="small"
                      :type="selectedProcessItem.isQuality ? 'success' : 'info'">
                {{ selectedProcessItem.isQuality ? '质检' : '非质检' }}
                      :type="selectedProcessItem.isQuality == 1 ? 'success' : 'info'">
                {{ selectedProcessItem.isQuality == 1 ? '质检' : '非质检' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="产品名称"
@@ -420,8 +420,8 @@
            <el-form-item label="是否质检"
                          prop="isQuality">
              <el-switch v-model="processForm.isQuality"
                         :active-value="true"
                         inactive-value="false" />
                         :active-value="1"
                         :inactive-value="0" />
            </el-form-item>
          </el-form>
          <el-empty v-else
@@ -1366,7 +1366,7 @@
    processForm.productName = "";
    processForm.productModelName = "";
    processForm.unit = "";
    processForm.isQuality = row.isQuality || false;
    processForm.isQuality = row.isQuality || 0;
  };
  // å¤„理工序选择时的产品选择
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -16,18 +16,10 @@
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">产品名称</span>
            <span class="info-label">产品类型</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.productName || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">规格名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.model || '-' }}</span>
            <span class="info-value">{{ routeInfo.dictLabel || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
@@ -55,6 +47,7 @@
         class="section-header">
      <div class="section-title">工艺路线项目列表</div>
      <div class="section-actions">
        <div class="sort-tip">拖拽表格排序</div>
        <el-button icon="Grid"
                   @click="toggleView"
                   style="margin-right: 10px;">
@@ -71,7 +64,9 @@
              :data="tableData"
              :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
              row-key="id"
              height="350"
              tooltip-effect="dark"
              style="margin-bottom: 20px;"
              class="lims-table">
      <el-table-column align="center"
                       label="序号"
@@ -84,12 +79,6 @@
          {{ getProcessName(scope.row.processId) || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="产品名称"
                       prop="productName"
                       min-width="160" />
      <el-table-column label="规格名称"
                       prop="model"
                       min-width="140" />
      <el-table-column label="参数列表"
                       min-width="160">
        <template #default="scope">
@@ -99,14 +88,12 @@
                     @click="handleViewParams(scope.row)">参数列表</el-button>
        </template>
      </el-table-column>
      <el-table-column label="单位"
                       prop="unit"
                       width="100" />
      <el-table-column label="是否质检"
                       prop="isQuality"
                       width="100">
                       prop="isQuality">
        <template #default="scope">
          {{scope.row.isQuality ? "是" : "否"}}
          <el-tag :type="scope.row.isQuality == 1 ? 'success' : 'danger'">
            {{scope.row.isQuality == 1 ? '是' : '否' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作"
@@ -119,10 +106,6 @@
                     size="small"
                     @click="handleEdit(scope.row)"
                     :disabled="scope.row.isComplete">编辑</el-button>
          <!-- <el-button type="info"
                     link
                     size="small"
                     @click="handleViewParams(scope.row)">参数列表</el-button> -->
          <el-button type="danger"
                     link
                     size="small"
@@ -136,6 +119,7 @@
      <div class="section-header">
        <div class="section-title">工艺路线项目列表</div>
        <div class="section-actions">
          <div class="sort-tip">长按拖拽卡片排序</div>
          <el-button icon="Menu"
                     @click="toggleView"
                     style="margin-right: 10px;">
@@ -159,22 +143,8 @@
              <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</div>
            </div>
            <!-- äº§å“ä¿¡æ¯ -->
            <div class="card-content">
              <div v-if="item.productName"
                   class="product-info">
                <div class="product-name">{{ item.productName }}</div>
                <div v-if="item.model"
                     class="product-model">
                  {{ item.model }}
                  <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> -->
                </div>
                <el-tag type="primary"
                        class="product-tag"
                        v-if="item.isQuality">质检</el-tag>
              </div>
              <div v-else
                   class="product-info empty">暂无产品信息</div>
            </div>
            <!-- <div class="card-content">
            </div> -->
            <!-- æ“ä½œæŒ‰é’® -->
            <div class="card-footer">
              <el-button type="primary"
@@ -196,6 +166,169 @@
        </div>
      </div>
    </template>
    <div class="section-BOM">
      <div class="section-header">
        <div class="section-title">BOM</div>
        <div class="section-actions"
             v-if="pageType === 'order'">
          <el-button type="primary"
                     @click="toggleBomEdit">
            {{ bomDataValue.isEdit ? '取消' : '编辑' }}
          </el-button>
          <el-button v-if=" bomDataValue.isEdit"
                     type="success"
                     @click="saveBomChanges">保存</el-button>
        </div>
      </div>
      <div>
        <!-- BOM表格 -->
        <el-table :data="bomTableData"
                  border
                  :preserve-expanded-content="false"
                  :default-expand-all="true"
                  style="width: 100%">
          <el-table-column type="expand">
            <template #default="props">
              <el-form ref="bomFormRef"
                       :model="bomDataValue">
                <el-table :data="props.row.bomList"
                          row-key="tempId"
                          default-expand-all
                          :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
                          style="width: 100%">
                  <el-table-column prop="productName"
                                   label="产品" />
                  <el-table-column prop="model"
                                   label="规格">
                    <template #default="{ row }">
                      <el-form-item v-if="bomDataValue.isEdit"
                                    style="margin: 0">
                        <el-select v-model="row.model"
                                   placeholder="请选择规格"
                                   :disabled="!bomDataValue.isEdit"
                                   style="width: 100%"
                                   @visible-change="(v) => { if (v) openBomProductDialog(row.tempId) }">
                          <el-option v-if="row.model"
                                     :label="row.model"
                                     :value="row.model" />
                        </el-select>
                      </el-form-item>
                      <span v-else>{{ row.model }}</span>
                    </template>
                  </el-table-column>
                  <el-table-column prop="processName"
                                   label="消耗工序">
                    <template #default="{ row }">
                      <el-form-item v-if="bomDataValue.isEdit"
                                    style="margin: 0">
                        <el-select v-model="row.processId"
                                   placeholder="请选择消耗工序"
                                   :disabled="!bomDataValue.isEdit"
                                   style="width: 100%">
                          <el-option v-for="process in processOptions"
                                     :key="process.id"
                                     :label="process.name"
                                     :value="process.id" />
                        </el-select>
                      </el-form-item>
                      <span v-else>{{ row.processName }}</span>
                    </template>
                  </el-table-column>
                  <el-table-column prop="unitQuantity"
                                   label="单位产出所需数量">
                    <template #default="{ row }">
                      <el-form-item v-if="bomDataValue.isEdit"
                                    style="margin: 0">
                        <el-input-number v-model="row.unitQuantity"
                                         :min="0"
                                         :step="1"
                                         controls-position="right"
                                         style="width: 100%"
                                         :disabled="!bomDataValue.isEdit" />
                      </el-form-item>
                      <span v-else>{{ row.unitQuantity }}</span>
                    </template>
                  </el-table-column>
                  <el-table-column prop="unit"
                                   label="单位">
                    <template #default="{ row }">
                      <el-form-item v-if="bomDataValue.isEdit"
                                    style="margin: 0">
                        <el-input v-model="row.unit"
                                  placeholder="请输入单位"
                                  clearable
                                  :disabled="!bomDataValue.isEdit" />
                      </el-form-item>
                      <span v-else>{{ row.unit }}</span>
                    </template>
                  </el-table-column>
                   <el-table-column prop="unitPrice"
                                   label="单价">
                    <template #default="{ row }">
                      <el-form-item v-if="bomDataValue.isEdit"
                                    style="margin: 0">
                        <el-input-number v-model="row.unitPrice"
                                         :min="0"
                                         :step="1"
                                         controls-position="right"
                                         style="width: 100%"
                                         :disabled="!bomDataValue.isEdit" />
                      </el-form-item>
                      <span v-else>{{ row.unitPrice }}</span>
                    </template>
                  </el-table-column>
                  <el-table-column label="操作"
                                   fixed="right"
                                   v-if="pageType === 'order'"
                                   width="180">
                    <template #default="{ row }">
                      <el-button v-if="bomDataValue.isEdit"
                                 type="danger"
                                 text
                                 size="small"
                                 @click="removeBomItem(row.tempId)">删除</el-button>
                      <el-button v-if="bomDataValue.isEdit"
                                 type="primary"
                                 text
                                 size="small"
                                 @click="addBomItem2(row.tempId)">添加子项</el-button>
                    </template>
                  </el-table-column>
                </el-table>
              </el-form>
            </template>
          </el-table-column>
          <el-table-column label="BOM编号"
                           prop="bomNo" />
          <el-table-column label="产品类型"
                           prop="dictLabel" />
          <!-- <el-table-column label="操作"
                           width="150">
            <template #default="{ row }">
            </template>
          </el-table-column> -->
          <!-- <el-table-column label="产品编码"
                           prop="productCode" />
          <el-table-column label="产品名称"
                           prop="productName" />
          <el-table-column label="规格型号"
                           prop="model" /> -->
        </el-table>
        <div v-if="bomDataValue.isEdit"
             style="text-align: center;border: 1px solid #e4e7ed;padding: 10px;transition: all 0.3s ease;cursor: pointer;"
             :class="{'hover-effect': bomDataValue.isEdit}">
          <el-button type="primary"
                     text
                     @click="addBomItem">
            <el-icon style="vertical-align: middle;margin-right: 5px;">
              <Plus />
            </el-icon>
            æ·»åŠ 
          </el-button>
        </div>
      </div>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog v-model="dialogVisible"
               :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'"
@@ -217,27 +350,11 @@
                       :value="process.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="产品名称"
                      prop="productModelId">
          <el-button type="primary"
                     @click="showProductSelectDialog = true">
            {{ form.productName && form.model
              ? `${form.productName} - ${form.model}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="单位"
                      prop="unit">
          <el-input v-model="form.unit"
                    :placeholder="form.productModelId ? '根据选择的产品自动带出' : '请先选择产品'"
                    clearable
                    :disabled="true" />
        </el-form-item>
        <el-form-item label="是否质检"
                      prop="isQuality">
          <el-switch v-model="form.isQuality"
                     :active-value="true"
                     inactive-value="false" />
                     :active-value="1"
                     :inactive-value="0" />
        </el-form-item>
      </el-form>
      <template #footer>
@@ -251,12 +368,18 @@
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- BOM产品选择对话框 -->
    <ProductSelectDialog v-model="bomDataValue.showProductDialog"
                         @confirm="handleBomProductSelect"
                         single />
    <!-- å‚数列表对话框 -->
    <ProcessParamListDialog v-model="showParamListDialog"
                            :title="`${currentProcess ? getProcessName(currentProcess.processId) : ''} - å‚数列表`"
                            :route-id="routeId"
                            :editable="false"
                            :editable="editable"
                            :order-id="orderId"
                            :process="currentProcess"
                            :page-type="pageType"
                            :param-list="paramList"
                            @refresh="refreshParamList" />
  </div>
@@ -284,10 +407,16 @@
    findProductProcessRouteItemList,
    deleteRouteItem,
    addRouteItem,
    findProcessParamListOrder,
    addOrUpdateProductProcessRouteItem,
    sortRouteItem,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { processList } from "@/api/productionManagement/productionProcess.js";
  import {
    queryList2,
    queryList,
    add2,
  } from "@/api/productionManagement/productStructure.js";
  import { useRoute } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
  import Sortable from "sortablejs";
@@ -298,6 +427,7 @@
  const routeId = computed(() => route.query.id);
  const orderId = computed(() => route.query.orderId);
  const pageType = computed(() => route.query.type);
  const editable = computed(() => route.query.editable === "true");
  const tableLoading = ref(false);
  const tableData = ref([]);
@@ -307,12 +437,13 @@
  const submitLoading = ref(false);
  const cardsContainer = ref(null);
  const tableRef = ref(null);
  const viewMode = ref("table"); // table | card
  const viewMode = ref("card"); // table | card
  const routeInfo = ref({
    processRouteCode: "",
    productName: "",
    model: "",
    bomNo: "",
    dictLabel: "",
    bomId: null,
    description: "",
  });
@@ -322,6 +453,15 @@
  const showParamListDialog = ref(false);
  const currentProcess = ref(null);
  const paramList = ref([]);
  const bomTableData = ref([]);
  const bomFormRef = ref(null);
  const bomDataValue = ref({
    dataList: [],
    showProductDialog: false,
    currentRowName: null,
    loading: false,
    isEdit: false,
  });
  let tableSortable = null;
  let cardSortable = null;
@@ -342,14 +482,11 @@
    productName: "",
    model: "",
    unit: "",
    isQuality: false,
    isQuality: 0,
  });
  const rules = {
    processId: [{ required: true, message: "请选择工序", trigger: "change" }],
    productModelId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
  };
  // æ ¹æ®å·¥åºID获取工序名称
@@ -401,9 +538,78 @@
      productName: route.query.productName || "",
      model: route.query.model || "",
      bomNo: route.query.bomNo || "",
      dictLabel: route.query.dictLabel || "",
      bomId: route.query.bomId || null,
      description: route.query.description || "",
    };
    if (pageType.value === "order") {
      queryList2(route.query.orderId)
        .then(res => {
          if (res.data) {
            // ä¸ºBOM数据设置tempId
            const setTempIdRecursively = items => {
              items.forEach(item => {
                item.tempId = item.id || new Date().getTime();
                if (item.children && item.children.length > 0) {
                  setTempIdRecursively(item.children);
                }
              });
            };
            setTempIdRecursively(res.data);
            bomTableData.value = [
              {
                bomNo: routeInfo.value.bomNo,
                dictLabel: routeInfo.value.dictLabel,
                productCode: "",
                productName: routeInfo.value.productName,
                model: routeInfo.value.model,
                bomList: res.data,
              },
            ];
            // ä¿å­˜åŽŸå§‹BOM数据
            bomDataValue.value.dataList = res.data;
          }
        })
        .catch(err => {
          console.error("获取BOM数据失败:", err);
        });
    } else {
      queryList(Number(route.query.bomId))
        .then(res => {
          if (res.data) {
            // ä¸ºBOM数据设置tempId
            const setTempIdRecursively = items => {
              items.forEach(item => {
                item.tempId = item.id || new Date().getTime();
                if (item.children && item.children.length > 0) {
                  setTempIdRecursively(item.children);
                }
              });
            };
            setTempIdRecursively(res.data);
            bomTableData.value = [
              {
                bomNo: routeInfo.value.bomNo,
                dictLabel: routeInfo.value.dictLabel,
                productCode: "",
                productName: routeInfo.value.productName,
                model: routeInfo.value.model,
                bomList: res.data,
              },
            ];
            // ä¿å­˜åŽŸå§‹BOM数据
            bomDataValue.value.dataList = res.data;
          }
        })
        .catch(err => {
          console.error("获取BOM数据失败:", err);
        });
    }
    // èŽ·å–BOM数据,使用新的接口
  };
  // æ–°å¢ž
@@ -483,12 +689,10 @@
          const addPromise = isOrderPage
            ? addRouteItem({
                productOrderId: orderId.value,
                productRouteId: routeId.value,
                orderId: orderId.value,
                routeId: routeId.value,
                processId: form.value.processId,
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
                dragSort,
              })
            : addOrUpdateProcessRouteItem({
                routeId: routeId.value,
@@ -518,7 +722,6 @@
            ? addOrUpdateProductProcessRouteItem({
                id: form.value.id,
                processId: form.value.processId,
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
              })
            : addOrUpdateProcessRouteItem({
@@ -564,6 +767,253 @@
  const closeDialog = () => {
    dialogVisible.value = false;
    resetForm();
  };
  // BOM相关方法
  // åˆ‡æ¢BOM编辑模式
  const toggleBomEdit = () => {
    bomDataValue.value.isEdit = !bomDataValue.value.isEdit;
    if (!bomDataValue.value.isEdit) {
      // å–消编辑时重新加载数据
      getRouteInfo();
    }
  };
  // æ·»åŠ BOM项
  const addBomItem = () => {
    if (bomTableData.value.length > 0) {
      const newItem = {
        parentId: "",
        parentTempId: "",
        productName: "",
        productId: "",
        model: undefined,
        productModelId: undefined,
        processId: "",
        processName: "",
        unitQuantity: 0,
        unitPrice: 0,
        unit: "",
        children: [],
        tempId: new Date().getTime(),
      };
      bomTableData.value[0].bomList.push(newItem);
      // ç”±äºŽbomDataValue.value.dataList和bomTableData.value[0].bomList指向同一个数组,不需要重复添加
    }
  };
  // æ·»åŠ BOM子项
  const addBomItem2 = tempId => {
    const addChildItem = (items, tempId) => {
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.tempId === tempId) {
          if (!item.children) {
            item.children = [];
          }
          item.children.push({
            parentId: item.id || "",
            parentTempId: item.tempId || "",
            productName: "",
            productId: "",
            model: undefined,
            productModelId: undefined,
            processId: "",
            processName: "",
            unitQuantity: 0,
            unitPrice: 0,
            unit: "",
            children: [],
            tempId: new Date().getTime(),
          });
          return true;
        }
        if (item.children && item.children.length > 0) {
          if (addChildItem(item.children, tempId)) {
            return true;
          }
        }
      }
      return false;
    };
    if (bomTableData.value.length > 0) {
      addChildItem(bomTableData.value[0].bomList, tempId);
      // ç”±äºŽbomDataValue.value.dataList和bomTableData.value[0].bomList指向同一个数组,不需要重复添加
    }
  };
  // åˆ é™¤BOM项
  const removeBomItem = tempId => {
    // ä»ŽBOM表格数据中删除
    if (bomTableData.value.length > 0) {
      const removeFromList = (items, tempId) => {
        for (let i = 0; i < items.length; i++) {
          const item = items[i];
          if (item.tempId === tempId) {
            items.splice(i, 1);
            return true;
          }
          if (item.children && item.children.length > 0) {
            if (removeFromList(item.children, tempId)) {
              return true;
            }
          }
        }
        return false;
      };
      removeFromList(bomTableData.value[0].bomList, tempId);
      // ç”±äºŽbomDataValue.value.dataList和bomTableData.value[0].bomList指向同一个数组,不需要重复删除
    }
  };
  // æ‰“å¼€BOM产品选择对话框
  const openBomProductDialog = tempId => {
    bomDataValue.value.currentRowName = tempId;
    bomDataValue.value.showProductDialog = true;
  };
  // å¤„理BOM产品选择
  const handleBomProductSelect = products => {
    if (products && products.length > 0) {
      const product = products[0];
      const updateProductInfo = (items, tempId, productData) => {
        for (let i = 0; i < items.length; i++) {
          const item = items[i];
          if (item.tempId === tempId) {
            item.productName = productData.productName;
            item.model = productData.model;
            item.productModelId = productData.id;
            item.unit = productData.unit || "";
            return true;
          }
          if (item.children && item.children.length > 0) {
            if (updateProductInfo(item.children, tempId, productData)) {
              return true;
            }
          }
        }
        return false;
      };
      if (bomTableData.value.length > 0) {
        updateProductInfo(
          bomTableData.value[0].bomList,
          bomDataValue.value.currentRowName,
          product
        );
        // ç”±äºŽbomDataValue.value.dataList和bomTableData.value[0].bomList指向同一个数组,不需要重复更新
      }
      bomDataValue.value.showProductDialog = false;
    }
  };
  // ä¿å­˜BOM更改
  const saveBomChanges = () => {
    // æ ¡éªŒBOM数据
    const validateBomData = items => {
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        // æ ¡éªŒäº§å“æ˜¯å¦å¿…å¡«
        if (!item.productModelId) {
          ElMessage.error("请选择产品");
          return false;
        }
        // æ ¡éªŒå•位产出所需数量是否必填
        if (
          item.unitQuantity === undefined ||
          item.unitQuantity === null ||
          item.unitQuantity === 0
        ) {
          ElMessage.error("请填写单位产出所需数量");
          return false;
        }
        // æ ¡éªŒå•价是否必填
        if (
          item.unitPrice === undefined ||
          item.unitPrice === null ||
          item.unitPrice === 0
        ) {
          ElMessage.error("请填写单价");
          return false;
        }
        // é€’归校验子项
        if (item.children && item.children.length > 0) {
          if (!validateBomData(item.children)) {
            return false;
          }
        }
      }
      return true;
    };
    // æ‰§è¡Œæ ¡éªŒ
    if (bomTableData.value.length > 0) {
      if (!validateBomData(bomTableData.value[0].bomList)) {
        return;
      }
    }
    // è°ƒç”¨æ–°çš„保存接口
    // å‡†å¤‡ä¿å­˜æ•°æ®ï¼Œç¡®ä¿æ ¼å¼æ­£ç¡®
    // é€’归处理BOM项及其子项
    const processBomItem = (item, parentId = null, parentTempId = null) => {
      const cleanItem = {
        id: item.id || null,
        orderId: Number(orderId.value) || null,
        parentId: parentId,
        parentTempId: parentTempId || null,
        productModelId: item.productModelId || null,
        processId: item.processId || null,
        unitQuantity: item.unitQuantity || 0,
        unitPrice: item.unitPrice || 0,
        demandedQuantity: item.demandedQuantity || null,
        unit: item.unit || "",
        tempId: item.tempId || new Date().getTime(),
        tenantId: item.tenantId || null,
        bomId: Number(route.query.bomId) || null,
        children: [],
      };
      // é€’归处理子项
      if (item.children && item.children.length > 0) {
        cleanItem.children = item.children.map(child =>
          processBomItem(child, item.id, item.tempId || null)
        );
      }
      return cleanItem;
    };
    const saveData = bomTableData.value[0].bomList.map(item =>
      processBomItem(item, item.parentId, item.parentTempId || null)
    );
    const formData = {
      orderId: Number(orderId.value) || null,
      children: saveData,
    };
    add2(formData)
      .then(res => {
        if (res.code === 200) {
          ElMessage.success("BOM保存成功");
          bomDataValue.value.isEdit = false;
          // é‡æ–°åŠ è½½æ•°æ®ä»¥èŽ·å–æœ€æ–°çŠ¶æ€
          getRouteInfo();
        } else {
          ElMessage.error("BOM保存失败:" + (res.msg || "未知错误"));
        }
      })
      .catch(err => {
        console.error("保存BOM数据失败:", err);
        ElMessage.error("BOM保存失败:网络错误");
      });
  };
  // å–消BOM编辑
  const cancelBomEdit = () => {
    bomDataValue.value.isEdit = false;
    getRouteInfo();
  };
  // åˆå§‹åŒ–拖拽排序
@@ -641,6 +1091,7 @@
        ghostClass: "sortable-ghost",
        handle: ".process-card",
        filter: ".el-button",
        delay: 500, // é•¿æŒ‰500毫秒后开始拖拽
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
            return;
@@ -711,50 +1162,96 @@
  const handleViewParams = process => {
    currentProcess.value = process;
    // è°ƒç”¨API获取参数列表
    getProcessParamList({
      routeItemId: process.id,
      pageNum: 1,
      pageSize: 1000,
    })
      .then(res => {
        if (res.code === 200) {
          paramList.value = res.data?.records || [];
        } else {
          ElMessage.error(res.msg || "获取参数列表失败");
          paramList.value = [];
        }
        showParamListDialog.value = true;
    if (pageType.value === "order") {
      findProcessParamListOrder({
        orderId: orderId.value,
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
      })
      .catch(err => {
        console.error("获取参数列表失败:", err);
        ElMessage.error("获取参数列表失败");
        paramList.value = [];
        showParamListDialog.value = true;
      });
        .then(res => {
          if (res.code === 200) {
            paramList.value = res.data || [];
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            paramList.value = [];
          }
          showParamListDialog.value = true;
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          paramList.value = [];
          showParamListDialog.value = true;
        });
    } else {
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
      })
        .then(res => {
          if (res.code === 200) {
            paramList.value = res.data?.records || [];
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            paramList.value = [];
          }
          showParamListDialog.value = true;
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          paramList.value = [];
          showParamListDialog.value = true;
        });
    }
  };
  // åˆ·æ–°å‚数列表
  const refreshParamList = () => {
    if (!currentProcess.value) return;
    // é‡æ–°è°ƒç”¨API获取参数列表
    getProcessParamList({
      routeItemId: currentProcess.value.id,
      pageNum: 1,
      pageSize: 1000,
    })
      .then(res => {
        if (res.code === 200) {
          paramList.value = res.data?.records || [];
        } else {
          ElMessage.error(res.msg || "获取参数列表失败");
          paramList.value = [];
        }
    if (pageType.value === "order") {
      findProcessParamListOrder({
        orderId: orderId.value,
        routeItemId: currentProcess.value.id,
        pageNum: 1,
        pageSize: 1000,
      })
      .catch(err => {
        console.error("获取参数列表失败:", err);
        ElMessage.error("获取参数列表失败");
        paramList.value = [];
      });
        .then(res => {
          if (res.code === 200) {
            paramList.value = res.data || [];
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            paramList.value = [];
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          paramList.value = [];
        });
    } else {
      getProcessParamList({
        routeItemId: currentProcess.value.id,
        pageNum: 1,
        pageSize: 1000,
      })
        .then(res => {
          if (res.code === 200) {
            paramList.value = res.data?.records || [];
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            paramList.value = [];
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          paramList.value = [];
        });
    }
  };
  onUnmounted(() => {
@@ -765,14 +1262,16 @@
<style scoped>
  .card-container {
    padding: 20px 0;
    /* height: 350px; */
    margin-bottom: 20px;
  }
  .cards-wrapper {
    display: flex;
    gap: 16px;
    gap: 24px;
    overflow-x: auto;
    padding: 10px 0;
    min-height: 200px;
    /* min-height: 250px; */
  }
  .cards-wrapper::-webkit-scrollbar {
@@ -795,11 +1294,12 @@
  .process-card {
    flex-shrink: 0;
    width: 220px;
    /* width: 300px; */
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 16px;
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    /* padding: 30px 24px; */
    padding: 25px 50px;
    display: flex;
    flex-direction: column;
    cursor: move;
@@ -807,45 +1307,45 @@
  }
  .process-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
    transform: translateY(-4px);
  }
  .card-header {
    text-align: center;
    margin-bottom: 12px;
    margin-bottom: 20px;
  }
  .card-number {
    width: 36px;
    height: 36px;
    line-height: 36px;
    width: 60px;
    height: 60px;
    line-height: 60px;
    border-radius: 50%;
    background: #409eff;
    color: #fff;
    font-weight: bold;
    font-size: 16px;
    margin: 0 auto 8px;
    font-size: 20px;
    margin: 0 auto 16px;
  }
  .card-process-name {
    font-size: 14px;
    font-size: 18px;
    color: #333;
    font-weight: 500;
    font-weight: 600;
    word-break: break-all;
  }
  .card-content {
    flex: 1;
    margin-bottom: 12px;
    min-height: 60px;
    margin-bottom: 20px;
    min-height: 80px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .product-info {
    font-size: 13px;
    font-size: 14px;
    color: #666;
    text-align: center;
    width: 100%;
@@ -858,7 +1358,7 @@
  }
  .product-name {
    margin-bottom: 6px;
    margin-bottom: 8px;
    word-break: break-all;
    line-height: 1.5;
    text-align: center;
@@ -866,7 +1366,7 @@
  .product-model {
    color: #909399;
    font-size: 12px;
    font-size: 13px;
    word-break: break-all;
    line-height: 1.5;
    text-align: center;
@@ -878,19 +1378,32 @@
  }
  .product-tag {
    margin: 10px 0;
    margin: 12px 0;
  }
  .card-footer {
    display: flex;
    justify-content: space-around;
    padding-top: 12px;
    justify-content: center;
    gap: 20px;
    padding-top: 16px;
    border-top: 1px solid #f0f0f0;
  }
  .card-footer .el-button {
    padding: 0;
    font-size: 12px;
    font-size: 14px;
  }
  .card-footer .el-button:nth-child(1) {
    color: #409eff;
  }
  .card-footer .el-button:nth-child(2) {
    color: #67c23a;
  }
  .card-footer .el-button:nth-child(3) {
    color: #f56c6c;
  }
  :deep(.sortable-ghost) {
@@ -902,13 +1415,13 @@
    opacity: 0.8;
  }
  /* è¡¨æ ¼è§†å›¾æ ·å¼ */
  :deep(.el-table__row) {
  /* è¡¨æ ¼è§†å›¾æ ·å¼ - ä»…应用于项目列表 */
  :deep(.lims-table .el-table__row) {
    transition: background-color 0.2s;
    cursor: move;
  }
  :deep(.el-table__row:hover) {
  :deep(.lims-table .el-table__row:hover) {
    background-color: #f9fafc !important;
  }
@@ -945,6 +1458,12 @@
    display: flex;
    align-items: center;
  }
  .sort-tip {
    font-size: 12px;
    color: #909399;
    margin-left: 8px;
    margin-right: 20px;
  }
  /* å·¥è‰ºè·¯çº¿ä¿¡æ¯å¡ç‰‡æ ·å¼ */
  .route-info-card {
@@ -957,7 +1476,7 @@
  .route-info {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
    gap: 16px;
    padding: 4px;
  }
@@ -1022,3 +1541,11 @@
    word-break: break-all;
  }
</style>
<style scoped>
  .hover-effect:hover {
    border-color: #409eff;
    background-color: #ecf5ff;
    transform: translateY(-2px);
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  }
</style>
src/views/productionManagement/productStructure/Detail/index.vue
@@ -38,12 +38,11 @@
                               label="规格">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                :rules="[{ required: false, message: '请选择规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择规格"
                               clearable
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                               :disabled="!dataValue.isEdit"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openDialog(row.tempId) }">
                      <el-option v-if="row.model"
@@ -57,14 +56,14 @@
                               label="消耗工序">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                :rules="[{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                               clearable
                               style="width: 100%"
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
                               :disabled="!dataValue.isEdit">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
@@ -81,11 +80,10 @@
                                style="margin: 0">
                    <el-input-number v-model="row.unitQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -102,7 +100,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -115,7 +113,22 @@
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                              :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                              :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
               <el-table-column prop="unitPrice"
                               label="单价">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="[{ required: true, message: '请输入单价', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitPrice"
                                     :min="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -123,7 +136,7 @@
                               fixed="right"
                               width="200">
                <template #default="{ row, $index }">
                  <el-button v-if="dataValue.isEdit && !dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                  <el-button v-if="dataValue.isEdit"
                             type="danger"
                             text
                             @click="removeItem(row.tempId)">删除
@@ -141,13 +154,21 @@
      </el-table-column>
      <el-table-column label="BOM编号"
                       prop="bomNo" />
      <el-table-column label="产品编码"
                       prop="productCode" />
      <el-table-column label="产品名称"
                       prop="productName" />
      <el-table-column label="规格型号"
                       prop="model" />
      <el-table-column label="产品类型"
                       prop="dictLabel" />
    </el-table>
    <div v-if="dataValue.isEdit"
         style="text-align: center;border: 1px solid #e4e7ed;padding: 10px;transition: all 0.3s ease;cursor: pointer;"
         :class="{'hover-effect': dataValue.isEdit}">
      <el-button type="primary"
                 text
                 @click="addItem">
        <el-icon style="vertical-align: middle;margin-right: 5px;">
          <Plus />
        </el-icon>
        æ·»åŠ 
      </el-button>
    </div>
    <product-select-dialog v-if="dataValue.showProductDialog"
                           v-model:model-value="dataValue.showProductDialog"
                           single
@@ -194,11 +215,8 @@
  // ä»Žè·¯ç”±å‚数获取产品信息
  const routeBomNo = computed(() => route.query.bomNo || "");
  const routeProductCode = computed(() => route.query.productCode || "");
  const routeProductName = computed(() => route.query.productName || "");
  const routeProductModelName = computed(
    () => route.query.productModelName || ""
  );
  const routeDictLabel = computed(() => route.query.dictLabel || "");
  const routeOrderId = computed(() => route.query.orderId);
  const pageType = computed(() => route.query.type);
  const isOrderPage = computed(
@@ -218,9 +236,8 @@
  const tableData = reactive([
    {
      productName: "",
      model: "",
      bomNo: "",
      dictLabel: "",
    },
  ]);
@@ -332,18 +349,18 @@
    const validateItem = (item: any, isTopLevel = false) => {
      console.log(item, "item");
      // æ ¡éªŒå½“前项的必填字段
      if (!item.model) {
        ElMessage.error("请选择规格");
        isValid = false;
        return;
      }
      if (!isTopLevel && !item.processId) {
      if (!item.processId) {
        ElMessage.error("请选择消耗工序");
        isValid = false;
        return;
      }
      if (!item.unitQuantity) {
        ElMessage.error("请输入单位产出所需数量");
        isValid = false;
        return;
      }
      if (!item.unitPrice) {
        ElMessage.error("请输入单价");
        isValid = false;
        return;
      }
@@ -431,6 +448,24 @@
      }
    });
  };
  const addItem = () => {
    dataValue.dataList.push({
      parentId: "",
      parentTempId: "",
      productName: "",
      productId: "",
      model: undefined,
      productModelId: undefined,
      processId: "",
      processName: "",
      unitQuantity: 0,
      unitPrice: 0,
      demandedQuantity: 0,
      unit: "",
      children: [],
      tempId: new Date().getTime(),
    });
  };
  const addItem2 = tempId => {
    dataValue.dataList.map(item => {
      if (item.tempId === tempId) {
@@ -447,6 +482,7 @@
          processId: "",
          processName: "",
          unitQuantity: 0,
          unitPrice: 0,
          demandedQuantity: 0,
          unit: "",
          children: [],
@@ -473,6 +509,7 @@
        productModelId: undefined,
        processId: "",
        unitQuantity: 0,
        unitPrice: 0,
        demandedQuantity: 0,
        children: [],
        unit: "",
@@ -510,9 +547,8 @@
  onMounted(async () => {
    // ä»Žè·¯ç”±å‚数回显数据
    tableData[0].productName = routeProductName.value as string;
    tableData[0].model = routeProductModelName.value as string;
    tableData[0].bomNo = routeBomNo.value as string;
    tableData[0].dictLabel = routeDictLabel.value as string;
    // è®¢å•情况下禁用编辑
    if (isOrderPage.value) {
@@ -523,4 +559,13 @@
    await fetchProcessOptions();
    await fetchData();
  });
</script>
</script>
<style scoped>
  .hover-effect:hover {
    border-color: #409eff;
    background-color: #ecf5ff;
    transform: translateY(-2px);
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  }
</style>
src/views/productionManagement/productStructure/StructureEdit.vue
@@ -38,7 +38,7 @@
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :prop="`dataList.${$index}.model`"
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                :rules="[{ required: false, message: '请选择规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择产品"
@@ -119,6 +119,23 @@
                              placeholder="请输入单位"
                              clearable
                              :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unitPrice"
                               label="单价"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.unitPrice`"
                                :rules="[{ required: true, message: '请输入单价', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitPrice"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -294,6 +311,7 @@
      productModelId: undefined,
      processId: "",
      unitQuantity: 0,
      unitPrice: 0,
      demandedQuantity: 0,
      unit: "",
      diskQuantity: 0,
src/views/productionManagement/productStructure/index.vue
@@ -567,8 +567,7 @@
      query: {
        id: row.id,
        bomNo: row.bomNo || "",
        productName: row.productName || "",
        productModelName: row.productModelName || "",
        dictLabel: row.dictLabel || "",
      },
    });
  };
src/views/productionManagement/productionOrder/index.vue
@@ -3,16 +3,8 @@
    <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: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="合同号:">
          <el-input v-model="searchForm.salesContractNo"
        <el-form-item label="订单号:">
          <el-input v-model="searchForm.npsNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
@@ -20,31 +12,67 @@
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品名称:">
          <el-input v-model="searchForm.productCategory"
          <el-input v-model="searchForm.productName"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品类型:">
          <el-select v-model="searchForm.strength"
                     style="width: 200px;"
                     placeholder="请选择产品类型"
                     clearable
                     @change="handleQuery">
            <el-option v-for="option in productTypeOptions2"
                       :key="option.dictLabel"
                       :label="option.dictLabel"
                       :value="option.dictLabel" />
          </el-select>
        </el-form-item>
        <el-form-item label="创建时间:">
          <el-date-picker v-model="createTime"
                          type="daterange"
                          range-separator="至"
                          start-placeholder="开始日期"
                          value-format="YYYY-MM-DD"
                          format="YYYY-MM-DD"
                          end-placeholder="结束日期"
                          style="width: 300px;"
                          @change="handleQuery" />
        </el-form-item>
        <el-form-item label="规格:">
          <el-input v-model="searchForm.specificationModel"
          <el-input v-model="searchForm.model"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="状态:">
          <el-select v-model="searchForm.status"
                     placeholder="请选择"
                     style="width: 160px;"
                     @change="handleQuery">
            <el-option v-for="item in statusOptions"
                       :key="item.value"
                       :label="item.label"
                       :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
          <el-button type="primary"
                     @click="handleReset">重置</el-button>
          <el-button type="danger"
                     @click="handleDelete">退回</el-button>
          <el-button @click="handleOut">导出</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 style="width:350px;text-align:right;">
      </div> -->
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
@@ -54,14 +82,19 @@
                :tableLoading="tableLoading"
                :row-class-name="tableRowClassName"
                :isSelection="true"
                :selectable="row => row.status != 4"
                @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' : ''"
          />
          <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
                       :color="progressColor(toProgressPercentage(row?.completionStatus))"
                       :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
        </template>
        <template #quantity="{ row }">
          {{ row.quantity || '-' }}<span style="color:rgb(63, 95, 211)"> æ–¹</span>
        </template>
        <template #completeQuantity="{ row }">
          {{ row.completeQuantity || '-' }}<span style="color:rgb(42, 169, 146)"> æ–¹</span>
        </template>
      </PIMTable>
    </div>
@@ -90,10 +123,130 @@
        </span>
      </template>
    </el-dialog>
    <new-product-order v-if="isShowNewModal"
                         v-model:visible="isShowNewModal"
                         @completed="handleQuery" />
                       v-model:visible="isShowNewModal"
                       @completed="handleQuery" />
    <!-- æ¥æºæ•°æ®å¼¹çª— -->
    <el-dialog v-model="sourceDataDialogVisible"
               title="来源数据"
               width="1000px">
      <div class="applyno-summary1">
        <div class="summary-item">
          <span class="summary-label">产品名称:</span>
          <span class="summary-value">
            <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>
        </div>
        <div class="summary-item">
          <span class="summary-label">物料编码:</span>
          <span class="summary-value">{{ sourceRowData.materialCode || '-' }}</span>
        </div>
        <div class="summary-item">
          <span class="summary-label">强度:</span>
          <span class="summary-value">{{ sourceRowData.strength || '-' }}</span>
        </div>
      </div>
      <div class="source-data-container">
        <!-- å·¦ä¾§applyNo列表 -->
        <div class="applyno-list">
          <div class="list-header">申请单列表</div>
          <div class="list-body">
            <div v-for="(item, index) in sourceTableData"
                 :key="item.applyNo || index"
                 class="applyno-item"
                 :class="{ active: selectedApplyNo === item.applyNo }"
                 @click="selectApplyNo(item)">
              <div class="applyno-text">{{ item.applyNo }}</div>
              <div class="applyno-info">{{ item.customerName }}</div>
            </div>
          </div>
        </div>
        <!-- å³ä¾§è¯¦ç»†ä¿¡æ¯ -->
        <div class="detail-info">
          <div v-if="selectedSourceData && selectedSourceData.items && selectedSourceData.items.length > 0">
            <div v-for="item in selectedSourceData.items"
                 :key="item.id"
                 class="source-data-card">
              <!-- <div class="card-header">
                <div class="data-source-tag">
                </div>
                <div class="card-title">产品明细</div>
              </div> -->
              <div class="card-body">
                <div class="info-grid">
                  <div class="info-item">
                    <div class="info-label">数据来源</div>
                    <div class="info-value">
                      <el-tag :type="item.dataSourceType == 1 ? 'primary' : 'warning'">
                        {{ item.dataSourceType == 1 ? '钉钉同步' : '手动新增' }}
                      </el-tag>
                    </div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">块数</div>
                    <div class="info-value">{{ item.quantity || '-' }}<span style="color:rgb(63, 95, 211)"> å—</span></div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">方数</div>
                    <div class="info-value">{{ item.volume || '-' }}<span style="color:rgba(27, 104, 90, 0.76)"> æ–¹</span></div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">下发状态</div>
                    <div class="info-value">
                      <el-tag :type="{
                        0: 'warning',
                        1: 'primary',
                        2: 'info'
                      }[item.status] || 'info'">
                        {{ item.status == 0 ? '待下发' : item.status == 1 ? '部分下发' : '已下发' }}
                      </el-tag>
                    </div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">已下发方数</div>
                    <div class="info-value">{{ item.assignedQuantity ? `${item.assignedQuantity}` : 0 }}<span style="color:rgba(214, 134, 22, 0.76)"> æ–¹</span></div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">尺寸</div>
                    <div class="info-value">{{ item.length || '-' }}mm Ã— {{ item.width || '-' }}mm Ã— {{ item.height || '-' }}mm</div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">计划开始日期</div>
                    <div class="info-value">{{ item.startDate ? dayjs(item.startDate).format('YYYY-MM-DD') : '' }}</div>
                  </div>
                  <div class="info-item">
                    <div class="info-label">计划结束日期</div>
                    <div class="info-value">{{ item.endDate ? dayjs(item.endDate).format('YYYY-MM-DD') : '' }}</div>
                  </div>
                  <!-- <div class="info-item">
                    <div class="info-label">强度</div>
                    <div class="info-value">{{ item.strength || '' }}</div>
                  </div> -->
                </div>
                <div class="remarks-section">
                  <div class="info-item full-width">
                    <div class="info-label">备注 1</div>
                    <div class="info-value">{{ item.remarkOne || '-' }}</div>
                  </div>
                  <div class="info-item full-width">
                    <div class="info-label">备注 2</div>
                    <div class="info-value">{{ item.remarkTwo || '-' }}</div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div v-else
               class="empty-state">
            <el-empty :description="selectedSourceData ? '该申请单暂无数据' : '请选择一个申请单'" />
          </div>
        </div>
      </div>
    </el-dialog>
  </div>
</template>
@@ -102,60 +255,108 @@
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import { useRouter } from "vue-router";
  import { getDicts } from "@/api/system/dict/data";
  import {
    productOrderListPage,
    listProcessRoute,
    bindingRoute,
    listProcessBom, delProductOrder,
    listProcessBom,
    delProductOrder,
    revokeProductOrder,
    getProductOrderSource,
  } from "@/api/productionManagement/productionOrder.js";
  import { listPage } from "@/api/productionManagement/processRoute.js";
  import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
  import {fileDel} from "@/api/financialManagement/revenueManagement.js";
  import { fileDel } from "@/api/financialManagement/revenueManagement.js";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  const NewProductOrder = defineAsyncComponent(() => import("@/views/productionManagement/productionOrder/New.vue"));
  const NewProductOrder = defineAsyncComponent(() =>
    import("@/views/productionManagement/productionOrder/New.vue")
  );
  const { proxy } = getCurrentInstance();
  const router = useRouter();
  const isShowNewModal = ref(false);
  const sourceDataDialogVisible = ref(false);
  const sourceTableData = ref([]);
  const selectedApplyNo = ref("");
  const selectedSourceData = ref(null);
  const sourceRowData = ref(null);
  const tableColumn = ref([
    {
      label: "状态",
      prop: "status",
      dataType: "tag",
      formatData: val => {
        const statusMap = {
          1: "待开始",
          2: "进行中",
          3: "已完成",
          4: "已取消",
        };
        return statusMap[val] || "";
      },
      formatType: val => {
        const statusMap = {
          1: "error",
          2: "warning",
          3: "success",
          4: "info",
        };
        return statusMap[val] || "info";
      },
      width: 100,
    },
    {
      label: "生产订单号",
      prop: "npsNo",
      width: '120px',
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
      width: '150px',
    },
    {
      label: "客户名称",
      prop: "customerName",
      width: '200px',
      width: "150px",
    },
    {
      label: "产品名称",
      prop: "productCategory",
      width: '120px',
      prop: "productName",
      width: "120px",
      dataType: "tag",
    },
    {
      label: "规格",
      prop: "specificationModel",
      width: '120px',
      prop: "model",
      width: "120px",
    },
    {
      label: "强度",
      prop: "strength",
      width: "120px",
      dataType: "tag",
    },
    {
      label: "物料编码",
      prop: "materialCode",
      width: "120px",
    },
    {
      label: "工艺路线编号",
      prop: "processRouteCode",
      width: '200px',
      width: "200px",
      className: "status-cell",
    },
    {
      label: "需求数量",
      prop: "quantity",
      dataType: "slot",
      align: "right",
      slot: "quantity",
      width: 120,
    },
    {
      label: "完成数量",
      prop: "completeQuantity",
      dataType: "slot",
      align: "right",
      slot: "completeQuantity",
      width: 120,
    },
    {
      dataType: "slot",
@@ -178,17 +379,31 @@
    },
    {
      label: "交付日期",
      prop: "deliveryDate",
      prop: "planCompleteTime",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
      width: 120,
    },
    {
      label: "创建时间",
      prop: "createTime",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD HH:mm:ss") : ""),
      width: 120,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      width: 300,
      operation: [
        {
          name: "来源",
          type: "text",
          clickFun: row => {
            showSourceData(row);
          },
        },
        {
          name: "工艺路线",
          type: "text",
@@ -199,18 +414,26 @@
        {
          name: "绑定工艺路线",
          type: "text",
          showHide: row => !row.processRouteCode,
          showHide: row => !row.routeId,
          clickFun: row => {
            openBindRouteDialog(row);
          },
        },
        {
          name: "产品结构",
          name: "删除",
          type: "text",
          showHide: row => row.status == 4,
          clickFun: row => {
            showProductStructure(row);
            handleDeleteSolo(row);
          },
        },
        // {
        //   name: "产品结构",
        //   type: "text",
        //   clickFun: row => {
        //     showProductStructure(row);
        //   },
        // },
      ],
    },
  ]);
@@ -223,13 +446,168 @@
  });
  const selectedRows = ref([]);
  // æ¥æºæ•°æ®å¼¹çª—相关
  const sourceTableColumn = ref([
    {
      label: "数据来源",
      width: "100px",
      prop: "dataSourceType",
      dataType: "tag",
      formatType: params => {
        const typeMap = {
          2: "warning",
          1: "primary",
        };
        return typeMap[params] || "info";
      },
      formatData: cell => (cell == 1 ? "钉钉同步" : "手动新增"),
    },
    {
      label: "申请单编号",
      prop: "applyNo",
      width: "150px",
    },
    {
      label: "客户名称",
      prop: "customerName",
      width: "150px",
    },
    {
      label: "产品名称",
      prop: "productName",
      width: "200px",
      dataType: "tag",
      formatType: params => {
        return "primary";
      },
    },
    {
      label: "产品规格",
      prop: "model",
      width: "150px",
      className: "spec-cell",
    },
    {
      label: "物料编码",
      prop: "materialCode",
      width: "150px",
    },
    {
      label: "块数",
      prop: "quantity",
      align: "right",
      dataType: "slot",
      slot: "quantity",
    },
    {
      label: "方数",
      prop: "volume",
      width: "150px",
      align: "right",
      dataType: "slot",
      slot: "volume",
      className: "volume-cell",
    },
    {
      label: "下发状态",
      prop: "status",
      width: "150px",
      className: "status-cell",
      dataType: "tag",
      formatType: params => {
        const typeMap = {
          0: "warning",
          1: "primary",
          2: "info",
        };
        return typeMap[params] || "info";
      },
      formatData: cell => {
        const statusMap = {
          0: "待下发",
          1: "部分下发",
          2: "已下发",
        };
        return statusMap[cell] || "";
      },
    },
    {
      label: "已下发方数",
      prop: "assignedQuantity",
      width: "150px",
      className: "spec-cell",
      formatData: cell => (cell ? `${cell}方` : 0),
    },
    {
      label: "长",
      prop: "length",
      className: "dimension-cell",
      formatData: cell => (cell ? `${cell}mm` : ""),
    },
    {
      label: "宽",
      prop: "width",
      className: "dimension-cell",
      formatData: cell => (cell ? `${cell}mm` : ""),
    },
    {
      label: "高",
      prop: "height",
      className: "dimension-cell",
      formatData: cell => (cell ? `${cell}mm` : ""),
    },
    {
      label: "计划开始日期",
      prop: "startDate",
      width: "150px",
      className: "date-cell",
      formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
    },
    {
      label: "计划结束日期",
      prop: "endDate",
      width: "150px",
      className: "date-cell",
      formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
    },
    {
      label: "强度",
      prop: "strength",
      formatData: cell => {
        if (!cell) return "";
        return cell;
      },
    },
    {
      label: "备注 1",
      width: "150px",
      prop: "remarkOne",
    },
    {
      label: "备注 2",
      width: "150px",
      prop: "remarkTwo",
    },
  ]);
  const sourceTableLoading = ref(false);
  const sourcePage = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const data = reactive({
    searchForm: {
      customerName: "",
      salesContractNo: "",
      projectName: "",
      productCategory: "",
      specificationModel: "",
      productName: "",
      model: "",
      dictCode: null,
      startTime: null,
      endTime: null,
      strength: null,
      status: "",
    },
  });
  const { searchForm } = toRefs(data);
@@ -253,19 +631,18 @@
  // æ·»åŠ è¡¨è¡Œç±»åæ–¹æ³•
  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';
    }
    // if (!row.planCompleteTime) 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";
    // }
  };
  // ç»‘定工艺路线弹框
@@ -273,6 +650,7 @@
  const bindRouteLoading = ref(false);
  const bindRouteSaving = ref(false);
  const routeOptions = ref([]);
  const productTypeOptions = ref([]);
  const bindForm = reactive({
    orderId: null,
    routeId: null,
@@ -283,15 +661,32 @@
    bindForm.routeId = null;
    bindRouteDialogVisible.value = true;
    routeOptions.value = [];
    if (!row.productModelId) {
    if (!row.model) {
      proxy.$modal.msgWarning("当前订单缺少产品型号,无法查询工艺路线");
      bindRouteDialogVisible.value = false;
      return;
    }
    bindRouteLoading.value = true;
    const distName =
      row.productName == "板材"
        ? row.productName
        : row.productName + "-" + row.strength;
    try {
      const res = await listProcessRoute({ productModelId: row.productModelId });
      routeOptions.value = res.data || [];
      // èŽ·å–äº§å“ç±»åž‹å­—å…¸
      const dictRes = await getDicts("product_type");
      if (dictRes.code === 200) {
        productTypeOptions.value = dictRes.data;
        // ç”¨distName匹配dictLabel,获取dictCode
        const matchedType = productTypeOptions.value.find(
          item => item.dictLabel === distName
        );
        const dictCode = matchedType ? matchedType.dictCode : row.productType;
        // ä½¿ç”¨dictCode查询工艺路线列表
        const res = await listPage({ dictCode, status: true });
        routeOptions.value = res.data.records || [];
      }
    } catch (e) {
      console.error("获取工艺路线列表失败:", e);
      proxy.$modal.msgError("获取工艺路线列表失败");
@@ -321,7 +716,27 @@
      bindRouteSaving.value = false;
    }
  };
  const statusOptions = ref([
    { value: 1, label: "待开始" },
    { value: 2, label: "进行中" },
    { value: 3, label: "已完成" },
    { value: 4, label: "已取消" },
  ]);
  const handleReset = () => {
    searchForm.value = {
      customerName: "",
      salesContractNo: "",
      projectName: "",
      productName: "",
      model: "",
      status: "",
      strength: null,
      startTime: null,
      endTime: null,
    };
    createTime.value = [];
    handleQuery();
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
@@ -343,11 +758,25 @@
    }
    handleQuery();
  };
  const createTime = ref([]);
  const getList = () => {
    tableLoading.value = true;
    // æž„造一个新的对象,不包含entryDate字段
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    params.startTime =
      createTime.value.length > 0
        ? dayjs(createTime.value[0]).format("YYYY-MM-DD HH:mm:ss")
        : undefined;
    params.endTime =
      createTime.value.length > 0
        ? dayjs(createTime.value[1])
            .hour(23)
            .minute(59)
            .second(59)
            .format("YYYY-MM-DD HH:mm:ss")
        : undefined;
    productOrderListPage(params)
      .then(res => {
        tableLoading.value = false;
@@ -362,23 +791,23 @@
  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,
          id: row.routeId,
          processRouteCode: row.processRouteCode || "",
          productName: row.productName || "",
          model: row.model || "",
          bomNo: row.bomNo || "",
          bomId: row.bomId || "",
          description: row.description || "",
          dictLabel:
            row.productName == "板材"
              ? row.productName
              : row.productName + "-" + row.strength,
          orderId: row.id,
          type: "order",
          editable: true,
        },
      });
    } catch (e) {
@@ -388,44 +817,128 @@
  };
  const showProductStructure = row => {
    if (!row.processRouteCode) {
      proxy.$modal.msgWarning("请先绑定工艺路线");
      return;
    }
    router.push({
      path: "/productionManagement/productStructureDetail",
      query: {
        id: row.id,
        bomNo: row.bomNo || "",
        productName: row.productCategory || "",
        productModelName: row.specificationModel || "",
        productName: row.productName || "",
        productModelName: row.model || "",
        orderId: row.id,
        type: "order",
      },
    });
  };
  // é€‰æ‹©ç”³è¯·å•
  const selectApplyNo = item => {
    selectedApplyNo.value = item.applyNo;
    selectedSourceData.value = item;
  };
  // æŸ¥çœ‹æ¥æºç”Ÿäº§è®¡åˆ’数据
  const showSourceData = row => {
    // å­˜å‚¨ç‚¹å‡»æ¥æºæŒ‰é’®æ—¶ä¼ é€’çš„row参数
    sourceRowData.value = row;
    // è°ƒç”¨API获取来源数据
    getProductOrderSource(row.id)
      .then(res => {
        if (res.code === 200) {
          // å¤„理接口返回的数据,调整为我们需要的格式
          sourceTableData.value = res.data.map(item => {
            return {
              applyNo: item.applyNo,
              customerName: item.productPlans[0]?.customerName || "",
              items: item.productPlans.map(plan => {
                return {
                  id: plan.id,
                  dataSourceType: plan.dataSourceType,
                  productName: plan.productName,
                  model: plan.model,
                  materialCode: plan.materialCode,
                  quantity: plan.quantity,
                  volume: plan.volume,
                  status: plan.status,
                  assignedQuantity: plan.assignedQuantity,
                  length: plan.length,
                  width: plan.width,
                  height: plan.height,
                  startDate: plan.startDate,
                  endDate: plan.endDate,
                  strength: plan.strength,
                  remarkOne: plan.remarkOne,
                  remarkTwo: plan.remarkTwo,
                };
              }),
            };
          });
          sourcePage.total = sourceTableData.value.length;
          // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªç”³è¯·å•
          if (sourceTableData.value.length > 0) {
            selectApplyNo(sourceTableData.value[0]);
          } else {
            selectedApplyNo.value = "";
            selectedSourceData.value = null;
          }
          // æ‰“开弹窗
          sourceDataDialogVisible.value = true;
        } else {
          proxy.$modal.msgError(res.msg || "获取来源数据失败");
        }
      })
      .catch(err => {
        proxy.$modal.msgError("获取来源数据失败");
        console.error(err);
      });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = (selection) => {
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const handleDeleteSolo = row => {
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        delProductOrder(row.id).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map((item) => item.id);
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    ElMessageBox.confirm("选中的内容将被退回,是否确认退回?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      delProductOrder(ids).then((res) => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
    })
      .then(() => {
        revokeProductOrder(ids).then(res => {
          proxy.$modal.msgSuccess("退回成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
    }).catch(() => {
      proxy.$modal.msg("已取消");
    });
  };
  // å¯¼å‡º
@@ -436,7 +949,11 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/productOrder/export", {...searchForm.value}, "生产订单.xlsx");
        proxy.download(
          "/productOrder/export",
          { ...searchForm.value },
          "生产订单.xlsx"
        );
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -444,30 +961,274 @@
  };
  const handleConfirmRoute = () => {};
  const productTypeOptions2 = ref([]);
  // èŽ·å–äº§å“ç±»åž‹å­—å…¸
  const getProductTypeOptions = () => {
    getDicts("block_strength")
      .then(res => {
        if (res.code === 200) {
          productTypeOptions2.value = res.data;
        }
      })
      .catch(err => {
        console.error("获取产品类型字典失败:", err);
      });
  };
  onMounted(() => {
    getProductTypeOptions();
    getList();
  });
</script>
<style scoped lang="scss">
.search_form{
  align-items: start;
}
  .search_form {
    align-items: start;
  }
::v-deep .yellow {
  background-color: #FAF0DE;
}
  ::v-deep .yellow {
    background-color: #faf0de;
  }
::v-deep .pink {
  background-color: #FAE1DE;
}
  ::v-deep .pink {
    background-color: #fae1de;
  }
::v-deep .red {
  background-color: #f80202;
}
  ::v-deep .red {
    background-color: #f80202;
  }
::v-deep .purple{
  background-color: #F4DEFA;
}
  ::v-deep .purple {
    background-color: #f4defa;
  }
</style>
<style lang="scss">
  .status-cell {
    font-weight: 600;
    color: #409eff;
    font-family: "Courier New", monospace;
    text-shadow: 0 1px 2px rgba(64, 158, 255, 0.2);
  }
  .source-data-container {
    display: flex;
    gap: 20px;
    height: 500px;
    .applyno-list {
      width: 250px;
      background: #fff;
      border-radius: 8px;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
      overflow: hidden;
      .list-header {
        padding: 12px 16px;
        background: #f5f7fa;
        border-bottom: 1px solid #e4e7ed;
        font-weight: 600;
        color: #303133;
      }
      .list-body {
        height: calc(100% - 48px);
        overflow-y: auto;
        .applyno-item {
          padding: 12px 16px;
          border-bottom: 1px solid #f0f2f5;
          cursor: pointer;
          transition: all 0.3s;
          &:hover {
            background: #f5f7fa;
          }
          &.active {
            background: #ecf5ff;
            border-left: 4px solid #409eff;
          }
          .applyno-text {
            font-weight: 600;
            color: #303133;
            font-family: "Courier New", monospace;
            margin-bottom: 4px;
          }
          .applyno-info {
            font-size: 12px;
            color: #909399;
          }
        }
      }
    }
    .detail-info {
      flex: 1;
      background: #fff;
      border-radius: 8px;
      // box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
      overflow: auto;
      display: flex;
      flex-direction: column;
      .applyno-summary {
        padding: 16px 20px;
        background: #f5f7fa;
        border-bottom: 1px solid #e4e7ed;
        display: flex;
        flex-wrap: wrap;
        gap: 16px;
        .summary-item {
          display: flex;
          align-items: center;
          .summary-label {
            font-size: 13px;
            color: #909399;
            margin-right: 8px;
            font-weight: 500;
          }
          .summary-value {
            font-size: 14px;
            color: #303133;
            font-weight: 500;
          }
        }
      }
      .empty-state {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .source-data-card {
        flex: 1;
        display: flex;
        flex-direction: column;
        overflow: hidden;
        margin-top: 20px;
        margin-right: 20px;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
        .card-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 16px 20px;
          background: #f5f7fa;
          border-bottom: 1px solid #e4e7ed;
          .data-source-tag {
            flex-shrink: 0;
          }
          .card-title {
            font-weight: 600;
            color: #303133;
            font-size: 14px;
          }
        }
        .card-body {
          flex: 1;
          padding: 20px;
          overflow-y: auto;
          background-color: #f5f7fa;
          .info-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 16px;
            margin-bottom: 20px;
            .info-item {
              display: flex;
              flex-direction: column;
              .info-label {
                font-size: 12px;
                color: #909399;
                margin-bottom: 4px;
                font-weight: 500;
              }
              .info-value {
                font-size: 14px;
                color: #303133;
                font-weight: 500;
              }
            }
            .info-item.full-width {
              grid-column: 1 / -1;
            }
          }
          .remarks-section {
            display: flex;
            // flex-direction: column;
            gap: 12px;
            border-top: 1px solid #e4e7ed;
            padding-top: 16px;
            .info-item {
              display: flex;
              width: 50%;
              flex-direction: column;
              .info-label {
                font-size: 12px;
                color: #909399;
                margin-bottom: 4px;
                font-weight: 500;
              }
              .info-value {
                font-size: 14px;
                color: #303133;
                line-height: 1.5;
                padding: 8px;
                background: #f9fafc;
                border-radius: 4px;
                border: 1px solid #ecf5ff;
              }
            }
          }
        }
      }
    }
  }
  .applyno-summary1 {
    padding: 16px 20px;
    background: #f5f7fa;
    border-bottom: 1px solid #e4e7ed;
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    .summary-item {
      display: flex;
      align-items: center;
      margin-right: 20px;
      .summary-label {
        font-size: 13px;
        color: #909399;
        margin-right: 8px;
        font-weight: 500;
      }
      .summary-value {
        font-size: 14px;
        color: #303133;
        font-weight: 500;
      }
    }
  }
</style>
src/views/productionManagement/productionProcess/index.vue
@@ -51,9 +51,9 @@
                  {{ process.status ? '启用' : '停用' }}
                </el-tag>
                <el-tag size="small"
                        :type="process.isQuality ? 'warning' : 'info'"
                        :type="process.isQuality == 1 ? 'warning' : 'info'"
                        style="margin-left: 8px">
                  {{ process.isQuality ? '质检' : '非质检' }}
                  {{ process.isQuality == 1 ? '质检' : '非质检' }}
                </el-tag>
              </div>
              <span class="param-count">工资定额: Â¥{{ process.salaryQuota || 0 }}</span>
@@ -119,8 +119,8 @@
        <el-form-item label="是否质检"
                      prop="isQuality">
          <el-switch v-model="processForm.isQuality"
                     :active-value="true"
                     inactive-value="false" />
                     :active-value="1"
                     :inactive-value="0" />
        </el-form-item>
        <el-form-item label="工序描述"
                      prop="remark">
@@ -249,8 +249,8 @@
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired"
                         :active-value="1"
                         :inactive-value="0" />
                         :active-value="true"
                         :inactive-value="false" />
            </el-form-item>
          </el-form>
          <el-empty v-else
@@ -313,8 +313,8 @@
        <el-form-item label="是否必填"
                      prop="isRequired">
          <el-switch v-model="editParamForm.isRequired"
                     :active-value="1"
                     :inactive-value="0" />
                     :active-value="true"
                     :inactive-value="false" />
        </el-form-item>
      </el-form>
      <template #footer>
@@ -368,7 +368,7 @@
    no: "",
    name: "",
    salaryQuota: null,
    isQuality: false,
    isQuality: 0,
    remark: "",
    status: true,
  });
@@ -418,7 +418,7 @@
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
    isRequired: false,
    tenantId: 1,
  });
  const editParamRules = {
@@ -562,8 +562,8 @@
      label: "是否必填",
      prop: "isRequired",
      dataType: "tag",
      formatType: row => (row.isRequired === 1 ? "success" : "info"),
      formatData: row => (row.isRequired === 1 ? "是" : "否"),
      formatType: row => (row.isRequired === true ? "success" : "info"),
      formatData: row => (row.isRequired === true ? "是" : "否"),
    },
    {
      label: "操作",
@@ -626,7 +626,7 @@
    processForm.no = "";
    processForm.name = "";
    processForm.salaryQuota = null;
    processForm.isQuality = false;
    processForm.isQuality = 0;
    processForm.remark = "";
    processForm.status = true;
    processDialogVisible.value = true;
@@ -638,7 +638,7 @@
    processForm.no = process.no;
    processForm.name = process.name;
    processForm.salaryQuota = process.salaryQuota;
    processForm.isQuality = process.isQuality || false;
    processForm.isQuality = process.isQuality || 0;
    processForm.remark = process.remark || "";
    processForm.status = process.status;
    processDialogVisible.value = true;
@@ -797,7 +797,7 @@
    editParamForm.minValue = row.minValue;
    editParamForm.maxValue = row.maxValue;
    editParamForm.sort = row.sort || 1;
    editParamForm.isRequired = row.isRequired || 0;
    editParamForm.isRequired = row.isRequired || false;
    editParamForm.tenantId = 1;
    editParamDialogVisible.value = true;
  };
@@ -830,7 +830,7 @@
      standardValue: selectedParam.value.standardValue,
      minValue: selectedParam.value.minValue,
      maxValue: selectedParam.value.maxValue,
      isRequired: selectedParam.value.isRequired || 0,
      isRequired: selectedParam.value.isRequired || false,
      tenantId: 1,
    })
      .then(() => {
@@ -990,17 +990,52 @@
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    display: flex;
    flex-direction: column;
    min-width: 0;
  }
  .param-table-wrapper {
    flex: 1;
    padding: 0 20px 20px;
    overflow: auto;
    min-width: 100%;
  }
  /* è¡¨æ ¼æ¨ªå‘滚动 */
  :deep(.el-table) {
    // min-width: 800px;
  .param-table-wrapper :deep(.el-table) {
    min-width: 100%;
  }
  .param-table-wrapper :deep(.el-table__body-wrapper) {
    overflow-x: auto;
  }
  .pagination-container {
    margin-top: 10px;
    overflow-x: auto;
    padding-bottom: 8px;
  }
  .pagination-container .el-pagination {
    white-space: nowrap;
  }
  /* å“åº”式调整 */
  @media screen and (max-width: 768px) {
    .pagination-container {
      font-size: 12px;
    }
    .pagination-container .el-pagination__sizes {
      margin-right: 8px;
    }
    .pagination-container .el-pagination__jump {
      margin-left: 8px;
    }
    .pagination-container .el-pagination__page-size {
      font-size: 12px;
    }
  }
  .empty-tip {
src/views/productionManagement/productionReporting/Input.vue
ÎļþÒÑɾ³ý
src/views/productionManagement/productionReporting/Output.vue
ÎļþÒÑɾ³ý
src/views/productionManagement/productionReporting/components/formDia.vue
ÎļþÒÑɾ³ý
src/views/productionManagement/productionReporting/index.vue
@@ -3,418 +3,557 @@
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="报工人员名称:">
          <el-input v-model="searchForm.nickName"
        <el-form-item label="生产订单号:">
          <el-input v-model="searchForm.orderNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="工单号:">
          <el-input v-model="searchForm.workOrderNo"
        <el-form-item label="班组:">
          <el-select v-model="searchForm.teamName"
                     placeholder="请选择"
                     clearable
                     style="width: 160px;"
                     @keyup.enter="handleQuery">
            <el-option label="白班"
                       value="白班" />
            <el-option label="夜班"
                       value="夜班" />
          </el-select>
          <!-- <el-input v-model="searchForm.teamName"
                    placeholder="请输入""
                    @keyup.enter="handleQuery" /> -->
        </el-form-item>
        <el-form-item label="产品名称:">
          <el-input v-model="searchForm.productName"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
          <el-button type="primary"
                     @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button type="primary"
                   @click="handleAdd">新增</el-button>
        <el-button @click="handleExport">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <div style="text-align: right"
           class="mb10">
        <!-- <el-button type="primary"
                   @click="openForm('add')">生产报工</el-button> -->
        <el-button @click="handleOut">导出</el-button>
      </div>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                :expandRowKeys="expandedRowKeys"
                @expand-change="expandChange"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total">
        <template #expand="{ row }">
          <el-table :data="expandData"
                    border
                    show-summary
                    :summary-method="summarizeMainTable"
                    v-loading="childrenLoading">
            <el-table-column align="center"
                             label="序号"
                             type="index"
                             width="60" />
            <el-table-column label="本次生产数量"
                             prop="finishedNum"
                             align="center"
                             width="400">
              <template #default="scope">
                <el-input-number :step="0.01"
                                 :min="0"
                                 style="width: 100%"
                                 v-model="scope.row.finishedNum"
                                 :disabled="!scope.row.editType"
                                 :precision="2"
                                 placeholder="请输入"
                                 clearable
                                 @change="changeNum(scope.row)" />
              </template>
            </el-table-column>
            <!--                        <el-table-column label="待生产数量" prop="pendingNum" width="240" align="center"></el-table-column>-->
            <el-table-column label="生产人"
                             prop="schedulingUserId"
                             width="400">
              <template #default="scope">
                <el-select v-model="scope.row.schedulingUserId"
                           placeholder="选择人员"
                           :disabled="!scope.row.editType"
                           style="width: 100%;">
                  <el-option v-for="user in userList"
                             :key="user.userId"
                             :label="user.nickName"
                             :value="user.userId" />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="生产日期"
                             prop="schedulingDate"
                             width="400">
              <template #default="scope">
                <el-date-picker v-model="scope.row.schedulingDate"
                                type="date"
                                :disabled="!scope.row.editType"
                                placeholder="请选择日期"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                clearable
                                style="width: 100%" />
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             >
              <template #default="scope">
                <el-button link
                           type="primary"
                           size="small"
                           @click="changeEditType(scope.row)"
                           v-if="!scope.row.editType"
                           :disabled="scope.row.parentStatus === 3">编辑</el-button>
                <el-button link
                           type="primary"
                           size="small"
                           @click="saveReceiptPayment(scope.row)"
                           v-if="scope.row.editType">保存</el-button>
              </template>
            </el-table-column>
          </el-table>
                :isSelection="false"
                @selection-change="handleSelectionChange"
                @pagination="pagination">
        <template #outputVolume="{ row }">
          <span style="font-weight: bold;color: #409eff;">{{ row.outputVolume }}</span><span style="margin-left: 5px;color: #909399;">方</span>
        </template>
        <template #unqualifiedVolume="{ row }">
          <span style="font-weight: bold;color: #b43434;">{{ row.unqualifiedVolume }}</span><span style="margin-left: 5px;color: #909399;">方</span>
        </template>
        <template #completedVolume="{ row }">
          <span style="font-weight: bold;color: #28e431;">{{ row.completedVolume }}</span><span style="margin-left: 5px;color: #909399;">方</span>
        </template>
      </PIMTable>
    </div>
    <form-dia ref="formDia"
              @close="handleQuery"></form-dia>
    <input-modal v-if="isShowInput"
                 v-model:visible="isShowInput"
                 :production-product-main-id="isShowingId" />
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import FormDia from "@/views/productionManagement/productionReporting/components/formDia.vue";
  import { ElMessageBox } from "element-plus";
  import { onMounted, ref, reactive, getCurrentInstance } from "vue";
  import { useRouter } from "vue-router";
  import { ElMessage, ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import {
    workListPage,
    productionReport,
    productionReportUpdate,
    workListPageById,
    productionReportDelete,
  } from "@/api/productionManagement/productionReporting.js";
  import { productionProductMainListPage } from "@/api/productionManagement/productionProductMain.js";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  import InputModal from "@/views/productionManagement/productionReporting/Input.vue";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  const data = reactive({
    searchForm: {
      nickName: "",
      workOrderNo: "",
      workOrderStatus: "",
    },
  });
  const { searchForm } = toRefs(data);
  const expandedRowKeys = ref([]);
  const expandData = ref([]);
  const userList = ref([]);
  const router = useRouter();
  const { proxy } = getCurrentInstance();
  const tableColumn = ref([
    {
      label: "报工单号",
      prop: "productNo",
      width: 120,
      label: "生产订单号",
      prop: "orderNo",
    },
    {
      label: "报工人员",
      prop: "nickName",
      width: 120,
      label: "班组",
      prop: "teamName",
      width: "120px",
      dataType: "tag",
      formatType: params => {
        return params === "白班" ? "primary" : "warning";
      },
    },
    {
      label: "工序",
      prop: "process",
      width: 120,
    },
    {
      label: "工单编号",
      prop: "workOrderNo",
      width: 120,
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
      width: 120,
      label: "产品编码",
      prop: "materialCode",
      width: "150px",
    },
    {
      label: "产品名称",
      prop: "productName",
      width: 120,
      width: "150px",
    },
    {
      label: "产品规格型号",
      prop: "productModelName",
      width: 120,
      label: "规格",
      prop: "specification",
      width: "120px",
      className: "specification-cell",
    },
    {
      label: "产出数量",
      prop: "quantity",
      width: 120,
      label: "产出方量",
      prop: "outputVolume",
      width: "120px",
      align: "right",
      dataType: "slot",
      slot: "outputVolume",
    },
    {
      label: "报废数量",
      prop: "scrapQty",
      width: 120,
      label: "不合格方量",
      prop: "unqualifiedVolume",
      width: "120px",
      align: "right",
      dataType: "slot",
      slot: "unqualifiedVolume",
    },
    {
      label: "单位",
      prop: "unit",
      width: 120,
      label: "完成方量",
      prop: "completedVolume",
      width: "120px",
      align: "right",
      dataType: "slot",
      slot: "completedVolume",
    },
    {
      label: "创建人",
      prop: "createBy",
      width: "120px",
      dataType: "tag",
    },
    {
      label: "创建时间",
      prop: "createTime",
      width: 120,
      width: "160px",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD HH:mm:ss") : ""),
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      dataType: "action",
      width: "200px",
      fixed: "right",
      operation: [
        {
          name: "查看投入",
          name: "详情",
          type: "text",
          clickFun: row => {
            showInput(row);
            handleDetail(row);
          },
        },
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            handleEdit(row);
          },
        },
        {
          name: "删除",
          type: "danger",
          type: "text",
          clickFun: row => {
            deleteReport(row);
            handleDelete(row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const childrenLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    size: 10,
    total: 0,
  });
  const formDia = ref();
  const { proxy } = getCurrentInstance();
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const searchForm = reactive({
    orderNo: "",
    teamName: "",
    productName: "",
  });
  const mockData = [
    {
      id: 1,
      orderNo: "PO202401001",
      teamName: "白班",
      materialCode: "PC001",
      productName: "标准砌块",
      specification: "600×240×200",
      outputVolume: 120.5,
      unqualifiedVolume: 2.3,
      completedVolume: 118.2,
      createBy: "张三",
      createTime: "2024-01-15 08:30:00",
    },
    {
      id: 2,
      orderNo: "PO202401002",
      teamName: "夜班",
      materialCode: "PC002",
      productName: "标准砌块",
      specification: "600×240×200",
      outputVolume: 150.8,
      unqualifiedVolume: 1.5,
      completedVolume: 149.3,
      createBy: "李四",
      createTime: "2024-01-15 09:15:00",
    },
    {
      id: 3,
      orderNo: "PO202401003",
      teamName: "白班",
      materialCode: "PC003",
      productName: "加气砌块",
      specification: "600×240×250",
      outputVolume: 95.2,
      unqualifiedVolume: 0.8,
      completedVolume: 94.4,
      createBy: "王五",
      createTime: "2024-01-15 10:00:00",
    },
    {
      id: 4,
      orderNo: "PO202401004",
      teamName: "白班",
      materialCode: "PC004",
      productName: "标准砌块",
      specification: "600×240×200",
      outputVolume: 180.6,
      unqualifiedVolume: 3.2,
      completedVolume: 177.4,
      createBy: "赵六",
      createTime: "2024-01-15 14:20:00",
    },
    {
      id: 5,
      orderNo: "PO202401005",
      teamName: "夜班",
      materialCode: "PC005",
      productName: "加气砌块",
      specification: "600×240×250",
      outputVolume: 110.3,
      unqualifiedVolume: 1.1,
      completedVolume: 109.2,
      createBy: "孙七",
      createTime: "2024-01-15 15:45:00",
    },
    {
      id: 6,
      orderNo: "PO202401006",
      teamName: "白班",
      materialCode: "PC006",
      productName: "标准砌块",
      specification: "600×240×200",
      outputVolume: 135.7,
      unqualifiedVolume: 2.5,
      completedVolume: 133.2,
      createBy: "周八",
      createTime: "2024-01-16 08:00:00",
    },
    {
      id: 7,
      orderNo: "PO202401007",
      teamName: "白班",
      materialCode: "PC007",
      productName: "加气砌块",
      specification: "600×240×250",
      outputVolume: 88.4,
      unqualifiedVolume: 0.6,
      completedVolume: 87.8,
      createBy: "吴九",
      createTime: "2024-01-16 09:30:00",
    },
    {
      id: 8,
      orderNo: "PO202401008",
      teamName: "夜班",
      materialCode: "PC008",
      productName: "标准砌块",
      specification: "600×240×200",
      outputVolume: 165.2,
      unqualifiedVolume: 2.8,
      completedVolume: 162.4,
      createBy: "郑十",
      createTime: "2024-01-16 11:00:00",
    },
    {
      id: 9,
      orderNo: "PO202401009",
      teamName: "白班",
      materialCode: "PC009",
      productName: "加气砌块",
      specification: "600×240×250",
      outputVolume: 102.5,
      unqualifiedVolume: 1.3,
      completedVolume: 101.2,
      createBy: "钱十一",
      createTime: "2024-01-16 13:15:00",
    },
    {
      id: 10,
      orderNo: "PO202401010",
      teamName: "白班",
      materialCode: "PC010",
      productName: "标准砌块",
      specification: "600×240×200",
      outputVolume: 142.8,
      unqualifiedVolume: 2.1,
      completedVolume: 140.7,
      createBy: "刘十二",
      createTime: "2024-01-16 15:00:00",
    },
  ];
  const form = reactive({
    id: undefined,
    orderId: "",
    orderNo: "",
    teamName: "",
    materialCode: "",
    productName: "",
    specification: "",
    outputVolume: 0,
    unqualifiedVolume: 0,
    completedVolume: 0,
    processId: "",
    params: {},
  });
  const selectedRows = ref([]);
  const getList = () => {
    tableLoading.value = true;
    setTimeout(() => {
      tableLoading.value = false;
      const start = (page.current - 1) * page.size;
      const end = start + page.size;
      tableData.value = mockData.slice(start, end);
      page.total = mockData.length;
    }, 500);
  };
  const handleQuery = () => {
    page.current = 1;
    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 handleReset = () => {
    searchForm.orderNo = "";
    searchForm.teamName = "";
    searchForm.productName = "";
    page.current = 1;
    getList();
  };
  const deleteReport = row => {
    ElMessageBox.confirm("确定删除该报工吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      productionReportDelete({ id: row.id }).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        } else {
          ElMessageBox.alert(res.msg || "删除失败", "提示", {
            confirmButtonText: "确定",
          });
        }
      });
    });
  };
  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;
    expandedRowKeys.value = [];
    productionProductMainListPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records.map(item => ({
          ...item,
          pendingFinishNum:
            (Number(item.schedulingNum) || 0) - (Number(item.finishedNum) || 0),
        }));
        page.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // å±•开行
  const expandChange = (row, expandedRows) => {
    userListNoPageByTenantId().then(res => {
      userList.value = res.data;
    });
    if (expandedRows.length > 0) {
      nextTick(() => {
        expandedRowKeys.value = [];
        try {
          childrenLoading.value = true;
          workListPageById({ id: row.id }).then(res => {
            childrenLoading.value = false;
            const index = tableData.value.findIndex(item => item.id === row.id);
            if (index > -1) {
              expandData.value = res.data.map(item => ({
                ...item,
                pendingNum:
                  (Number(item.schedulingNum) || 0) -
                  (Number(item.finishedNum) || 0),
                parentStatus: row.status, // æ–°å¢žçˆ¶è¡¨çŠ¶æ€
              }));
            }
            expandedRowKeys.value.push(row.id);
          });
        } catch (error) {
          childrenLoading.value = false;
          console.log(error);
        }
      });
    } else {
      expandedRowKeys.value = [];
    }
  };
  const changeNum = row => {
    // æ‰¾åˆ°çˆ¶è¡¨æ ¼æ•°æ®
    const parentRow = tableData.value.find(
      item => item.id === expandedRowKeys.value[0]
    );
    // è®¡ç®—所有子表格 finishedNum çš„æ€»å’Œ
    const totalFinishedNum = expandData.value.reduce(
      (sum, item) => sum + (Number(item.finishedNum) || 0),
      0
    );
    // çˆ¶è¡¨æ ¼çš„æŽ’产数量
    const schedulingNum = parentRow ? Number(parentRow.schedulingNum) : 0;
    if (totalFinishedNum > schedulingNum) {
      // å›žé€€æœ¬æ¬¡è¾“å…¥
      row.finishedNum =
        schedulingNum - (totalFinishedNum - Number(row.finishedNum));
      proxy.$modal.msgWarning("所有本次生产数量之和不可大于排产数量");
    }
    row.pendingNum = row.schedulingNum - row.finishedNum;
  };
  // ç¼–辑修改状态
  const changeEditType = row => {
    row.editType = !row.editType;
  };
  // ä¿å­˜è®°å½•
  const saveReceiptPayment = row => {
    productionReportUpdate(row).then(res => {
      row.editType = !row.editType;
      getList();
      proxy.$modal.msgSuccess("提交成功");
    });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const summarizeMainTable = param => {
    return proxy.summarizeTable(param, ["finishedNum"]);
  };
  // æ‰“开弹框
  const openForm = (type, row) => {
    if (selectedRows.value.length !== 1) {
      proxy.$message.error("请选择一条数据");
      return;
    }
    if (selectedRows.value[0].pendingFinishNum == 0) {
      proxy.$message.warning("无需再报工");
      return;
    }
    nextTick(() => {
      const rowInfo = type === "add" ? selectedRows.value[0] : row;
      formDia.value?.openDialog(type, rowInfo);
  const handleAdd = () => {
    Object.assign(form, {
      id: undefined,
      orderId: "",
      orderNo: "",
      teamName: "",
      materialCode: "",
      productName: "",
      specification: "",
      outputVolume: 0,
      unqualifiedVolume: 0,
      completedVolume: 0,
      processId: "",
      params: {},
    });
    router.push({
      path: "/productionManagement/ReportingDialog",
      // query: { data: JSON.stringify(form) },
    });
  };
  // æ‰“开投入模态框
  const isShowInput = ref(false);
  const isShowingId = ref(0);
  const showInput = row => {
    isShowInput.value = true;
    isShowingId.value = row.id;
  const handleEdit = row => {
    Object.assign(form, {
      id: row.id,
      orderId: row.orderId || "",
      orderNo: row.orderNo,
      teamName: row.teamName,
      materialCode: row.materialCode,
      productName: row.productName,
      specification: row.specification,
      outputVolume: row.outputVolume,
      unqualifiedVolume: row.unqualifiedVolume,
      completedVolume: row.completedVolume,
      createBy: row.createBy || "",
      createTime: row.createTime || new Date(),
      processId: row.processId || "",
      params: row.params || {},
    });
    router.push({
      path: "/productionManagement/ReportingDialog",
      query: { data: JSON.stringify(form) },
    });
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
  const handleDetail = row => {
    ElMessage.info("详情功能待实现");
  };
  const handleDelete = row => {
    ElMessageBox.confirm("确认删除该条数据吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/productionProductMain/export", {}, "生产报工.xlsx");
        productionReportDelete({ id: row.id })
          .then(() => {
            ElMessage.success("删除成功");
            getList();
          })
          .catch(() => {
            ElMessage.error("删除失败");
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
      .catch(() => {});
  };
  const handleExport = () => {
    ElMessage.info("导出功能待实现");
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped></style>
<style scoped lang="scss">
  .app-container {
    padding: 24px;
    background-color: #f0f2f5;
    min-height: calc(100vh - 48px);
  }
  .search_form {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 16px;
    margin-bottom: 24px;
    padding: 20px;
    background-color: #ffffff;
    border-radius: 6px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
    &:hover {
      box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
    }
    :deep(.el-form) {
      display: flex;
      flex-wrap: wrap;
      gap: 0;
      .el-form-item {
        margin-right: 16px;
        margin-bottom: 0;
        &:last-child {
          margin-right: 0;
        }
      }
    }
    > div {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      flex-shrink: 0;
    }
  }
  .table_list {
    background-color: #ffffff;
    border-radius: 6px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
    overflow: hidden;
    height: calc(100vh - 220px);
  }
  :deep(.el-table) {
    border: none;
    border-radius: 6px;
    overflow: hidden;
    .el-table__header-wrapper {
      background-color: #fafafa;
      th {
        background-color: #fafafa;
        font-weight: 600;
        color: #303133;
        border-bottom: 1px solid #ebeef5;
        padding: 14px 0;
      }
    }
    .el-table__body-wrapper {
      tr {
        transition: all 0.3s ease;
        &:hover {
          background-color: #f5f7fa;
        }
        td {
          border-bottom: 1px solid #ebeef5;
          padding: 12px 0;
        }
      }
      tr.current-row {
        background-color: #ecf5ff;
      }
    }
    .el-table__empty-block {
      padding: 40px 0;
    }
  }
</style>
<style lang="scss">
  .specification-cell {
    color: #7a7d81;
    font-style: italic;
  }
</style>
src/views/productionManagement/productionReporting/reportingDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1706 @@
<template>
  <div class="reporting-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader content="生产工单报工">
    </PageHeader>
    <!-- æ­¥éª¤æŒ‡ç¤ºå™¨ -->
    <div class="step-indicator">
      <div v-for="(step, index) in steps"
           :key="index"
           class="step-item"
           :class="{
          'active': index === activeStep,
          'completed': index < activeStep
        }">
        <div class="step-circle">
          <span v-if="index < activeStep"
                class="step-check">
            <el-icon>
              <Check />
            </el-icon>
          </span>
          <span v-else
                class="step-number">{{ index + 1 }}</span>
        </div>
        <div class="step-content">
          <div class="step-title">{{ step.title }}</div>
          <div class="step-description">{{ step.description }}</div>
        </div>
        <div class="step-line"
             v-if="index < steps.length - 1"></div>
      </div>
    </div>
    <!-- é¡µé¢å†…容 -->
    <div class="page-content">
      <!-- ç¬¬ä¸€æ­¥ï¼šé€‰æ‹©ç”Ÿäº§è®¢å• -->
      <div v-if="activeStep === 0"
           class="step-panel">
        <div class="panel-header">
          <div>
            <h3 class="panel-title">选择生产订单</h3>
            <p class="panel-subtitle">请从以下列表中选择需要报工的生产订单</p>
          </div>
          <div class="header-actions">
            <el-button @click="activeStep--"
                       v-if="activeStep > 0"
                       :disabled="isSubmitting">
              <el-icon>
                <ArrowLeft />
              </el-icon> ä¸Šä¸€æ­¥
            </el-button>
            <el-button type="primary"
                       @click="handleNextStep"
                       v-if="activeStep < 3"
                       :disabled="isSubmitting">
              ä¸‹ä¸€æ­¥ <el-icon>
                <ArrowRight />
              </el-icon>
            </el-button>
          </div>
        </div>
        <el-form :model="form"
                 ref="formRef"
                 class="form-container">
          <el-form-item label="生产订单"
                        prop="orderId"
                        required>
            <el-select v-model="orderId"
                       placeholder="请选择生产订单"
                       clearable
                       filterable
                       class="form-select"
                       :loading="orderLoading"
                       @change="handleOrderChange">
              <el-option v-for="order in orderList"
                         :key="order.id"
                         :label="`${order.npsNo} - ${order.productName} ${order.model}`"
                         :value="order.id" />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
      <!-- ç¬¬äºŒæ­¥ï¼šå¡«å†™åŸºç¡€ä¿¡æ¯ -->
      <div v-else-if="activeStep === 1"
           class="step-panel">
        <div class="panel-header">
          <div>
            <h3 class="panel-title">填写基础信息</h3>
            <p class="panel-subtitle">请填写报工的基本信息</p>
          </div>
          <div class="header-actions">
            <el-button @click="activeStep--"
                       v-if="activeStep > 0"
                       :disabled="isSubmitting">
              <el-icon>
                <ArrowLeft />
              </el-icon> ä¸Šä¸€æ­¥
            </el-button>
            <el-button type="primary"
                       @click="handleNextStep"
                       v-if="activeStep < 3"
                       :disabled="isSubmitting">
              ä¸‹ä¸€æ­¥ <el-icon>
                <ArrowRight />
              </el-icon>
            </el-button>
          </div>
        </div>
        <el-form :model="form"
                 :rules="rules"
                 ref="formRef"
                 class="form-container">
          <div class="form-grid">
            <el-form-item label="生产订单号"
                          prop="npsNo"
                          class="form-item">
              <el-input disabled
                        v-model="form.npsNo"
                        class="form-input" />
            </el-form-item>
            <el-form-item label="产品编码"
                          prop="materialCode"
                          class="form-item">
              <el-input disabled
                        v-model="form.materialCode"
                        class="form-input" />
            </el-form-item>
            <el-form-item label="产品名称"
                          prop="productName"
                          class="form-item">
              <el-input disabled
                        v-model="form.productName"
                        class="form-input" />
            </el-form-item>
            <el-form-item label="规格"
                          prop="specification"
                          class="form-item">
              <el-input disabled
                        v-model="form.specification"
                        class="form-input" />
            </el-form-item>
            <el-form-item label="创建时间"
                          prop="createTime"
                          class="form-item">
              <el-date-picker disabled
                              v-model="form.createTime"
                              type="datetime"
                              placeholder="请选择创建时间"
                              class="form-input" />
            </el-form-item>
            <el-form-item label="班组"
                          prop="teamName"
                          required
                          class="form-item">
              <el-select v-model="form.teamName"
                         placeholder="请选择班组"
                         class="form-select">
                <el-option label="白班"
                           value="白班" />
                <el-option label="夜班"
                           value="夜班" />
              </el-select>
            </el-form-item>
            <el-form-item label="创建人"
                          prop="createBy"
                          required
                          class="form-item">
              <el-select v-model="form.createBy"
                         placeholder="请选择创建人"
                         class="form-select"
                         :loading="userLoading">
                <el-option v-for="user in userList"
                           :key="user.id"
                           :label="user.nickName || user.userName"
                           :value="user.nickName || user.userName" />
              </el-select>
            </el-form-item>
          </div>
        </el-form>
      </div>
      <!-- ç¬¬ä¸‰æ­¥ï¼šæŸ¥çœ‹å·¥åºå‚æ•° -->
      <div v-else-if="activeStep === 2"
           class="step-panel process-panel">
        <div class="panel-header">
          <div>
            <h3 class="panel-title">工序参数管理</h3>
            <p class="panel-subtitle">请查看并填写各工序的参数信息</p>
          </div>
          <div class="header-actions">
            <el-button @click="activeStep--"
                       v-if="activeStep > 0"
                       :disabled="isSubmitting">
              <el-icon>
                <ArrowLeft />
              </el-icon> ä¸Šä¸€æ­¥
            </el-button>
            <el-button type="primary"
                       @click="handleNextStep"
                       v-if="activeStep < 3"
                       :disabled="isSubmitting">
              ä¸‹ä¸€æ­¥ <el-icon>
                <ArrowRight />
              </el-icon>
            </el-button>
          </div>
        </div>
        <div class="process-container">
          <!-- å·¦ä¾§å·¥åºå¯¼èˆª -->
          <div class="process-nav">
            <div v-for="process in processList"
                 :key="process.processId"
                 class="process-nav-item"
                 :class="{ 'active': activeProcessId === process.processId + '' }"
                 @click="handleProcessClick(process.processId)">
              <span class="process-name">{{ process.processName }}</span>
              <span class="process-badge"
                    v-if="getProcessInfo(parseInt(process.processId)).postName">
                {{ getProcessInfo(parseInt(process.processId)).postName }}
              </span>
            </div>
          </div>
          <!-- å³ä¾§å·¥åºå†…容 -->
          <div class="process-content">
            <div v-if="activeProcessId"
                 class="process-details">
              <!-- å›ºå®šå‚æ•° -->
              <div class="param-section">
                <div class="section-header">
                  <h4 class="section-title">工序基本信息</h4>
                </div>
                <div class="param-form">
                  <el-form :label-position="'top'">
                    <div class="form-grid">
                      <el-form-item label="岗位人员"
                                    class="form-item">
                        <el-select v-model="getProcessInfo(parseInt(activeProcessId)).postName"
                                   placeholder="请选择岗位人员"
                                   class="form-select"
                                   :loading="userLoading">
                          <el-option v-for="user in userList"
                                     :key="user.id"
                                     :label="user.nickName || user.userName"
                                     :value="user.nickName || user.userName" />
                        </el-select>
                      </el-form-item>
                      <el-form-item label="设备异常情况"
                                    class="form-item">
                        <el-input v-model="getProcessInfo(parseInt(activeProcessId)).equipmentMalfunction"
                                  placeholder="请输入设备异常情况"
                                  type="textarea"
                                  :rows="2"
                                  class="form-textarea" />
                      </el-form-item>
                      <el-form-item label="当班设备处置"
                                    class="form-item">
                        <el-input v-model="getProcessInfo(parseInt(activeProcessId)).equipmentDisposal"
                                  placeholder="请输入当班设备处置"
                                  type="textarea"
                                  :rows="2"
                                  class="form-textarea" />
                      </el-form-item>
                      <el-form-item label="工艺人员交待"
                                    class="form-item">
                        <el-input v-model="getProcessInfo(parseInt(activeProcessId)).processExplained"
                                  placeholder="请输入工艺人员交待"
                                  type="textarea"
                                  :rows="2"
                                  class="form-textarea" />
                      </el-form-item>
                      <el-form-item label="上传文件"
                                    class="form-item"
                                    :span="24">
                        <el-upload class="upload-demo upload-block"
                                   action="#"
                                   :on-preview="handlePreview"
                                   :on-remove="handleRemove"
                                   :file-list="getProcessInfo(parseInt(activeProcessId)).files || []"
                                   :auto-upload="false"
                                   :accept="'.jpg,.png'"
                                   :max-size="500000"
                                   :on-change="handleFileChange">
                          <el-button type="primary"
                                     :icon="Upload">点击上传</el-button>
                          <template #tip>
                            <div class="el-upload__tip">
                              åªèƒ½ä¸Šä¼ jpg/png文件,且不超过500kb
                            </div>
                          </template>
                        </el-upload>
                      </el-form-item>
                    </div>
                  </el-form>
                </div>
              </div>
              <!-- BOM信息 -->
              <div class="param-section"
                   v-if="getProcessStructures(parseInt(activeProcessId)).length > 0">
                <div class="section-header">
                  <h4 class="section-title">BOM信息</h4>
                </div>
                <div class="param-form">
                  <el-form :label-position="'top'">
                    <div class="form-grid">
                      <el-form-item v-for="item in getProcessStructures(parseInt(activeProcessId))"
                                    :key="item.id"
                                    :label="`${item.productName} ${item.model}`"
                                    class="form-item">
                        <div class="consumable-input-group">
                          <el-input-number v-model="getProcessInfo(parseInt(activeProcessId)).consumables[item.id]"
                                           :min="0"
                                           :model-value="getConsumableValue(parseInt(activeProcessId), item.id)"
                                           @change="val => getProcessInfo(parseInt(activeProcessId)).consumables[item.id] = val"
                                           class="consumable-input" />
                          <span class="consumable-unit">{{ item.unit }}</span>
                        </div>
                      </el-form-item>
                    </div>
                  </el-form>
                </div>
              </div>
              <!-- å‚数组列表 -->
              <div class="param-section">
                <div class="section-header">
                  <h4 class="section-title">参数组管理</h4>
                  <div class="section-actions">
                    <el-switch v-model="useTableView"
                               active-text="表格视图"
                               inactive-text="卡片视图"
                               inline-prompt />
                    <el-button type="primary"
                               @click="addParamGroup(parseInt(activeProcessId))"
                               :icon="Plus">
                      æ–°å¢žå‚数组
                    </el-button>
                  </div>
                </div>
                <!-- å¡ç‰‡è§†å›¾ -->
                <div v-if="!useTableView"
                     class="param-cards">
                  <div v-for="(group, index) in form.paramGroups[activeProcessId] || []"
                       :key="index"
                       class="param-card">
                    <div class="card-header">
                      <span class="card-title">参数组 {{ index + 1 }}</span>
                      <el-button type="danger"
                                 size="small"
                                 @click="removeParamGroup(parseInt(activeProcessId), index)"
                                 v-if="(form.paramGroups[activeProcessId] || []).length > 1"
                                 circle>
                        <el-icon>
                          <Delete />
                        </el-icon>
                      </el-button>
                    </div>
                    <div class="card-body">
                      <div class="param-grid">
                        <el-form-item v-for="param in params"
                                      :key="param.id"
                                      :label="param.paramName"
                                      :label-width="120"
                                      :prop="`paramGroups.${activeProcessId}.${index}.${param.id}`"
                                      class="param-item">
                          <template v-if="param.paramType == '1'">
                            <!-- æ•°å­—类型 -->
                            <div class="param-input-group">
                              <el-input-number v-model="form.paramGroups[activeProcessId][index][param.id]"
                                               controls-position="right"
                                               :precision="getPrecision(param.paramFormat)"
                                               class="param-input" />
                              <span v-if="param.unit && param.unit != '/'"
                                    class="param-unit">
                                {{ param.unit }}
                              </span>
                            </div>
                          </template>
                          <template v-else-if="param.paramType == '2'">
                            <!-- æ–‡æœ¬ç±»åž‹ -->
                            <div class="param-input-group">
                              <el-input v-model="form.paramGroups[activeProcessId][index][param.id]"
                                        class="param-input" />
                              <span v-if="param.unit && param.unit != '/'"
                                    class="param-unit">
                                {{ param.unit }}
                              </span>
                            </div>
                          </template>
                          <template v-else-if="param.paramType == '3'">
                            <!-- å­—典类型 -->
                            <div class="param-input-group">
                              <el-select v-model="form.paramGroups[activeProcessId][index][param.id]"
                                         placeholder="请选择"
                                         class="param-select"
                                         style="width: 100%">
                                <el-option v-for="option in dictOptions[param.paramFormat] || []"
                                           :key="option.dictValue"
                                           :label="option.dictLabel"
                                           :value="option.dictValue" />
                              </el-select>
                              <span v-if="param.unit && param.unit != '/'"
                                    class="param-unit">
                                {{ param.unit }}
                              </span>
                            </div>
                          </template>
                          <template v-else-if="param.paramType == '4'">
                            <!-- æ—¥æœŸç±»åž‹ -->
                            <div class="param-input-group">
                              <el-date-picker :value-format="param.paramFormat"
                                              :format="param.paramFormat"
                                              :type="param.paramFormat=='YYYY-MM-DD'?'date':'datetime'"
                                              v-model="form.paramGroups[activeProcessId][index][param.id]"
                                              class="param-input" />
                              <span v-if="param.unit && param.unit != '/'"
                                    class="param-unit">
                                {{ param.unit }}
                              </span>
                            </div>
                          </template>
                          <template v-else>
                            <!-- å…¶ä»–类型 -->
                            <div class="param-input-group">
                              <el-input v-model="form.paramGroups[activeProcessId][index][param.id]"
                                        class="param-input" />
                              <span v-if="param.unit && param.unit != '/'"
                                    class="param-unit">
                                {{ param.unit }}
                              </span>
                            </div>
                          </template>
                        </el-form-item>
                      </div>
                    </div>
                  </div>
                </div>
                <!-- è¡¨æ ¼è§†å›¾ -->
                <div v-else
                     class="param-table">
                  <el-table :data="form.paramGroups[activeProcessId] || []"
                            style="width: 100%"
                            class="table-view">
                    <!-- æ“ä½œåˆ— -->
                    <el-table-column label="操作"
                                     width="100"
                                     fixed>
                      <template #default="{ $index }">
                        <el-button type="danger"
                                   size="small"
                                   @click="removeParamGroup(parseInt(activeProcessId), $index)"
                                   circle>
                          <el-icon>
                            <Delete />
                          </el-icon>
                        </el-button>
                      </template>
                    </el-table-column>
                    <!-- å‚数列 -->
                    <el-table-column v-for="param in params"
                                     :key="param.id"
                                     :min-width="200">
                      <template #header>
                        <span>{{ param.paramName }}</span>
                      </template>
                      <template #default="{ row }">
                        <template v-if="param.paramType == '1'">
                          <!-- æ•°å­—类型 -->
                          <el-input-number v-model="row[param.id]"
                                           controls-position="right"
                                           :precision="getPrecision(param.paramFormat)"
                                           class="table-input" />
                        </template>
                        <template v-else-if="param.paramType == '2'">
                          <!-- æ–‡æœ¬ç±»åž‹ -->
                          <el-input v-model="row[param.id]"
                                    class="table-input" />
                        </template>
                        <template v-else-if="param.paramType == '3'">
                          <!-- å­—典类型 -->
                          <el-select v-model="row[param.id]"
                                     placeholder="请选择"
                                     class="table-select">
                            <el-option v-for="option in dictOptions[param.paramFormat] || []"
                                       :key="option.dictValue"
                                       :label="option.dictLabel"
                                       :value="option.dictValue" />
                          </el-select>
                        </template>
                        <template v-else-if="param.paramType == '4'">
                          <!-- æ—¥æœŸç±»åž‹ -->
                          <el-date-picker :value-format="param.paramFormat"
                                          :format="param.paramFormat"
                                          width="100%"
                                          :type="param.paramFormat=='YYYY-MM-DD'?'date':'datetime'"
                                          v-model="row[param.id]"
                                          class="table-input table-select" />
                        </template>
                        <template v-else>
                          <!-- å…¶ä»–类型 -->
                          <el-input v-model="row[param.id]"
                                    class="table-input" />
                        </template>
                      </template>
                    </el-table-column>
                  </el-table>
                </div>
                <!-- æ–°å¢žå‚数组按钮 -->
                <!-- <div class="param-actions">
                  <el-button type="primary"
                             @click="addParamGroup(parseInt(activeProcessId))"
                             :icon="Plus">
                    æ–°å¢žå‚数组
                  </el-button>
                </div> -->
              </div>
            </div>
            <div v-else
                 class="empty-process">
              <el-empty description="请选择一个工序"
                        :image-size="120" />
            </div>
          </div>
        </div>
      </div>
      <!-- ç¬¬å››æ­¥ï¼šå¡«å†™äº§é‡ä¿¡æ¯ -->
      <div v-else-if="activeStep === 3"
           class="step-panel">
        <div class="panel-header">
          <div>
            <h3 class="panel-title">填写产量信息</h3>
            <p class="panel-subtitle">请填写本次报工的产量数据</p>
          </div>
          <div class="header-actions">
            <el-button @click="activeStep--"
                       v-if="activeStep > 0"
                       :disabled="isSubmitting">
              <el-icon>
                <ArrowLeft />
              </el-icon> ä¸Šä¸€æ­¥
            </el-button>
            <el-button type="primary"
                       @click="handleSubmit"
                       v-if="activeStep === 3"
                       :loading="isSubmitting">
              <el-icon v-if="!isSubmitting">
                <Check />
              </el-icon>
              <el-icon v-else>
                <Loading />
              </el-icon>
              {{ isSubmitting ? '提交中...' : '确认提交' }}
            </el-button>
          </div>
        </div>
        <el-form :model="form"
                 :rules="rules"
                 ref="formRef"
                 :label-position="'top'"
                 class="form-container">
          <div class="form-grid1">
            <el-form-item label="产出方量"
                          prop="outputVolume"
                          required
                          class="form-item">
              <div class="volume-input-group">
                <el-input-number v-model="form.outputVolume"
                                 :min="0"
                                 :precision="2"
                                 @change="handleVolumeChange"
                                 class="volume-input" />
                <span class="volume-unit">方</span>
              </div>
            </el-form-item>
            <el-form-item label="不合格方量"
                          prop="unqualifiedVolume"
                          required
                          class="form-item">
              <div class="volume-input-group">
                <el-input-number v-model="form.unqualifiedVolume"
                                 :min="0"
                                 :precision="2"
                                 @change="handleVolumeChange"
                                 class="volume-input" />
                <span class="volume-unit">方</span>
              </div>
            </el-form-item>
            <el-form-item label="完成方量"
                          prop="completedVolume"
                          required
                          class="form-item">
              <div class="volume-input-group">
                <el-input-number v-model="form.completedVolume"
                                 :min="0"
                                 :precision="2"
                                 class="volume-input" />
                <span class="volume-unit">方</span>
              </div>
            </el-form-item>
          </div>
        </el-form>
      </div>
    </div>
    <!-- åº•部按钮 -->
  </div>
</template>
<script setup>
  import { ref, reactive, computed, watch, onMounted } from "vue";
  import { ElMessage, ElEmpty } from "element-plus";
  import { useRouter, useRoute } from "vue-router";
  import { getDicts } from "@/api/system/dict/data";
  import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
  import {
    productionRecordAdd,
    productionRecordAddSubmit,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { userListNoPage } from "@/api/system/user.js";
  import { getInfo } from "@/api/login.js";
  import request from "@/utils/request";
  import { getToken } from "@/utils/auth";
  import dayjs from "dayjs";
  const router = useRouter();
  // ä»Žè·¯ç”±å‚数获取数据
  const route = useRoute();
  const data = route.query.data ? JSON.parse(route.query.data) : {};
  const dialogTitle = computed(() => (data.id ? "编辑报工" : "新增报工"));
  const formRef = ref(null);
  const isSubmitting = ref(false);
  const orderLoading = ref(false);
  const processLoading = ref(false);
  const activeStep = ref(0);
  const steps = [
    { title: "选择生产订单", description: "选择需要报工的生产订单" },
    { title: "填写基础信息", description: "填写报工的基本信息" },
    { title: "查看工序参数", description: "填写各工序的参数信息" },
    { title: "填写产量信息", description: "填写本次报工的产量数据" },
  ];
  // è®¡ç®—当前工序的参数组数量
  const paramGroupCount = computed(() => {
    if (!activeProcessId.value) return 0;
    return (form.paramGroups[activeProcessId.value] || []).length;
  });
  const orderId = ref(data.orderId || "");
  const processId = ref(data.processId || "");
  const activeProcessId = ref("");
  const orderList = ref([]);
  const processList = ref([]);
  const params = ref([]);
  const dictOptions = ref({});
  const userList = ref([]);
  const userLoading = ref(false);
  const useTableView = ref(false); // æŽ§åˆ¶æ˜¯å¦ä½¿ç”¨è¡¨æ ¼è§†å›¾
  const form = reactive({
    id: data.id || undefined,
    orderId: data.orderId || "",
    npsNo: data.npsNo || "",
    teamName: data.teamName || "白班",
    materialCode: data.materialCode || "",
    productName: data.productName || "",
    specification: data.specification || "",
    outputVolume: data.outputVolume || 0,
    unqualifiedVolume: data.unqualifiedVolume || 0,
    completedVolume: data.completedVolume || 0,
    createBy: data.createBy || "当前登录人",
    createTime: data.createTime || new Date(),
    paramGroups: data.paramGroups || {}, // å­˜å‚¨æ¯ä¸ªå·¥åºçš„参数组
    processInfo: data.processInfo || {}, // å­˜å‚¨æ¯ä¸ªå·¥åºçš„基本信息
  });
  const rules = {
    teamName: [{ required: true, message: "请选择班组", trigger: "blur" }],
    outputVolume: [
      { required: true, message: "请输入产出方量", trigger: "blur" },
    ],
    unqualifiedVolume: [
      { required: true, message: "请输入不合格方量", trigger: "blur" },
    ],
    completedVolume: [
      { required: true, message: "请输入完成方量", trigger: "blur" },
    ],
    createBy: [{ required: true, message: "请输入创建人", trigger: "blur" }],
  };
  // åŠ è½½ç”Ÿäº§è®¢å•åˆ—è¡¨
  const loadOrders = () => {
    orderLoading.value = true;
    productOrderListPage({ pageNum: 1, pageSize: 100 })
      .then(res => {
        orderList.value = res.data.records || [];
      })
      .finally(() => {
        orderLoading.value = false;
      });
  };
  const handleVolumeChange = () => {
    form.completedVolume = form.outputVolume - form.unqualifiedVolume;
  };
  // å¤„理生产订单选择
  const handleOrderChange = val => {
    if (val) {
      const order = orderList.value.find(item => item.id === val);
      if (order) {
        form.orderId = val;
        form.npsNo = order.npsNo;
        form.materialCode = order.materialCode;
        form.productName = order.productName;
        form.specification = order.model;
      }
      // åŠ è½½å·¥åºåˆ—è¡¨
      loadProcesses(val);
    } else {
      form.orderId = "";
      form.npsNo = "";
      form.materialCode = "";
      form.productName = "";
      form.specification = "";
      processId.value = "";
      activeProcessId.value = "";
      processList.value = [];
      params.value = [];
      form.params = {};
    }
  };
  // åŠ è½½å·¥åºåˆ—è¡¨
  const loadProcesses = orderId => {
    processLoading.value = true;
    // è°ƒç”¨æ–°çš„æŽ¥å£
    productionRecordAdd(orderId)
      .then(res => {
        if (res.code === 200) {
          const data = res.data;
          // æå–工序列表
          processList.value = data.productionOrderRouteItemVos || [];
          // å­˜å‚¨å·¥åºç»“构数据
          form.processStructures = {};
          processList.value.forEach(process => {
            form.processStructures[process.processId] =
              process.orderStructureVos || [];
            loadParams(process.processId, orderId);
          });
          // å¦‚果有工序,默认选择第一个
          if (processList.value.length > 0) {
            const firstProcess = processList.value[0];
            activeProcessId.value = firstProcess.processId + "";
            processId.value = firstProcess.processId;
            form.processId = firstProcess.processId;
            // åŠ è½½ç¬¬ä¸€ä¸ªå·¥åºçš„å‚æ•°
            // loadParams(firstProcess.processId, orderId);
          }
        }
      })
      .finally(() => {
        processLoading.value = false;
      });
  };
  // å¤„理工序导航点击
  const handleProcessClick = selectedProcessId => {
    activeProcessId.value = selectedProcessId + "";
    processId.value = selectedProcessId;
    form.processId = selectedProcessId;
    // åŠ è½½å‚æ•°åˆ—è¡¨
    loadParams(selectedProcessId, form.orderId);
  };
  // èŽ·å–å·¥åºåŸºæœ¬ä¿¡æ¯ï¼Œä¸å­˜åœ¨åˆ™åˆå§‹åŒ–
  const getProcessInfo = processId => {
    if (!form.processInfo) {
      form.processInfo = {};
    }
    if (!form.processInfo[processId]) {
      form.processInfo[processId] = {
        postName: "",
        equipmentMalfunction: "",
        equipmentDisposal: "",
        processExplained: "",
        files: [],
        consumables: {},
      };
    }
    return form.processInfo[processId];
  };
  // èŽ·å–å·¥åºç»“æž„æ•°æ®ï¼ˆBOM列表)
  const getProcessStructures = processId => {
    return form.processStructures && form.processStructures[processId]
      ? form.processStructures[processId]
      : [];
  };
  // èŽ·å–æ¶ˆè€—å“æ•°é‡ï¼Œé»˜è®¤ä¸º0
  const getConsumableValue = (processId, itemId) => {
    const processInfo = getProcessInfo(processId);
    if (!processInfo.consumables[itemId]) {
      processInfo.consumables[itemId] = 0;
    }
    return processInfo.consumables[itemId];
  };
  // å¤„理文件预览
  const handlePreview = file => {
    // æ£€æŸ¥æ˜¯å¦æ˜¯å›¾ç‰‡æ–‡ä»¶
    if (file.raw && file.raw.type.startsWith("image/")) {
      // åˆ›å»ºå›¾ç‰‡é¢„览
      const imageUrl = URL.createObjectURL(file.raw);
      const image = new Image();
      image.src = imageUrl;
      // åˆ›å»ºé¢„览容器
      const previewContainer = document.createElement("div");
      previewContainer.style.position = "fixed";
      previewContainer.style.top = "0";
      previewContainer.style.left = "0";
      previewContainer.style.width = "100%";
      previewContainer.style.height = "100%";
      previewContainer.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
      previewContainer.style.display = "flex";
      previewContainer.style.alignItems = "center";
      previewContainer.style.justifyContent = "center";
      previewContainer.style.zIndex = "9999";
      previewContainer.style.cursor = "pointer";
      // æ·»åŠ å›¾ç‰‡
      previewContainer.appendChild(image);
      image.style.maxWidth = "90%";
      image.style.maxHeight = "90%";
      // æ·»åŠ å…³é—­åŠŸèƒ½
      previewContainer.addEventListener("click", () => {
        URL.revokeObjectURL(imageUrl);
        document.body.removeChild(previewContainer);
      });
      // æ·»åŠ åˆ°æ–‡æ¡£
      document.body.appendChild(previewContainer);
    }
  };
  // å¤„理文件删除
  const handleRemove = (file, fileList) => {
    const processId = parseInt(activeProcessId.value);
    if (processId) {
      const processInfo = getProcessInfo(processId);
      processInfo.files = fileList;
    }
  };
  // å¤„理文件变更
  const handleFileChange = async (file, fileList) => {
    console.log(file, fileList);
    const formData = new FormData();
    formData.append("file", file.raw);
    const uploadRes = await request({
      url: "/file/upload",
      method: "post",
      data: formData,
      headers: {
        "Content-Type": "multipart/form-data",
        Authorization: `Bearer ${getToken()}`,
      },
    });
    console.log(uploadRes);
    if (uploadRes.code === 200) {
      const tempId = uploadRes.data.tempId;
      // å°†tempId存储到file对象中
      file.tempId = tempId;
    }
    const processId = parseInt(activeProcessId.value);
    if (processId) {
      const processInfo = getProcessInfo(processId);
      processInfo.files = fileList;
    }
  };
  // èŽ·å–å­—å…¸æ•°æ®
  const getDictOptions = async dictType => {
    if (!dictType) return [];
    if (dictOptions.value[dictType]) return dictOptions.value[dictType];
    try {
      const res = await getDicts(dictType);
      if (res.code === 200) {
        dictOptions.value[dictType] = res.data;
        return res.data;
      }
      return [];
    } catch (error) {
      console.error("获取字典数据失败:", error);
      return [];
    }
  };
  // åŠ è½½å‚æ•°åˆ—è¡¨
  const loadParams = (processId, orderId) => {
    // ä»Žå·²åŠ è½½çš„å·¥åºæ•°æ®ä¸­èŽ·å–å‚æ•°åˆ—è¡¨
    const process = processList.value.find(
      p => p.processId === parseInt(processId)
    );
    if (process) {
      params.value = process.orderRouteItemParaVos || [];
      // åˆå§‹åŒ–参数组
      if (!form.paramGroups[processId]) {
        form.paramGroups[processId] = [];
      }
      // å¦‚果没有参数组,添加一个默认参数组
      if (form.paramGroups[processId].length === 0) {
        const defaultGroup = {};
        for (const param of params.value) {
          defaultGroup[param.id] = param.standardValue || "";
          // å¦‚果是字典类型参数,获取字典数据
          if (param.paramType == "3" && param.paramFormat) {
            getDictOptions(param.paramFormat);
          }
        }
        form.paramGroups[processId].push(defaultGroup);
      }
    }
  };
  // èŽ·å–å°æ•°ç²¾åº¦
  const getPrecision = format => {
    if (!format) return 2;
    const match = format.match(/\.(\d+)/);
    return match ? parseInt(match[1].length) : 2;
  };
  // å¤„理下一步
  const handleNextStep = () => {
    if (activeStep.value === 0) {
      // ç¬¬ä¸€æ­¥ï¼šéªŒè¯ç”Ÿäº§è®¢å•选择
      if (!orderId.value) {
        ElMessage.error("请选择生产订单");
        return;
      }
      activeStep.value = 1;
    } else if (activeStep.value === 1) {
      // ç¬¬äºŒæ­¥ï¼šéªŒè¯åŸºç¡€ä¿¡æ¯
      formRef.value.validate(valid => {
        if (valid) {
          activeStep.value = 2;
        }
      });
    } else if (activeStep.value === 2) {
      activeStep.value = 3;
    }
  };
  // å¤„理提交
  const handleSubmit = () => {
    formRef.value.validate(valid => {
      if (valid) {
        isSubmitting.value = true;
        // æž„建请求参数
        const order = orderList.value.find(item => item.id === form.orderId);
        console.log(order, "order");
        const submitParams = {
          productOrderId: form.orderId,
          productId: order ? order.productId : null,
          postName: form.createBy,
          schedule: form.teamName,
          // reportingTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"),
          qualifiedQuantity: form.outputVolume - form.unqualifiedVolume,
          unqualifiedQuantity: form.unqualifiedVolume,
          quantity: form.outputVolume,
          productionProductRouteItemDtoList: processList.value.map(process => {
            const processInfo = getProcessInfo(process.processId);
            const paramGroups = form.paramGroups[process.processId] || [];
            const productionProductRouteItemParamDtoList = [];
            // æ·»åŠ å‚æ•°ç»„
            paramGroups.forEach((group, index) => {
              Object.entries(group).forEach(([paramId, value]) => {
                // ä»Žå½“前工序的参数列表中查找参数
                const processData = processList.value.find(
                  p => p.processId === process.processId
                );
                const param = processData
                  ? processData.orderRouteItemParaVos.find(
                      p => p.id === parseInt(paramId)
                    )
                  : null;
                if (param) {
                  productionProductRouteItemParamDtoList.push({
                    id: parseInt(paramId),
                    standardValue: param.standardValue,
                    minValue: param.minValue,
                    maxValue: param.maxValue,
                    productId: param.productId,
                    productValue: value,
                    sourceSort: index + 1,
                    unit: param.unit,
                    isRequired: param.isRequired,
                  });
                }
              });
            });
            // æ·»åŠ BOM信息
            const structures = getProcessStructures(process.processId);
            console.log(structures, "structures");
            structures.forEach(structure => {
              const consumableValue = getConsumableValue(
                process.processId,
                structure.id
              );
              if (consumableValue > 0) {
                productionProductRouteItemParamDtoList.push({
                  productId: structure.productModelId,
                  bomId: structure.bomId,
                  id: null,
                  productValue: consumableValue,
                  unit: structure.unit,
                });
              }
            });
            return {
              postName: processInfo.postName,
              equipmentMalfunction: processInfo.equipmentMalfunction,
              equipmentDisposal: processInfo.equipmentDisposal,
              processExplained: processInfo.processExplained,
              processId: process.processId,
              productionProductRouteItemParamDtoList,
              files: processInfo.files.map(file => file.tempId || file.uid),
            };
          }),
        };
        // è°ƒç”¨API进行提交
        productionRecordAddSubmit(submitParams)
          .then(res => {
            if (res.code === 200) {
              ElMessage.success(data.id ? "修改成功" : "新增成功");
              router.back();
            } else {
              ElMessage.error(res.msg || "提交失败");
            }
          })
          .catch(error => {
            ElMessage.error("提交失败,请稍后重试");
            console.error("提交错误:", error);
          })
          .finally(() => {
            isSubmitting.value = false;
          });
      }
    });
  };
  // å¤„理取消
  const handleCancel = () => {
    router.back();
  };
  // æ–°å¢žå‚数组
  const addParamGroup = processId => {
    if (!form.paramGroups[processId]) {
      form.paramGroups[processId] = [];
    }
    // åˆ›å»ºä¸€ä¸ªæ–°çš„参数组,使用默认值
    const newGroup = {};
    // ä»Žå½“前工序的参数列表中获取参数信息
    const process = processList.value.find(
      p => p.processId === parseInt(processId)
    );
    if (process) {
      const processParams = process.orderRouteItemParaVos || [];
      processParams.forEach(param => {
        newGroup[param.id] = param.standardValue || "";
      });
    }
    form.paramGroups[processId].push(newGroup);
  };
  // åˆ é™¤å‚数组
  const removeParamGroup = (processId, index) => {
    if (form.paramGroups[processId] && form.paramGroups[processId].length > 1) {
      form.paramGroups[processId].splice(index, 1);
    }
  };
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  const loadUsers = () => {
    userLoading.value = true;
    userListNoPage()
      .then(res => {
        userList.value = res.data || [];
      })
      .finally(() => {
        userLoading.value = false;
      });
  };
  // èŽ·å–å½“å‰ç™»å½•äººä¿¡æ¯
  const getCurrentUser = async () => {
    try {
      const res = await getInfo();
      if (res && res.user) {
        form.createBy = res.user.nickName || res.user.userName;
      }
    } catch (error) {
      console.error("获取当前登录人信息失败:", error);
    }
  };
  // åˆå§‹åŒ–
  const init = () => {
    // æ— è®ºæ–°å¢žè¿˜æ˜¯ç¼–辑,都加载订单列表和用户列表
    loadOrders();
    loadUsers();
    getCurrentUser();
    if (data.id) {
      // ç¼–辑时设置表单数据
      Object.assign(form, data);
      // è®¾ç½®orderId
      orderId.value = data.orderId || "";
      // å¦‚果有订单ID,加载工序和参数
      if (data.orderId) {
        // æ¨¡æ‹Ÿé€‰æ‹©è®¢å•的操作,触发数据加载
        setTimeout(() => {
          handleOrderChange(data.orderId);
        }, 100);
      }
    } else {
      // æ–°å¢žæ—¶è®¾ç½®é»˜è®¤å€¼
      form.createTime = new Date();
    }
    // å§‹ç»ˆä»Žç¬¬ä¸€æ­¥å¼€å§‹
    activeStep.value = 0;
  };
  // é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–
  onMounted(() => {
    init();
  });
</script>
<style scoped>
  /* é¡µé¢å®¹å™¨ */
  .reporting-page {
    min-height: 100vh;
    padding: 24px;
    background-color: #f0f2f5;
  }
  /* é¡µé¢å¤´éƒ¨ */
  .page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 32px;
    padding-bottom: 16px;
    border-bottom: 1px solid #e8e8e8;
  }
  .page-title {
    margin: 0;
    font-size: 24px;
    font-weight: 600;
    color: #1f2329;
  }
  .header-actions {
    display: flex;
    gap: 12px;
  }
  /* æ­¥éª¤æŒ‡ç¤ºå™¨ */
  .step-indicator {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 32px;
    padding: 0 16px;
    padding-top: 16px;
  }
  .step-item {
    display: flex;
    align-items: center;
    flex: 1;
    position: relative;
  }
  .step-circle {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background-color: #f0f0f0;
    color: #999;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    font-weight: 600;
    z-index: 2;
    transition: all 0.3s ease;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  .step-item.active .step-circle {
    background-color: #1890ff;
    color: white;
    box-shadow: 0 0 0 8px rgba(24, 144, 255, 0.1);
  }
  .step-item.completed .step-circle {
    background-color: #52c41a;
    color: white;
    box-shadow: 0 0 0 8px rgba(82, 196, 26, 0.1);
  }
  .step-check {
    font-size: 20px;
  }
  .step-content {
    margin-left: 16px;
    flex: 1;
  }
  .step-title {
    font-size: 14px;
    font-weight: 600;
    color: #666;
    margin-bottom: 4px;
    transition: color 0.3s ease;
  }
  .step-item.active .step-title {
    color: #1890ff;
  }
  .step-item.completed .step-title {
    color: #52c41a;
  }
  .step-description {
    font-size: 12px;
    color: #999;
  }
  .step-line {
    position: absolute;
    top: 20px;
    left: 50%;
    right: -50%;
    height: 2px;
    background-color: #e8e8e8;
    z-index: 1;
    transition: all 0.3s ease;
  }
  .step-item.completed .step-line {
    background-color: #52c41a;
  }
  /* é¡µé¢å†…容 */
  .page-content {
    background-color: white;
    border-radius: 12px;
    padding: 0;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    margin-bottom: 32px;
    overflow: hidden;
  }
  /* æ­¥éª¤é¢æ¿ */
  .step-panel {
    padding: 32px;
  }
  .panel-header {
    margin-bottom: 24px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
  }
  .panel-title {
    font-size: 18px;
    font-weight: 600;
    color: #1f2329;
    margin: 0 0 8px 0;
  }
  .panel-subtitle {
    font-size: 14px;
    color: #666;
    margin: 0;
  }
  .header-actions {
    display: flex;
    gap: 12px;
    align-items: center;
  }
  /* è¡¨å•容器 */
  .form-container {
    width: 100%;
  }
  /* ä¸Šä¼ ç»„件样式 */
  .upload-block {
    display: block;
  }
  .upload-block .el-upload__tip {
    margin-top: 8px;
  }
  /* BOM输入组样式 */
  .consumable-input-group {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .consumable-input {
    flex: 1;
  }
  .consumable-unit {
    font-size: 14px;
    color: #666;
    white-space: nowrap;
  }
  .form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
  }
  .form-grid1 {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
    gap: 20px;
  }
  .form-item {
    margin-bottom: 0;
  }
  .form-select,
  .form-input {
    width: 100%;
  }
  .form-textarea {
    width: 100%;
    resize: vertical;
  }
  /* äº§é‡è¾“入组 */
  .volume-input-group {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .volume-input {
    flex: 1;
    width: 100%;
  }
  .volume-unit {
    font-size: 14px;
    color: #666;
    white-space: nowrap;
  }
  /* å·¥åºå®¹å™¨ */
  .process-container {
    display: flex;
    gap: 24px;
    min-height: 500px;
  }
  /* å·¥åºå¯¼èˆª */
  .process-nav {
    width: 200px;
    background-color: #fafafa;
    border-radius: 8px;
    padding: 16px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    overflow-y: auto;
  }
  .process-nav-item {
    padding: 12px 16px;
    margin-bottom: 8px;
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-size: 14px;
    font-weight: 500;
    color: #666;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .process-nav-item:hover {
    background-color: #e6f7ff;
    color: #1890ff;
  }
  .process-nav-item.active {
    background-color: #1890ff;
    color: white;
    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
  }
  .process-badge {
    font-size: 12px;
    background-color: rgba(255, 255, 255, 0.2);
    padding: 2px 8px;
    border-radius: 10px;
  }
  /* å·¥åºå†…容 */
  .process-content {
    flex: 1;
    overflow-y: auto;
  }
  .empty-process {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 400px;
  }
  /* å‚数部分 */
  .param-section {
    margin-bottom: 32px;
    padding: 24px;
    background-color: #fafafa;
    border-radius: 8px;
  }
  .section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
  }
  .section-title {
    font-size: 16px;
    font-weight: 600;
    color: #1f2329;
    margin: 0;
  }
  .section-actions {
    display: flex;
    gap: 8px;
  }
  /* å‚数表单 */
  .param-form {
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  }
  /* å‚数卡片 */
  .param-cards {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }
  .param-card {
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    overflow: hidden;
    transition: all 0.3s ease;
  }
  .param-card:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    transform: translateY(-2px);
  }
  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 16px 20px;
    background-color: #fafafa;
    border-bottom: 1px solid #e8e8e8;
  }
  .card-title {
    font-size: 14px;
    font-weight: 600;
    color: #1f2329;
  }
  .card-body {
    padding: 20px;
  }
  /* å‚数网格 */
  .param-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
    gap: 16px;
  }
  .param-item {
    margin-bottom: 0;
  }
  .param-input-group {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
  }
  .param-input,
  .param-select {
    flex: 1;
  }
  .param-unit {
    font-size: 14px;
    color: #666;
    white-space: nowrap;
  }
  /* è¡¨æ ¼è§†å›¾ */
  .param-table {
    margin: 16px 0;
  }
  .table-view {
    border-radius: 8px;
    overflow: hidden;
  }
  .table-view th {
    background-color: #fafafa;
    font-weight: 600;
  }
  .table-input,
  .table-select {
    width: 100%;
  }
  /* å‚数操作 */
  .param-actions {
    margin-top: 16px;
    display: flex;
    justify-content: flex-end;
  }
  /* é¡µé¢åº•部 */
  .page-footer {
    display: flex;
    justify-content: center;
    padding: 24px;
    background-color: white;
    border-top: 1px solid #e8e8e8;
    border-radius: 0 0 12px 12px;
  }
  .footer-actions {
    display: flex;
    gap: 12px;
  }
  /* å“åº”式设计 */
  @media (max-width: 1024px) {
    .form-grid {
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    }
    .param-grid {
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    }
  }
  @media (max-width: 768px) {
    .reporting-page {
      padding: 16px;
    }
    .step-indicator {
      flex-direction: column;
      align-items: flex-start;
      gap: 16px;
      padding: 0;
    }
    .step-item {
      flex-direction: row;
      width: 100%;
    }
    .step-line {
      display: none;
    }
    .step-panel {
      padding: 20px;
    }
    .form-grid {
      grid-template-columns: 1fr;
    }
    .process-container {
      flex-direction: column;
    }
    .process-nav {
      width: 100%;
      margin-bottom: 16px;
    }
    .param-grid {
      grid-template-columns: 1fr;
    }
    .footer-actions {
      flex-direction: column;
      width: 100%;
    }
    .footer-actions button {
      width: 100%;
    }
  }
  /* æ»šåŠ¨æ¡æ ·å¼ */
  .process-nav::-webkit-scrollbar,
  .process-content::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  .process-nav::-webkit-scrollbar-track,
  .process-content::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
  }
  .process-nav::-webkit-scrollbar-thumb,
  .process-content::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
  }
  .process-nav::-webkit-scrollbar-thumb:hover,
  .process-content::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  /* åŠ¨ç”»æ•ˆæžœ */
  @keyframes fadeIn {
    from {
      opacity: 0;
      transform: translateY(10px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
  .step-panel {
    animation: fadeIn 0.3s ease;
  }
  /* åŠ è½½çŠ¶æ€ */
  .loading-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(255, 255, 255, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 9999;
  }
</style>
src/views/productionPlan/productionPlan/index.vue
@@ -3,36 +3,9 @@
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="客户名称:">
          <el-input v-model="searchForm.customerName"
                    placeholder="请输入"
                    clearable
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <!-- ç®€åŒ–版搜索条件 -->
        <el-form-item label="产品名称:">
          <el-input v-model="searchForm.productName"
                    placeholder="请输入"
                    clearable
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="产品规格:">
          <el-input v-model="searchForm.model"
                    placeholder="请输入"
                    clearable
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="物料编码:">
          <el-input v-model="searchForm.materialCode"
                    placeholder="请输入"
                    clearable
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="申请单编号:">
          <el-input v-model="searchForm.applyNo"
                    placeholder="请输入"
                    clearable
                    style="width: 160px;"
@@ -50,7 +23,7 @@
        </el-form-item>
        <el-form-item label="下发状态:">
          <el-select v-model="searchForm.status"
                     placeholder="请选择状态"
                     placeholder="请选择状态"
                     clearable
                     filterable
                     style="width: 100px">
@@ -62,6 +35,37 @@
                       value="2" />
          </el-select>
        </el-form-item>
        <!-- å±•开版搜索条件 -->
        <template v-if="searchFormExpanded">
          <el-form-item label="客户名称:">
            <el-input v-model="searchForm.customerName"
                      placeholder="请输入"
                      clearable
                      style="width: 160px;"
                      @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item label="产品规格:">
            <el-input v-model="searchForm.model"
                      placeholder="请输入"
                      clearable
                      style="width: 160px;"
                      @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item label="物料编码:">
            <el-input v-model="searchForm.materialCode"
                      placeholder="请输入"
                      clearable
                      style="width: 160px;"
                      @keyup.enter="handleQuery" />
          </el-form-item>
          <el-form-item label="申请单编号:">
            <el-input v-model="searchForm.applyNo"
                      placeholder="请输入"
                      clearable
                      style="width: 160px;"
                      @keyup.enter="handleQuery" />
          </el-form-item>
        </template>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
@@ -82,6 +86,16 @@
      <div>
      </div>
    </div>
    <div class="search-header">
      <el-button type="text"
                 @click="toggleSearchForm">
        <el-icon>
          <ArrowUp v-if="searchFormExpanded" />
          <ArrowDown v-else />
        </el-icon>
        {{ searchFormExpanded ? '收起搜索条件' : '展开搜索条件' }}
      </el-button>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
@@ -93,10 +107,17 @@
                :selectable="isSelectable"
                @selection-change="handleSelectionChange"
                @pagination="pagination">
        <template #quantity="{ row }">
          {{ row.quantity || '-' }}<span style="color:rgb(63, 95, 211)"> å—</span>
        </template>
        <template #volume="{ row }">
          {{ row.volume || '-' }}<span style="color:rgba(12, 46, 40, 0.76)"> æ–¹</span>
        </template>
      </PIMTable>
    </div>
    <!-- åˆå¹¶ä¸‹å‘弹窗 -->
    <el-dialog v-model="isShowNewModal"
               destroy-on-close
               title="合并下发"
               width="600px">
      <el-form :model="mergeForm"
@@ -168,6 +189,7 @@
    </el-dialog>
    <!-- è¿½è¸ªè¿›åº¦å¼¹çª— -->
    <el-dialog v-model="showTrackProgressDialog"
               destroy-on-close
               :title="`追踪进度 - ${trackProgressForm.materialCode || ''}`"
               width="600px">
      <el-form :model="trackProgressForm"
@@ -246,6 +268,7 @@
                  @close="handleImportClose" />
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog v-model="dialogVisible"
               destroy-on-close
               :title="operationType === 'add' ? '新增生产计划' : '编辑生产计划'"
               width="600px">
      <el-form ref="formRef"
@@ -366,6 +389,7 @@
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { ElMessage } from "element-plus";
  import { ArrowUp, ArrowDown } from "@element-plus/icons-vue";
  import dayjs from "dayjs";
  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
  import { getToken } from "@/utils/auth";
@@ -441,14 +465,18 @@
    {
      label: "块数",
      prop: "quantity",
      formatData: cell => (cell ? `${cell}块` : ""),
      align: "right",
      dataType: "slot",
      slot: "quantity",
    },
    {
      label: "方数",
      prop: "volume",
      width: "150px",
      align: "right",
      dataType: "slot",
      slot: "volume",
      className: "volume-cell",
      formatData: cell => (cell ? `${cell}方` : ""),
    },
    {
      label: "下发状态",
@@ -896,8 +924,14 @@
      applyNo: "",
      dateRange: [],
    },
    searchFormExpanded: false,
  });
  const { searchForm } = toRefs(data);
  const { searchForm, searchFormExpanded } = toRefs(data);
  // åˆ‡æ¢æœç´¢è¡¨å•展开/收起状态
  const toggleSearchForm = () => {
    data.searchFormExpanded = !data.searchFormExpanded;
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
@@ -1042,7 +1076,7 @@
        sum +
        (row.volume == null
          ? 0
          : (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4))
          : Number(Number(row.volume) - Number(row.assignedQuantity).toFixed(4)))
      );
    }, 0);
    sumAssignedQuantity.value = totalAssignedQuantity;
@@ -1087,10 +1121,12 @@
    }
    console.log(mergeForm, "mergeForm");
    const strengthItem = block_strength.value.find(item => item.id === mergeForm.strength);
    const strengthItem = block_strength.value.find(
      item => item.id === mergeForm.strength
    );
    const payload = {
      ...mergeForm,
      strength: strengthItem ? strengthItem.label : mergeForm.strength
      strength: strengthItem ? strengthItem.label : mergeForm.strength,
    };
    productionPlanCombine(payload)
      .then(res => {
@@ -1323,10 +1359,7 @@
  }
  .search_form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24px;
    // margin-bottom: 24px;
    padding: 20px;
    background-color: #ffffff;
    border-radius: 6px;
@@ -1338,6 +1371,36 @@
    }
  }
  .search-header {
    display: flex;
    justify-content: center;
    align-items: center;
    // margin-bottom: 5px;
    // padding-bottom: 5px;
    position: relative;
    bottom: 35px;
    // border-bottom: 1px solid #ebeef5;
  }
  .search-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
  }
  .search-header .el-button {
    color: #606266;
    transition: all 0.3s ease;
  }
  .search-header .el-button:hover {
    color: #409eff;
  }
  .search-header .el-icon {
    margin-right: 4px;
  }
  .table_list {
    // margin-bottom: 24px;
    background-color: #ffffff;
@@ -1345,6 +1408,7 @@
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
    overflow: hidden;
    height: calc(100vh - 250px);
    margin-top: 0px !important;
  }
  :deep(.el-table) {