From 09b9bff70fe5d5d5121c1ca6ecbe5ab07da1949a Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期三, 22 四月 2026 14:16:55 +0800
Subject: [PATCH] 生产计划模块引入

---
 src/api/productionPlan/productionPlan.js                               |   79 ++
 src/views/productionManagement/processRoute/processRouteItem/index.vue |    2 
 src/views/productionPlan/productionPlan/components/PIMTable.vue        |  470 ++++++++++++
 src/views/productionPlan/productionPlan/index.vue                      | 1688 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 2,238 insertions(+), 1 deletions(-)

diff --git a/src/api/productionPlan/productionPlan.js b/src/api/productionPlan/productionPlan.js
new file mode 100644
index 0000000..a284f21
--- /dev/null
+++ b/src/api/productionPlan/productionPlan.js
@@ -0,0 +1,79 @@
+// 鐢熶骇璁㈠崟椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+export function productionPlanListPage(query) {
+  return request({
+    url: "/productionPlan/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鎷夊彇鏁版嵁
+export function loadProdData(query) {
+  return request({
+    url: "/productionPlan/loadProdData",
+    method: "get",
+    params: query,
+  });
+}
+
+export function summaryByProductType(query) {
+  return request({
+    url: "/productionPlan/summaryByProductType",
+    method: "get",
+    params: query,
+  });
+}
+
+// 瀵煎嚭鐢熶骇璁″垝
+export function exportProductionPlan(bomId) {
+  return request({
+    url: "/productionPlan/export",
+    method: "post",
+    params: { bomId },
+    responseType: "blob",
+  });
+}
+
+// 鐢熶骇璁″垝-鏂板淇敼
+export function productionPlanAdd(query) {
+  return request({
+    url: "/productionPlan",
+    method: "post",
+    data: query,
+  });
+}
+export function productionPlanUpdate(query) {
+  return request({
+    url: "/productionPlan",
+    method: "put",
+    data: query,
+  });
+}
+
+// 鐢熶骇璁″垝-鍒犻櫎
+export function productionPlanDelete(data) {
+  return request({
+    url: "/productionPlan",
+    method: "delete",
+    data,
+  });
+}
+// 鍚堝苟涓嬪彂
+export function productionPlanCombine(query) {
+  return request({
+    url: "/productionPlan/combine",
+    method: "post",
+    data: query,
+  });
+}
+
+// 杩借釜杩涘害
+export function trackProgressByNo(query) {
+  return request({
+    url: "/track/trackProgressByNo",
+    method: "get",
+    params: query,
+  });
+}
diff --git a/src/views/productionManagement/processRoute/processRouteItem/index.vue b/src/views/productionManagement/processRoute/processRouteItem/index.vue
index 6fbaa2c..edd582b 100644
--- a/src/views/productionManagement/processRoute/processRouteItem/index.vue
+++ b/src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -220,7 +220,7 @@
               :default-expand-all="true"
               style="width: 100%">
       <el-table-column type="expand">
-        <template #default="props">
+        <template #default>
           <el-form ref="form"
                    :model="bomDataValue">
             <el-table :data="bomDataValue.dataList"
diff --git a/src/views/productionPlan/productionPlan/components/PIMTable.vue b/src/views/productionPlan/productionPlan/components/PIMTable.vue
new file mode 100644
index 0000000..9431002
--- /dev/null
+++ b/src/views/productionPlan/productionPlan/components/PIMTable.vue
@@ -0,0 +1,470 @@
+<template>
+  <el-table ref="multipleTable"
+            v-loading="tableLoading"
+            :border="border"
+            :data="tableData"
+            :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+            :height="height"
+            :highlight-current-row="highlightCurrentRow"
+            :row-class-name="rowClassName"
+            :row-style="rowStyle"
+            :row-key="rowKey"
+            :style="tableStyle"
+            tooltip-effect="dark"
+            :expand-row-keys="expandRowKeys"
+            :show-summary="isShowSummary"
+            :summary-method="summaryMethod"
+            @row-click="rowClick"
+            @current-change="currentChange"
+            @selection-change="handleSelectionChange"
+            @expand-change="expandChange"
+            @select-all="handleSelectAll"
+            class="lims-table">
+    <el-table-column align="center"
+                     type="selection"
+                     width="55"
+                     v-if="isSelection"
+                     :selectable="selectable" />
+    <el-table-column align="center"
+                     label="搴忓彿"
+                     type="index"
+                     width="60" />
+    <el-table-column v-for="(item, index) in column"
+                     :key="index"
+                     :column-key="item.columnKey"
+                     :filter-method="item.filterHandler"
+                     :filter-multiple="item.filterMultiple"
+                     :filtered-value="item.filteredValue"
+                     :filters="item.filters"
+                     :fixed="item.fixed"
+                     :label="item.label"
+                     :prop="item.prop"
+                     :show-overflow-tooltip="item.dataType !== 'action' && item.dataType !== 'slot'"
+                     :align="item.align"
+                     :sortable="!!item.sortable"
+                     :type="item.type"
+                     :width="item.width"
+                     :class-name="item.className || ''">
+      <template #header="scope">
+        <div class="pim-table-header-cell">
+          <div class="pim-table-header-title">
+            {{ item.label }}
+          </div>
+          <div v-if="item.headerSlot"
+               class="pim-table-header-extra">
+            <slot :name="item.headerSlot"
+                  :column="scope.column" />
+          </div>
+        </div>
+      </template>
+      <template v-if="item.hasOwnProperty('colunmTemplate')"
+                #[item.colunmTemplate]="scope">
+        <slot v-if="item.theadSlot"
+              :name="item.theadSlot"
+              :index="scope.$index"
+              :row="scope.row" />
+      </template>
+      <template #default="scope">
+        <!-- 鎻掓Ы -->
+        <div v-if="item.dataType == 'slot'"
+             :class="item.className || ''">
+          <slot v-if="item.slot"
+                :index="scope.$index"
+                :name="item.slot"
+                :row="scope.row" />
+        </div>
+        <!-- 杩涘害鏉� -->
+        <div v-else-if="item.dataType == 'progress'"
+             :class="item.className || ''">
+          <el-progress :percentage="Number(scope.row[item.prop])" />
+        </div>
+        <!-- 鍥剧墖 -->
+        <div v-else-if="item.dataType == 'image'"
+             :class="item.className || ''">
+          <img :src="javaApi + '/img/' + scope.row[item.prop]"
+               alt=""
+               style="width: 40px; height: 40px; margin-top: 10px" />
+        </div>
+        <!-- tag -->
+        <div v-else-if="item.dataType == 'tag'"
+             :class="item.className || ''">
+          <el-tag v-if="
+              typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
+              'string'
+            "
+                  :title="formatters(scope.row[item.prop], item.formatData)"
+                  :type="formatType(scope.row[item.prop], item.formatType)">
+            {{ formatters(scope.row[item.prop], item.formatData) }}
+          </el-tag>
+          <el-tag v-for="(tag, index) in dataTypeFn(
+              scope.row[item.prop],
+              item.formatData
+            )"
+                  v-else-if="
+              typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
+              'object'
+            "
+                  :key="index"
+                  :title="formatters(scope.row[item.prop], item.formatData)"
+                  :type="formatType(tag, item.formatType)">
+            {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }}
+          </el-tag>
+          <el-tag v-else
+                  :title="formatters(scope.row[item.prop], item.formatData)"
+                  :type="formatType(scope.row[item.prop], item.formatType)">
+            {{ formatters(scope.row[item.prop], item.formatData) }}
+          </el-tag>
+        </div>
+        <!-- 鎸夐挳 -->
+        <div v-else-if="item.dataType == 'action'"
+             :class="item.className || ''"
+             @click.stop>
+          <template v-for="(o, key) in item.operation"
+                    :key="key">
+            <el-button v-show="o.type != 'upload'"
+                       v-if="o.showHide ? o.showHide(scope.row) : true"
+                       :disabled="o.disabled ? o.disabled(scope.row) : false"
+                       :plain="o.plain"
+                       type="primary"
+                       :style="{
+                color:
+                  o.name === '鍒犻櫎' || o.name === 'delete'
+                    ? '#f56c6c'
+                    : o.color,
+              }"
+                       link
+                       @click.stop="o.clickFun(scope.row)"
+                       :key="key">
+              {{ o.name }}
+            </el-button>
+            <el-upload :action="
+                javaApi +
+                o.url +
+                '?id=' +
+                (o.uploadIdFun ? o.uploadIdFun(scope.row) : scope.row.id)
+              "
+                       ref="uploadRef"
+                       :multiple="o.multiple ? o.multiple : false"
+                       :limit="1"
+                       :disabled="o.disabled ? o.disabled(scope.row) : false"
+                       :accept="
+                o.accept
+                  ? o.accept
+                  : '.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'
+              "
+                       v-if="o.type == 'upload'"
+                       style="display: inline-block; width: 50px"
+                       v-show="o.showHide ? o.showHide(scope.row) : true"
+                       :headers="uploadHeader"
+                       :before-upload="(file) => beforeUpload(file, scope.$index)"
+                       :on-change="
+                (file, fileList) => handleChange(file, fileList, scope.$index)
+              "
+                       :on-error="
+                (error, file, fileList) =>
+                  onError(error, file, fileList, scope.$index)
+              "
+                       :on-success="
+                (response, file, fileList) =>
+                  handleSuccessUp(response, file, fileList, scope.$index)
+              "
+                       :on-exceed="onExceed"
+                       :show-file-list="false">
+              <el-button link
+                         type="primary"
+                         :disabled="o.disabled ? o.disabled(scope.row) : false">{{ o.name }}</el-button>
+            </el-upload>
+          </template>
+        </div>
+        <!-- 鍙偣鍑荤殑鏂囧瓧 -->
+        <div v-else-if="item.dataType == 'link'"
+             :class="item.className || ''"
+             class="cell link"
+             style="width: 100%"
+             @click="goLink(scope.row, item.linkMethod)">
+          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
+        </div>
+        <!-- 榛樿绾睍绀烘暟鎹� -->
+        <div v-else
+             class="cell"
+             :class="item.className || ''"
+             style="width: 100%">
+          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
+          <span v-else>{{
+            formatters(scope.row[item.prop], item.formatData)
+          }}</span>
+        </div>
+      </template>
+    </el-table-column>
+  </el-table>
+  <pagination v-if="isShowPagination"
+              :total="page.total"
+              :layout="page.layout"
+              :page="page.current"
+              :limit="page.size"
+              @pagination="paginationSearch" />
+</template>
+
+<script setup>
+  import pagination from "../../../../components/PIMTable/Pagination.vue";
+  import { ref, inject, getCurrentInstance } from "vue";
+  import { ElMessage } from "element-plus";
+
+  // 鑾峰彇鍏ㄥ眬鐨� uploadHeader
+  const { proxy } = getCurrentInstance();
+  const uploadHeader = proxy.uploadHeader;
+  const javaApi = proxy.javaApi;
+
+  const emit = defineEmits([
+    "pagination",
+    "expand-change",
+    "selection-change",
+    "row-click",
+  ]);
+
+  // Filters
+  const typeFn = (val, row) => {
+    return typeof val === "function" ? val(row) : val;
+  };
+
+  const formatters = (val, format) => {
+    return typeof format === "function" ? format(val) : val;
+  };
+
+  // Props锛堜娇鐢� defineProps 鐨勯潪 TS 褰㈠紡锛�
+  const props = defineProps({
+    tableLoading: {
+      type: Boolean,
+      default: false,
+    },
+    height: {
+      type: [Number, String],
+      default: "calc(100vh - 22em)",
+    },
+    expandRowKeys: {
+      type: Array,
+      default: () => [],
+    },
+    summaryMethod: {
+      type: Function,
+      default: () => {},
+    },
+    rowClick: {
+      type: Function,
+      default: () => {},
+    },
+    currentChange: {
+      type: Function,
+      default: () => {},
+    },
+    border: {
+      type: Boolean,
+      default: true,
+    },
+    isSelection: {
+      type: Boolean,
+      default: false,
+    },
+    selectable: {
+      type: Function,
+      default: () => true,
+    },
+    isShowPagination: {
+      type: Boolean,
+      default: true,
+    },
+    isShowSummary: {
+      type: Boolean,
+      default: false,
+    },
+    highlightCurrentRow: {
+      type: Boolean,
+      default: false,
+    },
+    headerCellStyle: {
+      type: Object,
+      default: () => ({}),
+    },
+    column: {
+      type: Array,
+      default: () => [],
+    },
+    rowClassName: {
+      type: Function,
+      default: () => "",
+    },
+    rowStyle: {
+      type: [Object, Function],
+      default: () => ({}),
+    },
+    tableData: {
+      type: Array,
+      default: () => [],
+    },
+    rowKey: {
+      type: String,
+      default: "id",
+    },
+    page: {
+      type: Object,
+      default: () => ({
+        total: 0,
+        current: 0,
+        size: 10,
+        layout: "total, sizes, prev, pager, next, jumper",
+      }),
+    },
+    total: {
+      type: Number,
+      default: 0,
+    },
+    tableStyle: {
+      type: [String, Object],
+      default: () => ({ width: "100%" }),
+    },
+  });
+
+  // Data
+  const multipleTable = ref(null);
+  const uploadRefs = ref([]);
+  const currentFiles = ref({});
+  const uploadKeys = ref({});
+
+  const indexMethod = index => {
+    return (props.page.current - 1) * props.page.size + index + 1;
+  };
+
+  // 鐐瑰嚮 link 浜嬩欢
+  const goLink = (row, linkMethod) => {
+    if (!linkMethod) {
+      return ElMessage.warning("璇烽厤缃� link 浜嬩欢");
+    }
+    const parentMethod = getParentMethod(linkMethod);
+    if (typeof parentMethod === "function") {
+      parentMethod(row);
+    } else {
+      console.warn(`鐖剁粍浠朵腑鏈壘鍒版柟娉�: ${linkMethod}`);
+    }
+  };
+
+  // 鑾峰彇鐖剁粍浠舵柟娉曪紙绀轰緥瀹炵幇锛�
+  const getParentMethod = methodName => {
+    const parentMethods = inject("parentMethods", {});
+    return parentMethods[methodName];
+  };
+
+  const dataTypeFn = (val, format) => {
+    if (typeof format === "function") {
+      return format(val);
+    } else return val;
+  };
+
+  const formatType = (val, format) => {
+    if (typeof format === "function") {
+      return format(val);
+    } else return "";
+  };
+
+  // 鏂囦欢鍙樺寲澶勭悊
+  const handleChange = (file, fileList, index) => {
+    if (fileList.length > 1) {
+      const earliestFile = fileList[0];
+      uploadRefs.value[index]?.handleRemove(earliestFile);
+    }
+    currentFiles.value[index] = file;
+  };
+
+  // 鏂囦欢涓婁紶鍓嶆牎楠�
+  const beforeUpload = (rawFile, index) => {
+    currentFiles.value[index] = {};
+    if (rawfile.size > 1024 * 1024 * 10 * 10) {
+      ElMessage.error("涓婁紶鏂囦欢涓嶈秴杩�10M");
+      return false;
+    }
+    return true;
+  };
+
+  // 涓婁紶鎴愬姛
+  const handleSuccessUp = (response, file, fileList, index) => {
+    if (response.code == 200) {
+      if (uploadRefs[index]) {
+        uploadRefs[index].clearFiles();
+      }
+      currentFiles[index] = file;
+      ElMessage.success("涓婁紶鎴愬姛");
+      resetUploadComponent(index);
+    } else {
+      ElMessage.error(response.message);
+    }
+  };
+
+  const resetUploadComponent = index => {
+    uploadKeys[index] = Date.now();
+  };
+
+  // 涓婁紶澶辫触
+  const onError = (error, file, fileList, index) => {
+    ElMessage.error("鏂囦欢涓婁紶澶辫触锛岃閲嶈瘯");
+    if (uploadRefs.value[index]) {
+      uploadRefs.value[index].clearFiles();
+    }
+  };
+
+  // 鏂囦欢鏁伴噺瓒呴檺鎻愮ず
+  const onExceed = () => {
+    ElMessage.warning("瓒呭嚭鏂囦欢涓暟");
+  };
+
+  const paginationSearch = ({ page, limit }) => {
+    emit("pagination", { page: page, limit: limit });
+  };
+
+  const rowClick = row => {
+    emit("row-click", row);
+  };
+
+  const expandChange = (row, expandedRows) => {
+    emit("expand-change", row, expandedRows);
+  };
+
+  const handleSelectionChange = newSelection => {
+    emit("selection-change", newSelection);
+  };
+
+  // 澶勭悊鍏ㄩ�夋搷浣�
+  const handleSelectAll = selection => {
+    if (selection.length) {
+      console.log(selection, "selection");
+      // 鍏ㄩ�夋椂锛屽彧閫夋嫨鍙�夋嫨鐨勮
+      const selectableRows = props.tableData.filter(row => props.selectable(row));
+      // 娓呯┖褰撳墠閫夋嫨
+      multipleTable.value.clearSelection();
+      // 鍙�夋嫨鍙�夋嫨鐨勮
+      selectableRows.forEach(row => {
+        multipleTable.value.toggleRowSelection(row, true);
+      });
+    } else {
+      // 鍙栨秷鍏ㄩ�夋椂锛屽彧鍙栨秷鍙�夋嫨鐨勮鐨勯�夋嫨
+      props.tableData.forEach(row => {
+        if (props.selectable(row)) {
+          multipleTable.value.toggleRowSelection(row, false);
+        }
+      });
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .cell {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    padding-right: 0 !important;
+    padding-left: 0 !important;
+  }
+
+  .pim-table-header-extra :deep(.el-input),
+  .pim-table-header-extra :deep(.el-select) {
+    width: 100%;
+  }
+</style>
diff --git a/src/views/productionPlan/productionPlan/index.vue b/src/views/productionPlan/productionPlan/index.vue
new file mode 100644
index 0000000..53a99f5
--- /dev/null
+++ b/src/views/productionPlan/productionPlan/index.vue
@@ -0,0 +1,1688 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <el-form :model="searchForm"
+               :inline="true">
+        <!-- 绠�鍖栫増鎼滅储鏉′欢 -->
+        <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-date-picker v-model="searchForm.dateRange"
+                          type="daterange"
+                          range-separator="鑷�"
+                          start-placeholder="寮�濮嬫棩鏈�"
+                          end-placeholder="缁撴潫鏃ユ湡"
+                          value-format="YYYY-MM-DD"
+                          style="width: 240px;"
+                          @change="handleQuery" />
+        </el-form-item>
+        <el-form-item label="涓嬪彂鐘舵��:">
+          <el-select v-model="searchForm.status"
+                     placeholder="璇烽�夋嫨鐘舵��"
+                     clearable
+                     filterable
+                     style="width: 100px">
+            <el-option label="寰呬笅鍙�"
+                       value="0" />
+            <el-option label="閮ㄥ垎涓嬪彂"
+                       value="1" />
+            <el-option label="宸蹭笅鍙�"
+                       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>
+          <el-button type="info"
+                     @click="handleReset">閲嶇疆</el-button>
+          <el-button type="primary"
+                     @click="handleAdd">鏂板</el-button>
+          <el-button type="warning"
+                     @click="getLoadProdData"
+                     :loading="loadProdDataLoading">鎷夊彇鏁版嵁</el-button>
+          <el-button type="warning"
+                     @click="handleMerge">鍚堝苟涓嬪彂</el-button>
+          <el-button type="warning"
+                     @click="handleImport">瀵煎叆</el-button>
+          <el-button type="warning"
+                     @click="handleExport">瀵煎嚭</el-button>
+        </el-form-item>
+      </el-form>
+      <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"
+                :tableData="tableData"
+                :page="page"
+                height="calc(100vh - 350px)"
+                :tableLoading="tableLoading"
+                :isSelection="true"
+                :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"
+               label-width="120px">
+        <el-row :gutter="20">
+          <el-col :span="10">
+            <el-form-item label="鐗╂枡缂栫爜">
+              <div class="info-display">{{ mergeForm.materialCode || '-' }}</div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="10">
+            <el-form-item label="浜у搧鍚嶇О">
+              <el-tag class="info-display">{{ mergeForm.productName || '-' }}</el-tag>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="10">
+            <el-form-item label="浜у搧瑙勬牸">
+              <div class="info-display">{{ mergeForm.model || '-' }}</div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="10">
+            <el-form-item label="闀�*瀹�*楂�">
+              <div class="info-display">{{ mergeForm.length || '-' }}*{{ mergeForm.width || '-' }}*{{ mergeForm.height || '-' }}</div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="璁″垝瀹屾垚鏃堕棿">
+          <el-date-picker v-model="mergeForm.planCompleteTime"
+                          type="date"
+                          value-format="YYYY-MM-DD"
+                          style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="寮哄害"
+                      v-if="mergeForm.productName === '鐮屽潡'">
+          <div v-if="strengthError"
+               class="strength-error"
+               style="color: red; margin-bottom: 8px;">{{ strengthError }}</div>
+          <el-select v-model="mergeForm.strength"
+                     placeholder="璇烽�夋嫨寮哄害"
+                     style="width: 100%"
+                     required>
+            <el-option v-for="item in block_strength"
+                       :key="item.id"
+                       :label="item.label"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐢熶骇鏂规暟">
+          <el-input-number v-model="mergeForm.totalAssignedQuantity"
+                           :min="0"
+                           :max="sumAssignedQuantity"
+                           @change="onBlur"
+                           style="width: 100%" />
+        </el-form-item>
+        <!-- <el-form-item label="澶囨敞">
+          <el-input v-model="mergeForm.remark"
+                    type="textarea" />
+        </el-form-item> -->
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="isShowNewModal = false">鍙栨秷</el-button>
+          <el-button type="primary"
+                     @click="handleMergeSubmit">纭畾涓嬪彂</el-button>
+        </span>
+      </template>
+    </el-dialog>
+    <!-- 瀵煎叆寮圭獥 -->
+    <ImportDialog ref="importDialogRef"
+                  v-model="importDialogVisible"
+                  title="瀵煎叆鐢熶骇璁″垝"
+                  :action="importAction"
+                  :headers="importHeaders"
+                  :auto-upload="false"
+                  :on-success="handleImportSuccess"
+                  :on-error="handleImportError"
+                  @confirm="handleImportConfirm"
+                  @download-template="handleDownloadTemplate"
+                  @close="handleImportClose" />
+    <!-- 鏂板/缂栬緫寮圭獥 -->
+    <el-dialog v-model="dialogVisible"
+               destroy-on-close
+               :title="operationType === 'add' ? '鏂板鐢熶骇璁″垝' : '缂栬緫鐢熶骇璁″垝'"
+               width="600px">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="120px">
+        <el-form-item label="鐢宠鍗曠紪鍙�"
+                      prop="applyNo">
+          <el-input v-model="form.applyNo"
+                    placeholder="璇疯緭鍏ョ敵璇峰崟缂栧彿" />
+        </el-form-item>
+        <el-form-item label="瀹㈡埛鍚嶇О"
+                      prop="customerName">
+          <el-input v-model="form.customerName"
+                    placeholder="璇疯緭鍏ュ鎴峰悕绉�" />
+        </el-form-item>
+        <el-form-item label="浜у搧鍚嶇О"
+                      prop="productMaterialId">
+          <el-tree-select v-model="form.productMaterialId"
+                          placeholder="璇烽�夋嫨"
+                          clearable
+                          :data="productOptions"
+                          :render-after-expand="false"
+                          filterable
+                          @change="handleProductChange"
+                          style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="浜у搧瑙勬牸"
+                      prop="productMaterialSkuId">
+          <el-select v-model="form.productMaterialSkuId"
+                     @change="handleChangeSpecification"
+                     filterable
+                     placeholder="璇烽�夋嫨">
+            <el-option v-for="item in specificationOptions"
+                       :key="item.id"
+                       :label="item.model"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鍧楁暟"
+                      prop="quantity">
+          <el-input-number v-model="form.quantity"
+                           :min="0"
+                           placeholder="璇疯緭鍏ュ潡鏁�" />
+        </el-form-item>
+        <el-form-item label="鏂规暟"
+                      prop="volume">
+          <el-input-number v-model="form.volume"
+                           :min="0"
+                           placeholder="璇疯緭鍏ユ柟鏁�" />
+        </el-form-item>
+        <el-form-item label="闀�"
+                      prop="length">
+          <el-input-number v-model="form.length"
+                           :min="0"
+                           placeholder="璇疯緭鍏ラ暱搴�" />
+        </el-form-item>
+        <el-form-item label="瀹�"
+                      prop="width">
+          <el-input-number v-model="form.width"
+                           :min="0"
+                           placeholder="璇疯緭鍏ュ搴�" />
+        </el-form-item>
+        <el-form-item label="楂�"
+                      prop="height">
+          <el-input-number v-model="form.height"
+                           :min="0"
+                           placeholder="璇疯緭鍏ラ珮搴�" />
+        </el-form-item>
+        <el-form-item label="璁″垝寮�濮嬫棩鏈�"
+                      prop="startDate">
+          <el-date-picker v-model="form.startDate"
+                          type="date"
+                          value-format="YYYY-MM-DD"
+                          placeholder="璇烽�夋嫨璁″垝寮�濮嬫棩鏈�" />
+        </el-form-item>
+        <el-form-item label="璁″垝缁撴潫鏃ユ湡"
+                      prop="endDate">
+          <el-date-picker v-model="form.endDate"
+                          type="date"
+                          value-format="YYYY-MM-DD"
+                          placeholder="璇烽�夋嫨璁″垝缁撴潫鏃ユ湡" />
+        </el-form-item>
+        <el-form-item label="寮哄害"
+                      prop="strength"
+                      v-if="form.productName === '鐮屽潡'">
+          <el-select v-model="form.strength"
+                     placeholder="璇烽�夋嫨寮哄害"
+                     style="width: 100%">
+            <el-option v-for="item in block_strength"
+                       :key="item.label"
+                       :label="item.label"
+                       :value="item.label" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="澶囨敞 1"
+                      prop="remarkOne">
+          <el-input v-model="form.remarkOne"
+                    placeholder="璇疯緭鍏ュ娉� 1" />
+        </el-form-item>
+        <el-form-item label="澶囨敞 2"
+                      prop="remarkTwo">
+          <el-input v-model="form.remarkTwo"
+                    placeholder="璇疯緭鍏ュ娉� 2" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+          <el-button type="primary"
+                     @click="handleSubmit">纭畾</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<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";
+  import { useDict } from "@/utils/dict";
+  import { useRouter } from "vue-router";
+  // import {
+  //   productionPlanListPage,
+  //   loadProdData,
+  //   exportProductionPlan,
+  //   productionPlanAdd,
+  //   productionPlanUpdate,
+  //   productionPlanDelete,
+  //   productionPlanCombine,
+  // } from "@/api/productionPlan/productionPlan.js";
+
+  // Mock data and functions
+  const productionPlanListPage = params => {
+    console.log("Mock productionPlanListPage called with:", params);
+    return Promise.resolve({
+      data: {
+        records: [
+          {
+            id: 1,
+            dataSourceType: 1,
+            applyNo: "SQ20260422001",
+            customerName: "妯℃嫙瀹㈡埛A",
+            productName: "鏉挎潗",
+            model: "100*200*300",
+            materialCode: "MAT001",
+            quantity: 100,
+            volume: 50,
+            status: 0,
+            assignedQuantity: 0,
+            length: 100,
+            width: 200,
+            height: 300,
+            startDate: "2026-04-22",
+            endDate: "2026-04-25",
+            strength: "C20",
+            remarkOne: "澶囨敞1",
+            remarkTwo: "澶囨敞2",
+          },
+          {
+            id: 2,
+            dataSourceType: 2,
+            applyNo: "SQ20260422002",
+            customerName: "妯℃嫙瀹㈡埛B",
+            productName: "鐮屽潡",
+            model: "200*200*600",
+            materialCode: "MAT002",
+            quantity: 200,
+            volume: 80,
+            status: 1,
+            assignedQuantity: 30,
+            length: 200,
+            width: 200,
+            height: 600,
+            startDate: "2026-04-23",
+            endDate: "2026-04-26",
+            strength: "C25",
+            remarkOne: "澶囨敞1",
+            remarkTwo: "澶囨敞2",
+          },
+          {
+            id: 3,
+            dataSourceType: 1,
+            applyNo: "SQ20260422003",
+            customerName: "妯℃嫙瀹㈡埛C",
+            productName: "鐮屽潡",
+            model: "240*115*53",
+            materialCode: "MAT003",
+            quantity: 1000,
+            volume: 1.46,
+            status: 2,
+            assignedQuantity: 1.46,
+            length: 240,
+            width: 115,
+            height: 53,
+            startDate: "2026-04-20",
+            endDate: "2026-04-21",
+            strength: "MU10",
+            remarkOne: "宸蹭笅鍙戞暟鎹�",
+            remarkTwo: "",
+          },
+        ],
+        total: 3,
+      },
+    });
+  };
+
+  const loadProdData = () => {
+    console.log("Mock loadProdData called");
+    return Promise.resolve({ code: 200, msg: "鍚屾鎴愬姛" });
+  };
+
+  const exportProductionPlan = () => {
+    console.log("Mock exportProductionPlan called");
+    return Promise.resolve();
+  };
+
+  const productionPlanAdd = payload => {
+    console.log("Mock productionPlanAdd called with:", payload);
+    return Promise.resolve({ code: 200, msg: "鏂板鎴愬姛" });
+  };
+
+  const productionPlanUpdate = payload => {
+    console.log("Mock productionPlanUpdate called with:", payload);
+    return Promise.resolve({ code: 200, msg: "淇敼鎴愬姛" });
+  };
+
+  const productionPlanDelete = ids => {
+    console.log("Mock productionPlanDelete called with ids:", ids);
+    return Promise.resolve({ code: 200, msg: "鍒犻櫎鎴愬姛" });
+  };
+
+  const productionPlanCombine = payload => {
+    console.log("Mock productionPlanCombine called with:", payload);
+    return Promise.resolve({ code: 200, msg: "鍚堝苟涓嬪彂鎴愬姛" });
+  };
+  import PIMTable from "./components/PIMTable.vue";
+  // import {
+  //   modelListPage,
+  //   productTreeList,
+  //   productTreeListQuery,
+  // } from "@/api/basicData/newProduct.js";
+
+  const { proxy } = getCurrentInstance();
+  const router = useRouter();
+
+  const tableColumn = 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 => {
+        // const typeMap = {
+        //   鏉挎潗: "primary",
+        //   鐮屽潡: "warning",
+        // };
+        // return typeMap[params] || "info";
+        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: "serialNo",
+    //   width: "150px",
+    //   className: "code-cell",
+    // },
+    {
+      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 "";
+        const strengthItem = block_strength.value.find(item => item.id === cell);
+        return strengthItem ? strengthItem.label : cell;
+      },
+    },
+
+    {
+      label: "澶囨敞 1",
+      width: "150px",
+      prop: "remarkOne",
+    },
+    {
+      label: "澶囨敞 2",
+      width: "150px",
+      prop: "remarkTwo",
+    },
+
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 300,
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "primary",
+          link: true,
+          showHide: row => {
+            return row.status == 0 && row.dataSourceType != 1;
+            //status锛�0锛氬緟涓嬪彂锛�1锛氶儴鍒嗕笅鍙戯紝2锛氬凡涓嬪彂
+          },
+          clickFun: row => {
+            handleEdit(row);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          link: true,
+          showHide: row => {
+            return row.status == 0;
+          },
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+        {
+          name: "涓嬪彂",
+          type: "text",
+          showHide: row => {
+            // 璁$畻鍓╀綑鏂规暟
+            const remainingVolume =
+              (row.volume || 0) - (row.assignedQuantity || 0);
+            // 濡傛灉鍓╀綑鏂规暟灏忎簬绛変簬0锛岀姝㈤�夋嫨
+            return remainingVolume > 0;
+          },
+          clickFun: row => {
+            // 鍗曠嫭涓嬪彂鎿嶄綔
+            // 璁剧疆琛ㄥ崟鏁版嵁
+            strengthError.value = "";
+            mergeForm.ids = [row.id];
+            mergeForm.materialCode = row.materialCode;
+            mergeForm.productName = row.productName || "";
+            mergeForm.model = row.model || "";
+            mergeForm.length = row.length || 0;
+            mergeForm.width = row.width || 0;
+            mergeForm.height = row.height || 0;
+            mergeForm.totalAssignedQuantity =
+              (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4) || 0;
+            mergeForm.planCompleteTime = row.planCompleteTime || "";
+            mergeForm.productMaterialId = row.productMaterialId || "";
+            mergeForm.strength = row.strength || "";
+            sumAssignedQuantity.value = mergeForm.totalAssignedQuantity;
+            // 鎵撳紑寮圭獥
+            isShowNewModal.value = true;
+          },
+        },
+        // {
+        //   name: "杩借釜杩涘害",
+        //   type: "text",
+        //   clickFun: row => {
+        //     handleTrackProgress(row);
+        //   },
+        // },
+      ],
+    },
+  ]);
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const page = reactive({
+    current: 1,
+    size: 100,
+    total: 0,
+  });
+  const selectedRows = ref([]);
+
+  // 浜у搧绫诲埆姹囨�荤粺璁℃暟鎹�
+  const categorySummary = ref([]);
+  // 浜у搧绫诲埆姹囨�诲脊绐楁帶鍒�
+  const showCategorySummaryDialog = ref(false);
+
+  // 鍚堝苟涓嬪彂寮圭獥鎺у埗
+  const isShowNewModal = ref(false);
+  // 鍚堝苟涓嬪彂琛ㄥ崟鏁版嵁
+  const mergeForm = reactive({
+    materialCode: "",
+    productName: "",
+    model: "",
+    length: 0,
+    width: 0,
+    height: 0,
+    totalAssignedQuantity: 0,
+    planCompleteTime: "",
+    strength: "",
+    productMaterialId: "",
+  });
+
+  // 瀵煎叆鐩稿叧
+  const importDialogRef = ref(null);
+  const importDialogVisible = ref(false);
+  const importAction =
+    import.meta.env.VITE_APP_BASE_API + "/productionPlan/import";
+  const importHeaders = ref({
+    Authorization: `Bearer ${getToken()}`,
+  });
+
+  // 鏂板/缂栬緫鐩稿叧
+  const dialogVisible = ref(false);
+  const operationType = ref("add"); // add | edit
+  const productOptions = ref([]);
+  const specificationOptions = ref([]);
+  const formRef = ref(null);
+  // 鑾峰彇寮哄害瀛楀吀
+  const { block_strength } = useDict("block_strength");
+  const form = reactive({
+    id: undefined,
+    applyNo: "",
+    customerName: "",
+    productMaterialId: undefined,
+    productMaterialSkuId: undefined,
+    productName: "",
+    model: "",
+    materialCode: "",
+    quantity: 0,
+    volume: 0,
+    length: 0,
+    width: 0,
+    height: 0,
+    startDate: "",
+    endDate: "",
+    status: "",
+    strength: "",
+    remarkOne: "",
+    remarkTwo: "",
+  });
+  const rules = reactive({
+    applyNo: [{ required: true, message: "璇疯緭鍏ョ敵璇峰崟缂栧彿", trigger: "blur" }],
+    customerName: [
+      { required: true, message: "璇疯緭鍏ュ鎴峰悕绉�", trigger: "blur" },
+    ],
+    productMaterialSkuId: [
+      { required: true, message: "璇烽�夋嫨浜у搧瑙勬牸", trigger: "change" },
+    ],
+    volume: [{ required: true, message: "璇疯緭鍏ユ柟鏁�", trigger: "blur" }],
+    productMaterialId: [
+      { required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" },
+    ],
+    strength: [
+      {
+        validator: (rule, value, callback) => {
+          if (form.productName === "鐮屽潡" && !value) {
+            callback(new Error("鐮屽潡浜у搧鐨勫己搴︿负蹇呭~椤�"));
+          } else {
+            callback();
+          }
+        },
+        trigger: ["blur", "change"],
+        required: false,
+      },
+    ],
+  });
+
+  // 澶勭悊杩借釜杩涘害鎸夐挳鐐瑰嚮
+  const handleTrackProgress = row => {
+    // 璺宠浆鍒拌拷韪繘搴﹂〉闈�
+    router.push({
+      path: "/productionPlan/trackProgress",
+      query: {
+        id: row.id,
+        applyNo: row.applyNo,
+        productName: row.productName,
+        model: row.model,
+      },
+    });
+  };
+  const onBlur = value => {
+    // 闄愬埗鍥涗綅灏忔暟
+    mergeForm.totalAssignedQuantity = Number(value.toFixed(4));
+  };
+
+  const fetchProductOptions = () => {
+    // return productTreeList({ type: 2 }).then(res => {
+    //   productOptions.value = convertIdToValue(res.data);
+    //   return res;
+    // });
+  };
+
+  const convertIdToValue = data => {
+    return data.map(item => {
+      const newItem = {
+        value: `config_${item.configId}`, // 浣跨敤config_鍓嶇紑纭繚鍞竴鎬�
+        label: item.configName,
+        disabled: item.materialList.length === 0,
+      };
+      if (item.materialList && item.materialList.length > 0) {
+        newItem.children = item.materialList.map(material => ({
+          value: material.id, // 浣跨敤material鐨刬d浣滀负value
+          label: material.productName, // 浣跨敤materialName浣滀负label
+        }));
+      }
+
+      return newItem;
+    });
+  };
+
+  const handleProductChange = value => {
+    form.productMaterialSkuId = undefined;
+    // 鏌ユ壘閫変腑鐨勪骇鍝佸悕绉�
+    const findProductName = (options, value) => {
+      for (const option of options) {
+        if (option.value === value) {
+          return option.label;
+        }
+        if (option.children) {
+          const found = findProductName(option.children, value);
+          if (found) {
+            return found;
+          }
+        }
+      }
+      return "";
+    };
+    form.productName = findProductName(productOptions.value, value);
+    // 瑙﹀彂寮哄害瀛楁楠岃瘉
+    if (formRef.value) {
+      formRef.value.validateField("strength");
+    }
+    fetchSpecificationOptions(value);
+  };
+
+  const fetchSpecificationOptions = productId => {
+    specificationOptions.value = [];
+    if (productId) {
+      // modelListPage({ productId: productId, size: -1, current: -1 }).then(res => {
+      //   specificationOptions.value = res.data.records;
+      // });
+    }
+  };
+
+  const handleChangeSpecification = value => {
+    form.materialCode = undefined;
+    const selectedModel = specificationOptions.value.find(
+      item => item.id === value
+    );
+    if (selectedModel) {
+      form.materialCode = selectedModel.materialCode;
+      // 瑙f瀽瑙勬牸瀛楃涓茶幏鍙栭暱瀹介珮
+      const model = selectedModel.model;
+      if (model) {
+        const dimensions = model.match(/^(\d+)\*(\d+)\*(\d+)$/);
+        if (dimensions && dimensions.length === 4) {
+          form.length = parseInt(dimensions[1]);
+          form.width = parseInt(dimensions[2]);
+          form.height = parseInt(dimensions[3]);
+        }
+      }
+    }
+  };
+
+  const data = reactive({
+    searchForm: {
+      customerName: "",
+      productName: "",
+      model: "",
+      materialCode: "",
+      applyNo: "",
+      dateRange: [],
+    },
+    searchFormExpanded: false,
+  });
+  const { searchForm, searchFormExpanded } = toRefs(data);
+
+  // 鍒囨崲鎼滅储琛ㄥ崟灞曞紑/鏀惰捣鐘舵��
+  const toggleSearchForm = () => {
+    data.searchFormExpanded = !data.searchFormExpanded;
+  };
+
+  // 鏌ヨ鍒楄〃
+  /** 鎼滅储鎸夐挳鎿嶄綔 */
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  /** 閲嶇疆鎸夐挳鎿嶄綔 */
+  const handleReset = () => {
+    Object.assign(searchForm.value, {
+      customerName: "",
+      productName: "",
+      model: "",
+      materialCode: "",
+      applyNo: "",
+      dateRange: [],
+    });
+    page.current = 1;
+    getList();
+  };
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+  // 璁$畻浜у搧绫诲埆姹囨�荤粺璁�
+  const calculateCategorySummary = () => {
+    const summary = {};
+
+    // 閬嶅巻琛ㄦ牸鏁版嵁锛屾寜浜у搧绫诲埆姹囨��
+    tableData.value.forEach(row => {
+      const category = row.materialCode;
+      if (!summary[category]) {
+        summary[category] = {
+          materialCode: category,
+          totalAssignedQuantity: 0,
+        };
+      }
+      summary[category].totalAssignedQuantity += (
+        Number(row.volume) - Number(row.assignedQuantity)
+      ).toFixed(4);
+    });
+
+    // 杞崲涓烘暟缁勬牸寮�
+    categorySummary.value = Object.values(summary);
+  };
+
+  const getList = () => {
+    tableLoading.value = true;
+    // 鏋勯�犳悳绱㈠弬鏁�
+    const params = { ...searchForm.value, ...page };
+    params.startDate = params.dateRange ? params.dateRange[0] : "";
+    params.endDate = params.dateRange ? params.dateRange[1] : "";
+    delete params.dateRange;
+    productionPlanListPage(params)
+      .then(res => {
+        tableLoading.value = false;
+
+        tableData.value = res.data.records;
+        page.total = res.data.total;
+        // 璁$畻浜у搧绫诲埆姹囨�荤粺璁�
+        calculateCategorySummary();
+      })
+      .catch(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  // 閫変腑鐨勫簭鍒楀彿
+  const selectedserialNo = ref("");
+
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+    // 濡傛灉鏈夐�変腑鐨勮锛岃褰曠涓�涓�変腑琛岀殑搴忓垪鍙�
+    if (selection.length > 0) {
+      selectedserialNo.value = selection[0].materialCode;
+    } else {
+      // 濡傛灉娌℃湁閫変腑鐨勮锛屾竻绌哄簭鍒楀彿
+      selectedserialNo.value = "";
+    }
+  };
+
+  // 鍒ゆ柇琛屾槸鍚﹀彲閫夋嫨
+  const isSelectable = row => {
+    // 璁$畻鍓╀綑鏂规暟
+    const remainingVolume = (row.volume || 0) - (row.assignedQuantity || 0);
+    // 濡傛灉鍓╀綑鏂规暟灏忎簬绛変簬0锛岀姝㈤�夋嫨
+    if (remainingVolume <= 0) {
+      return false;
+    }
+    // 濡傛灉娌℃湁閫変腑鐨勮锛屾墍鏈夎閮藉彲閫夋嫨
+    if (!selectedserialNo.value) {
+      return true;
+    }
+    // 濡傛灉鏈夐�変腑鐨勮锛屽彧鏈夊簭鍒楀彿鐩稿悓鐨勮鎵嶅彲閫夋嫨
+    return row.materialCode === selectedserialNo.value;
+  };
+  // 鎷夊彇鏁版嵁鎸夐挳鎿嶄綔
+  const loadProdDataLoading = ref(false);
+  const getLoadProdData = () => {
+    // 鏄剧ず鍔犺浇鎻愮ず
+    loadProdDataLoading.value = true;
+    proxy.$modal.loading("姝e湪鎷夊彇鏁版嵁锛岃绋嶅��...");
+
+    loadProdData()
+      .then(res => {
+        proxy.$modal.closeLoading();
+        getList();
+        proxy.$modal.msgSuccess("鏁版嵁鎷夊彇鎴愬姛");
+      })
+      .catch(err => {
+        proxy.$modal.closeLoading();
+        console.error("鎷夊彇澶辫触:", err);
+        proxy.$modal.msgError("鏁版嵁鎷夊彇澶辫触锛岃閲嶈瘯");
+      })
+      .finally(() => {
+        loadProdDataLoading.value = false;
+      });
+  };
+  const sumAssignedQuantity = ref(0);
+  // 鍝嶅簲寮忔暟鎹�
+  const strengthError = ref("");
+
+  // 澶勭悊鍚堝苟涓嬪彂鎸夐挳鐐瑰嚮
+  const handleMerge = () => {
+    if (selectedRows.value.length === 0) {
+      ElMessage.warning("璇烽�夋嫨瑕佸悎骞朵笅鍙戠殑鐢熶骇璁″垝");
+      return;
+    }
+    console.log(selectedRows.value);
+    // 妫�鏌ュ己搴︿竴鑷存��
+    const firstRow = selectedRows.value[0];
+    const productName = firstRow.productName || "";
+    let strengthConsistent = true;
+    let firstStrength = firstRow.strength || "";
+    strengthError.value = "";
+
+    if (productName === "鐮屽潡") {
+      for (const row of selectedRows.value) {
+        if (row.strength !== firstStrength) {
+          strengthConsistent = false;
+          break;
+        }
+      }
+
+      if (!strengthConsistent) {
+        strengthError.value = "閫夋嫨鐨勭爩鍧楀己搴︿笉涓�鑷达紝璇烽噸鏂伴�夋嫨";
+      }
+    }
+
+    // 璁$畻鎬诲埗閫犳暟閲�
+    const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => {
+      return (
+        sum +
+        (row.volume == null
+          ? 0
+          : Number(Number(row.volume) - Number(row.assignedQuantity).toFixed(4)))
+      );
+    }, 0);
+    sumAssignedQuantity.value = totalAssignedQuantity;
+    console.log(totalAssignedQuantity);
+    // 璁剧疆琛ㄥ崟鏁版嵁
+    mergeForm.materialCode = selectedserialNo.value;
+    mergeForm.productName = productName;
+    mergeForm.model = firstRow.model || "";
+    mergeForm.length = firstRow.length || 0;
+    mergeForm.width = firstRow.width || 0;
+    mergeForm.height = firstRow.height || 0;
+    mergeForm.totalAssignedQuantity = totalAssignedQuantity;
+    mergeForm.planCompleteTime = firstRow.planCompleteTime || "";
+    mergeForm.productMaterialId = firstRow.productMaterialId || "";
+    mergeForm.strength = firstStrength;
+    mergeForm.ids = selectedRows.value.map(row => row.id);
+
+    // 鎵撳紑寮圭獥
+    isShowNewModal.value = true;
+  };
+
+  // 澶勭悊鍚堝苟涓嬪彂鎻愪氦
+  const handleMergeSubmit = () => {
+    if (mergeForm.totalAssignedQuantity === 0) {
+      ElMessage.warning("璇疯緭鍏ョ敓浜ф柟鏁�");
+      return;
+    }
+    // 楠岃瘉鐮屽潡浜у搧鐨勫己搴�
+    if (mergeForm.productName === "鐮屽潡" && !mergeForm.strength) {
+      ElMessage.error("鐮屽潡浜у搧鐨勫己搴︿负蹇呭~椤�");
+      return;
+    }
+    if (mergeForm.productName != "鐮屽潡") {
+      mergeForm.strength = "";
+    }
+    console.log(sumAssignedQuantity.value, "sumAssignedQuantity");
+    // 璁$畻褰撳墠閫変腑琛岀殑鎬绘柟鏁�
+    const totalVolume = selectedRows.value.reduce((sum, row) => {
+      return sum + (Number(row.volume) - Number(row.assignedQuantity) || 0);
+    }, 0);
+
+    // 楠岃瘉totalAssignedQuantity涓嶈兘澶т簬鎬绘柟鏁�
+    if (mergeForm.totalAssignedQuantity > sumAssignedQuantity.value) {
+      ElMessage.error("鐢熶骇鏂规暟涓嶈兘澶т簬褰撳墠璁$畻鐨勬�诲��");
+      return;
+    }
+
+    console.log(mergeForm, "mergeForm");
+    const strengthItem = block_strength.value.find(
+      item => item.id === mergeForm.strength
+    );
+    const payload = {
+      ...mergeForm,
+      strength: strengthItem ? strengthItem.label : mergeForm.strength,
+    };
+    productionPlanCombine(payload)
+      .then(res => {
+        if (res.code === 200) {
+          ElMessage.success("涓嬪彂鎴愬姛");
+          getList();
+          isShowNewModal.value = false;
+          // 鍙互閫夋嫨鍒锋柊鍒楄〃鎴栧叾浠栨搷浣�
+          getList();
+        } else {
+          ElMessage.error(res.message || "涓嬪彂澶辫触");
+        }
+      })
+      .catch(err => {
+        console.error("鍚堝苟涓嬪彂寮傚父锛�", err);
+        ElMessage.error("绯荤粺寮傚父锛屽悎骞朵笅鍙戝け璐�");
+      });
+    // 鍙互閫夋嫨鍒锋柊鍒楄〃鎴栧叾浠栨搷浣�
+  };
+
+  // 瀵煎叆
+  const handleImport = () => {
+    importDialogVisible.value = true;
+  };
+
+  // 瀵煎嚭
+  const handleExport = () => {
+    const fileName = `鐢熶骇璁″垝.xlsx`;
+    exportProductionPlan()
+      .then(res => {
+        // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+        if (!res) {
+          proxy.$modal.msgError("瀵煎嚭澶辫触锛岃繑鍥炴暟鎹负绌�");
+          return;
+        }
+
+        const blob = new Blob([res], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        const downloadElement = document.createElement("a");
+        const href = window.URL.createObjectURL(blob);
+
+        downloadElement.style.display = "none";
+        downloadElement.href = href;
+        downloadElement.download = fileName;
+
+        document.body.appendChild(downloadElement);
+        downloadElement.click();
+
+        document.body.removeChild(downloadElement);
+        window.URL.revokeObjectURL(href);
+
+        proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+      })
+      .catch(err => {
+        console.error("瀵煎嚭寮傚父锛�", err);
+        proxy.$modal.msgError("绯荤粺寮傚父锛屽鍑哄け璐�");
+      });
+  };
+
+  // 瀵煎叆鎴愬姛
+  const handleImportSuccess = response => {
+    if (response.code === 200) {
+      ElMessage.success("瀵煎叆鎴愬姛");
+      importDialogVisible.value = false;
+      getList();
+    } else {
+      ElMessage.error(response.msg || "瀵煎叆澶辫触");
+    }
+  };
+
+  // 瀵煎叆澶辫触
+  const handleImportError = error => {
+    ElMessage.error("瀵煎叆澶辫触锛岃妫�鏌ユ枃浠舵牸寮忔槸鍚︽纭�");
+  };
+
+  // 纭瀵煎叆
+  const handleImportConfirm = () => {
+    if (importDialogRef.value) {
+      importDialogRef.value.submit();
+    }
+  };
+
+  // 涓嬭浇妯℃澘
+  const handleDownloadTemplate = () => {
+    proxy.download(
+      "/productionPlan/downloadTemplate",
+      {},
+      "鐢熶骇璁″垝瀵煎叆妯℃澘.xlsx"
+    );
+  };
+
+  // 鍏抽棴瀵煎叆寮圭獥
+  const handleImportClose = () => {
+    importDialogVisible.value = false;
+  };
+
+  // 鏂板
+  const handleAdd = () => {
+    operationType.value = "add";
+    Object.assign(form, {
+      applyNo: "",
+      customerName: "",
+      productName: "",
+      productMaterialId: undefined,
+      productMaterialSkuId: undefined,
+      model: "",
+      materialCode: "",
+      quantity: 0,
+      volume: 0,
+      length: 0,
+      width: 0,
+      height: 0,
+      startDate: "",
+      endDate: "",
+      strength: "",
+      remarkOne: "",
+      remarkTwo: "",
+    });
+    dialogVisible.value = true;
+    fetchProductOptions();
+  };
+
+  // 缂栬緫
+  const handleEdit = row => {
+    operationType.value = "edit";
+    Object.assign(form, {
+      id: row.id,
+      applyNo: row.applyNo || "",
+      customerName: row.customerName || "",
+      productName: row.productName || "",
+      productMaterialId: row.productMaterialId || undefined,
+      productMaterialSkuId: row.productMaterialSkuId || undefined,
+      model: row.model || "",
+      materialCode: row.materialCode || "",
+      quantity: row.quantity || 0,
+      volume: row.volume || 0,
+      length: row.length || 0,
+      width: row.width || 0,
+      height: row.height || 0,
+      startDate: row.startDate || "",
+      endDate: row.endDate || "",
+      strength: row.strength || "",
+      remarkOne: row.remarkOne || "",
+      remarkTwo: row.remarkTwo || "",
+    });
+    dialogVisible.value = true;
+    fetchProductOptions();
+    fetchSpecificationOptions(row.productMaterialId);
+  };
+
+  // 鍒犻櫎
+  const handleDelete = row => {
+    proxy.$modal
+      .confirm("纭鍒犻櫎璇ョ敓浜ц鍒掞紵", "鎻愮ず", {
+        confirmButtonText: "纭",
+        cancelButtonText: "鍙栨秷",
+        type: "warning",
+      })
+      .then(() => {
+        productionPlanDelete([row.id])
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {
+            proxy.$modal.msgError("鍒犻櫎澶辫触");
+          });
+      })
+      .catch(() => {});
+  };
+
+  // 鎻愪氦琛ㄥ崟
+  const handleSubmit = () => {
+    formRef.value.validate(valid => {
+      if (valid) {
+        if (form.volume === 0) {
+          proxy.$modal.msgError("鏂规暟涓嶈兘涓�0");
+          return;
+        }
+        if (form.v === "add") {
+          payload.id = null;
+        }
+        const payload = { ...form };
+        if (operationType.value === "add") {
+          payload.id = null;
+          productionPlanAdd(payload)
+            .then(() => {
+              proxy.$modal.msgSuccess(
+                operationType.value === "add" ? "鏂板鎴愬姛" : "淇敼鎴愬姛"
+              );
+              dialogVisible.value = false;
+              getList();
+            })
+            .catch(() => {
+              proxy.$modal.msgError(
+                operationType.value === "add" ? "鏂板澶辫触" : "淇敼澶辫触"
+              );
+            });
+        }
+        if (operationType.value === "edit") {
+          productionPlanUpdate(payload)
+            .then(() => {
+              proxy.$modal.msgSuccess(
+                operationType.value === "add" ? "鏂板鎴愬姛" : "淇敼鎴愬姛"
+              );
+              dialogVisible.value = false;
+              getList();
+            })
+            .catch(() => {
+              proxy.$modal.msgError(
+                operationType.value === "add" ? "鏂板澶辫触" : "淇敼澶辫触"
+              );
+            });
+        }
+      }
+    });
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped lang="scss">
+  .app-container {
+    padding: 24px;
+    background-color: #f0f2f5;
+    min-height: calc(100vh - 48px);
+  }
+
+  .search_form {
+    // 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);
+    }
+  }
+
+  .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;
+    border-radius: 6px;
+    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) {
+    border: none;
+    border-radius: 6px;
+    overflow: hidden;
+    box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
+
+    .el-table__header-wrapper {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+
+      th {
+        background: transparent;
+        font-weight: 600;
+        color: #ffffff;
+        border-bottom: none;
+        padding: 16px 0;
+        letter-spacing: 0.5px;
+      }
+    }
+
+    .el-table__body-wrapper {
+      tr {
+        transition: all 0.3s ease;
+
+        &:hover {
+          background: linear-gradient(
+            90deg,
+            rgba(102, 126, 234, 0.05) 0%,
+            rgba(118, 75, 162, 0.05) 100%
+          );
+          transform: scale(1.002);
+          box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
+        }
+
+        td {
+          border-bottom: 1px solid #f0f0f0;
+          padding: 14px 0;
+          color: #303133;
+        }
+      }
+
+      tr.current-row {
+        background: linear-gradient(
+          90deg,
+          rgba(102, 126, 234, 0.08) 0%,
+          rgba(118, 75, 162, 0.08) 100%
+        );
+      }
+
+      // 鏁板�煎瓧娈垫牱寮�
+      .quantity-cell,
+      .volume-cell,
+      .dimension-cell {
+        font-weight: 600;
+        color: #409eff;
+        font-family: "Courier New", monospace;
+        text-shadow: 0 1px 2px rgba(64, 158, 255, 0.2);
+      }
+
+      // 瑙勬牸瀛楁鏍峰紡
+      .spec-cell {
+        color: #67c23a;
+        font-weight: 500;
+
+        padding: 4px 8px;
+        border-radius: 4px;
+      }
+
+      // 缂栫爜瀛楁鏍峰紡
+      .code-cell {
+        color: #e6a23c;
+        font-family: "Courier New", monospace;
+        font-weight: 500;
+        padding: 4px 8px;
+        border-radius: 4px;
+      }
+
+      // 鏃ユ湡瀛楁鏍峰紡
+      .date-cell {
+        color: #909399;
+        font-style: italic;
+      }
+
+      // 鐘舵�佹爣绛炬牱寮�
+      .status-tag {
+        &.pending {
+          background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
+          color: #d63031;
+          padding: 4px 12px;
+          border-radius: 12px;
+          font-weight: 500;
+          box-shadow: 0 2px 4px rgba(253, 203, 110, 0.3);
+        }
+
+        &.processing {
+          background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
+          color: #ffffff;
+          padding: 4px 12px;
+          border-radius: 12px;
+          font-weight: 500;
+          box-shadow: 0 2px 4px rgba(9, 132, 227, 0.3);
+        }
+
+        &.completed {
+          background: linear-gradient(135deg, #55efc4 0%, #00b894 100%);
+          color: #ffffff;
+          padding: 4px 12px;
+          border-radius: 12px;
+          font-weight: 500;
+          box-shadow: 0 2px 4px rgba(0, 184, 148, 0.3);
+        }
+      }
+    }
+
+    .el-table__empty-block {
+      padding: 60px 0;
+      background-color: #fafafa;
+    }
+  }
+
+  // 鎿嶄綔鎸夐挳鏍峰紡
+  :deep(.el-table .cell .el-button--text) {
+    padding: 6px 10px;
+    border-radius: 4px;
+    transition: all 0.3s ease;
+    font-weight: 500;
+
+    &:hover {
+      background-color: rgba(64, 158, 255, 0.1);
+      transform: translateY(-1px);
+      box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
+    }
+
+    &:nth-of-type(1) {
+      color: #409eff;
+      background: linear-gradient(
+        135deg,
+        rgba(64, 158, 255, 0.1) 0%,
+        rgba(64, 158, 255, 0.05) 100%
+      );
+    }
+
+    &:nth-of-type(2) {
+      color: #67c23a;
+      background: linear-gradient(
+        135deg,
+        rgba(103, 194, 58, 0.1) 0%,
+        rgba(103, 194, 58, 0.05) 100%
+      );
+    }
+  }
+
+  // 淇℃伅灞曠ず鏍峰紡
+  .info-display {
+    border-radius: 6px;
+    color: #303133;
+    font-size: 14px;
+    min-height: 32px;
+    display: flex;
+    align-items: center;
+  }
+
+  .pagination-container {
+    display: flex;
+    justify-content: flex-end;
+    padding: 16px 20px;
+    background-color: #ffffff;
+    border-top: 1px solid #ebeef5;
+    border-radius: 0 0 12px 12px;
+  }
+
+  :deep(.el-button) {
+    transition: all 0.3s ease;
+
+    &:hover {
+      transform: translateY(-1px);
+    }
+  }
+
+  :deep(.el-dialog) {
+    border-radius: 6px;
+    overflow: hidden;
+
+    .el-dialog__header {
+      background-color: #fafafa;
+      border-bottom: 1px solid #ebeef5;
+      padding: 20px 24px;
+
+      .el-dialog__title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+
+    .el-dialog__body {
+      padding: 24px;
+    }
+
+    .el-dialog__footer {
+      padding: 16px 24px;
+      border-top: 1px solid #ebeef5;
+      background-color: #fafafa;
+    }
+  }
+
+  :deep(.el-form) {
+    .el-form-item {
+      margin-bottom: 20px;
+
+      .el-form-item__label {
+        font-weight: 500;
+        color: #303133;
+      }
+
+      .el-input,
+      .el-select,
+      .el-date-picker,
+      .el-input-number {
+        width: 100%;
+
+        // .el-input__inner {
+        //   border-radius: 6px;
+        //   border: 1px solid #dcdfe6;
+        //   transition: all 0.3s ease;
+
+        //   &:focus {
+        //     border-color: #409eff;
+        //     box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+        //   }
+        // }
+      }
+    }
+  }
+
+  :deep(.el-tag) {
+    border-radius: 4px;
+    padding: 2px 8px;
+    font-size: 12px;
+  }
+
+  @media (max-width: 768px) {
+    .app-container {
+      padding: 16px;
+    }
+
+    .search_form {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+
+      .el-form {
+        width: 100%;
+
+        .el-form-item {
+          width: 100%;
+        }
+      }
+
+      > div {
+        width: 100%;
+        display: flex;
+        gap: 12px;
+
+        .el-button {
+          flex: 1;
+        }
+      }
+    }
+
+    :deep(.el-table) {
+      th,
+      td {
+        padding: 10px 0;
+        font-size: 12px;
+      }
+    }
+
+    :deep(.el-dialog) {
+      width: 90% !important;
+      margin: 20px auto !important;
+    }
+  }
+  .consumption-value {
+    font-weight: bold;
+    color: #409eff;
+  }
+
+  .consumption-unit {
+    font-size: 12px;
+    color: #909399;
+    margin-left: 4px;
+  }
+  // .search_form {
+  //   :deep(.el-form-item) {
+  //     margin-bottom: 0px !important;
+  //   }
+  // }
+  :deep(.el-table .el-table__body-wrapper tr td) {
+    background-color: #fff;
+  }
+</style>

--
Gitblit v1.9.3