zhangwencui
2026-04-22 09b9bff70fe5d5d5121c1ca6ecbe5ab07da1949a
生产计划模块引入
已添加3个文件
已修改1个文件
2239 ■■■■■ 文件已修改
src/api/productionPlan/productionPlan.js 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/components/PIMTable.vue 470 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 1688 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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,
  });
}
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"
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>
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的id作为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;
      // è§£æžè§„格字符串获取长宽高
      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("正在拉取数据,请稍候...");
    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>