Browse Source

feat(forecast-summary): 添加导出Excel模板功能并优化样式

yz 1 week ago
parent
commit
c5827ab897

+ 7 - 3
src/api/forecast/forecast-summary.js

@@ -304,10 +304,13 @@ export const batchSaveSalesForecastSummary = async (data) => {
  * 下载“销售预测汇总”导入模板(Excel)
  * 对应后端:GET /api/blade-factory/api/factory/salesForecastSummary/exportTemplate
  * 响应为二进制流(Blob),非 JSON;响应头包含 Content-Disposition、Content-Type 等
+ * @param {number} [current=1] - 当前页码(与列表接口保持一致,后端可忽略)
+ * @param {number} [size=10] - 每页数量(与列表接口保持一致,后端可忽略)
+ * @param {import('./types').SalesForecastMainListQueryParams} [params={}] - 查询条件(year、month、customerName)
  * @returns {Promise<SalesForecastTemplateResponse>} Axios Blob 响应,data 为 Excel 二进制
  * @example
  * // 前端触发下载示例:
- * const res = await exportSalesForecastTemplate()
+ * const res = await exportSalesForecastTemplate(1, 10, { year: 2025, month: 9, customerName: '库比森' })
  * const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
  * const url = window.URL.createObjectURL(blob)
  * const a = document.createElement('a')
@@ -316,10 +319,11 @@ export const batchSaveSalesForecastSummary = async (data) => {
  * a.click()
  * window.URL.revokeObjectURL(url)
  */
-export const exportSalesForecastTemplate = async () => {
+export const exportSalesForecastTemplate = async (current = 1, size = 10, params = {}) => {
   return request({
     url: '/api/blade-factory/api/factory/salesForecastSummary/exportTemplate',
     method: 'get',
-    responseType: 'blob'
+    responseType: 'blob',
+    params: { current, size, ...params }
   })
 }

+ 18 - 1
src/views/forecast-summary/index.scss

@@ -210,4 +210,21 @@
 
   .expand-body {
     margin-top: 12px;
-  }
+  }
+
+  .forecast-summary .toolbar {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 12px;
+    .el-button + .el-button {
+      margin-left: 8px;
+    }
+  }
+
+@media screen and (max-width: 768px) {
+  .forecast-summary {
+    .toolbar {
+      margin-bottom: 10px;
+    }
+  }
+}

+ 14 - 0
src/views/forecast-summary/index.vue

@@ -17,6 +17,20 @@
       @refresh-change="refreshChange"
       @on-load="onLoad"
     >
+      <!-- 顶部菜单左侧:导出按钮(仿 avue 原生按钮样式) -->
+      <template slot="menuLeft">
+        <el-button
+          type="primary"
+          size="small"
+          icon="el-icon-download"
+          style="margin-left: 10px;"
+          :loading="loading"
+          @click="onExportTemplate"
+        >
+          导出Excel
+        </el-button>
+      </template>
+
       <!-- 自定义审批状态列 -->
       <template slot="approvalStatus" slot-scope="{row}">
         <el-tag

+ 508 - 448
src/views/forecast-summary/summaryIndex.js

@@ -7,7 +7,7 @@
  * @typedef {import('./types').ForecastSummaryComponent} ForecastSummaryComponent
  */
 
-import { getSalesForecastMainList, getForecastSummaryDetail } from '@/api/forecast/forecast-summary'
+import { getSalesForecastMainList, getForecastSummaryDetail, exportSalesForecastTemplate } from '@/api/forecast/forecast-summary'
 import {
   APPROVAL_STATUS,
   APPROVAL_STATUS_CONFIG,
@@ -27,450 +27,510 @@ import { mapGetters } from 'vuex'
  * @description 提供预测汇总的查询、分页、表格配置等功能
  * @this {ForecastSummaryComponent & Vue}
  */
-export default {
-  data() {
-    return {
-      /**
-       * 表单数据
-       * @type {Record<string, any>}
-       */
-      form: {},
-
-      /**
-       * 查询参数
-       * @type {typeof DEFAULT_FORECAST_SUMMARY_QUERY}
-       */
-      query: { ...DEFAULT_FORECAST_SUMMARY_QUERY },
-
-      /**
-       * 加载状态
-       * @type {boolean}
-       */
-      loading: true,
-
-      /**
-       * 分页配置
-       * @type {PageConfig}
-       */
-      page: {
-        pageSize: 10,
-        currentPage: 1,
-        total: 0
-      },
-
-      /**
-       * 表格数据(主表记录)
-       * @type {Array<SalesForecastMainRecord>}
-       */
-      data: [],
-
-      /**
-       * 选中的行数据(主表记录)
-       * @type {Array<SalesForecastMainRecord>}
-       */
-      selectionList: [],
-
-      /**
-       * avue表格配置
-       * @type {AvueCrudOption}
-       */
-      option: {
-        height: 'auto',
-        calcHeight: 30,
-        tip: false,
-        searchShow: true,
-        searchMenuSpan: 6,
-        border: true,
-        index: true,
-        viewBtn: false,
-        editBtn: false,
-        delBtn: false,
-        addBtn: false,
-        selection: false,
-        dialogClickModal: false,
-        menu: false,
-        // 启用行展开,展示子表格
-        expand: true,
-        expandRowKeys: [],
-        defaultExpandAll: false,
-        column: [
-          {
-            label: '年月',
-            prop: 'yearMonth',
-            minWidth: 100,
-            sortable: true,
-            slot: true,
-            search: true,
-            searchSpan: 6,
-            type: 'month',
-            format: 'yyyy-MM',
-            valueFormat: 'yyyy-MM',
-            searchslot: false,
-            hide: false
-          },
-          {
-            label: '年份',
-            prop: 'year',
-            minWidth: 80,
-            type: 'year',
-            format: 'yyyy',
-            valueFormat: 'yyyy',
-            search: true,
-            searchSpan: 6,
-            hide: true
-          },
-          {
-            label: '月份',
-            prop: 'month',
-            minWidth: 80,
-            type: 'select',
-            dicData: MONTH_OPTIONS,
-            search: true,
-            searchSpan: 6,
-            hide: true
-          },
-          {
-            label: '客户编码',
-            prop: 'customerCode',
-            minWidth: 120,
-            search: true,
-            searchSpan: 6
-          },
-          {
-            label: '客户名称',
-            prop: 'customerName',
-            minWidth: 200,
-            search: true,
-            searchSpan: 6
-          },
-          {
-            label: '审批状态',
-            prop: 'approvalStatus',
-            minWidth: 100,
-            type: 'select',
-            dicData: APPROVAL_STATUS_OPTIONS,
-            search: true,
-            searchSpan: 6,
-            slot: true
-          },
-          {
-            label: '审批人',
-            prop: 'approvedName',
-            minWidth: 100,
-            overHidden: true
-          },
-          {
-            label: '审批时间',
-            prop: 'approvedTime',
-            minWidth: 160,
-            sortable: true,
-            slot: true
-          },
-          {
-            label: '创建时间',
-            prop: 'createTime',
-            minWidth: 160,
-            sortable: true,
-            slot: true
-          }
-        ]
-      },
-      
-      // 子表(展开区)表格配置:展示 pcBladeSalesForecastSummaryList
-      childOption: {
-        height: 'auto',
-        calcHeight: 0,
-        tip: false,
-        searchShow: false,
-        border: true,
-        index: true,
-        viewBtn: false,
-        editBtn: false,
-        delBtn: false,
-        addBtn: false,
-        selection: false,
-        menu: false,
-        expand: false,
-        column: [
-          { label: '商品编码', prop: 'itemCode', minWidth: 120 },
-          { label: '商品名称', prop: 'itemName', minWidth: 180, overHidden: true },
-          { label: '规格型号', prop: 'specs', minWidth: 140, overHidden: true },
-          { label: '花型/图案', prop: 'pattern', minWidth: 120, overHidden: true },
-          { label: '品牌名称', prop: 'brandName', minWidth: 120, overHidden: true },
-          { label: '预测数量', prop: 'forecastQuantity', minWidth: 120, align: 'right', slot: true }
-        ]
-      }
-    }
-  },
-
-  computed: {
-    ...mapGetters(['permission']),
-
-    /**
-     * 权限列表
-     * @returns {Object} 权限配置对象
-     */
-    permissionList() {
-      return {
-        addBtn: false,
-        viewBtn: true,
-        editBtn: false,
-        delBtn: false
-      }
-    },
-
-    /**
-     * 是否为只读模式
-     * @returns {boolean} 只读模式标识
-     */
-    readOnlyMode() {
-      return isReadOnlyMode()
-    }
-  },
-
-  /**
-   * 组件创建时初始化数据
-   */
-  created() {
-    this.onLoad(this.page)
-  },
-
-  methods: {
-    /**
-     * 获取审批状态配置
-     * @param {number} status - 审批状态值
-     * @returns {{label: string, type: string}} 状态配置对象
-     */
-    getApprovalStatusConfig(status) {
-      const config = APPROVAL_STATUS_CONFIG[status]
-      if (!config) {
-        return {
-          label: '未知状态',
-          type: 'info'
-        }
-      }
-      return {
-        label: config.label,
-        type: config.type
-      }
-    },
-
-    /**
-     * 格式化数字显示
-     * @param {string|number} value - 数字值
-     * @returns {string} 格式化后的数字字符串
-     */
-    formatNumber(value) {
-      return formatNumber(value)
-    },
-
-    /**
-     * 格式化日期时间
-     * @param {string} dateTime - 日期时间字符串
-     * @returns {string} 格式化后的日期时间
-     */
-    formatDateTime(dateTime) {
-      return formatDateTime(dateTime)
-    },
-
-    /**
-     * 格式化年月显示
-     * @param {number} year - 年份
-     * @param {number} month - 月份
-     * @returns {string} 格式化后的年月字符串
-     */
-    formatYearMonth(year, month) {
-      return formatYearMonth(year, month)
-    },
-
-    /**
-     * 搜索条件变化处理
-     * @this {ForecastSummaryComponent & Vue}
-     * @param {Record<string, any>} params - 搜索参数
-     * @param {() => void} done - 完成回调
-     */
-    searchChange(params, done) {
-      // 处理年月字段拆分
-      const processedParams = { ...params }
-
-      // 如果有yearMonth字段,拆分为year和month
-      if (processedParams.yearMonth) {
-        const yearMonthValue = processedParams.yearMonth
-        if (yearMonthValue) {
-          // 解析年月格式 (yyyy-MM)
-          const [year, month] = yearMonthValue.split('-')
-          if (year) {
-            processedParams.year = parseInt(year, 10)
-          }
-          if (month) {
-            processedParams.month = parseInt(month, 10)
-          }
-        }
-        // 删除原始的yearMonth字段,避免传递到后端
-        delete processedParams.yearMonth
-      }
-
-      this.query = {
-        ...this.query,
-        ...processedParams
-      }
-      this.onLoad(this.page, processedParams)
-      done()
-    },
-
-    /**
-     * 搜索重置处理
-     * @this {ForecastSummaryComponent & Vue}
-     */
-    searchReset() {
-      this.query = { ...DEFAULT_FORECAST_SUMMARY_QUERY }
-      this.onLoad(this.page)
-    },
-
-    /**
-     * 选择变化处理
-     * @this {ForecastSummaryComponent & Vue}
-     * @param {Array<SalesForecastMainRecord>} selection - 选中的行数据
-     */
-    selectionChange(selection) {
-      this.selectionList = selection
-    },
-
-    /**
-     * 当前页变化处理
-     * @this {ForecastSummaryComponent & Vue}
-     * @param {number} currentPage - 当前页码
-     */
-    currentChange(currentPage) {
-      this.page.currentPage = currentPage
-    },
-
-    /**
-     * 页大小变化处理
-     * @this {ForecastSummaryComponent & Vue}
-     * @param {number} pageSize - 页大小
-     */
-    sizeChange(pageSize) {
-      this.page.pageSize = pageSize
-    },
-
-    /**
-     * 刷新处理
-     * @this {ForecastSummaryComponent & Vue}
-     */
-    refreshChange() {
-      this.onLoad(this.page, this.query)
-    },
-
-    /**
-     * 弹窗打开前处理
-     * @param {Function} done - 完成回调
-     * @param {string} type - 操作类型
-     */
-    beforeOpen(done, type) {
-      // 由于是只读模式,不需要处理弹窗
-      done()
-    },
-
-    /**
-     * 加载数据
-     * @this {ForecastSummaryComponent & Vue}
-     * @param {PageConfig} page - 分页参数
-     * @param {Record<string, any>} params - 查询参数
-     * @returns {Promise<void>}
-     */
-    async onLoad(page, params = {}) {
-      this.loading = true
-      try {
-        // 仅挑选 main-list 支持的查询参数,避免无效字段影响
-        const merged = { ...this.query, ...params }
-        const safeParams = {
-          year: merged.year,
-          month: merged.month,
-          customerName: merged.customerName
-        }
-
-        const response = await getSalesForecastMainList(
-          page.currentPage || 1,
-          page.pageSize || 10,
-          safeParams
-        )
-
-        if (response && response.data && response.data.code === 200 && response.data.data) {
-          const pageData = response.data.data
-          const records = Array.isArray(pageData.records) ? pageData.records : []
-          // 字段类型转换与BigInt处理
-          this.data = records.map(r => {
-            const children = Array.isArray(r.pcBladeSalesForecastSummaryList)
-              ? r.pcBladeSalesForecastSummaryList.map(it => ({
-                  ...it,
-                  idBigint: this.safeBigInt(it.id),
-                  forecastMainIdBigint: it.forecastMainId != null ? this.safeBigInt(it.forecastMainId) : null,
-                  year: Number(it.year),
-                  month: Number(it.month),
-                  forecastQuantity: typeof it.forecastQuantity === 'string' ? Number(it.forecastQuantity) : Number(it.forecastQuantity)
-                }))
-              : []
-            return {
-              ...r,
-              idBigint: this.safeBigInt(r.id),
-              year: Number(r.year),
-              month: Number(r.month),
-              pcBladeSalesForecastSummaryList: children
-            }
-          })
-          this.page.total = Number(pageData.total) || 0
-          this.page.currentPage = Number(pageData.current) || 1
-          this.page.pageSize = Number(pageData.size) || 10
-        } else {
-          this.$message.error(response?.data?.message || '获取数据失败')
-          this.data = []
-          this.page.total = 0
-        }
-      } catch (error) {
-        console.error('获取预测主表分页列表失败:', error)
-        this.$message.error('获取数据失败,请稍后重试')
-        this.data = []
-        this.page.total = 0
-      } finally {
-        this.loading = false
-      }
-    },
-
-    /**
-     * 安全将值转换为BigInt
-     * @param {string|number|null|undefined} v
-     * @returns {bigint|null}
-     */
-    safeBigInt(v) {
-      try {
-        if (v === null || v === undefined || v === '') return null
-        return BigInt(v)
-      } catch (e) {
-        return null
-      }
-    },
-
-    /**
-     * 获取预测汇总详情
-     * @this {ForecastSummaryComponent & Vue}
-     * @param {string|number} id - 预测汇总ID
-     * @returns {Promise<import('@/api/forecast/types').ForecastSummaryRecord|null>} 详情数据
-     */
-    async getForecastSummaryDetail(id) {
-      try {
-        const response = await getForecastSummaryDetail(id)
-        if (response && response.data && response.data.code === 200) {
-          return response.data.data
-        } else {
-          this.$message.error(response.data.message || '获取详情失败')
-          return null
-        }
-      } catch (error) {
-        console.error('获取预测汇总详情失败:', error)
-        this.$message.error('获取详情失败,请稍后重试')
-        return null
-      }
-    }
-  }
-}
+ export default {
+   data() {
+     return {
+       /**
+        * 表单数据
+        * @type {Record<string, any>}
+        */
+       form: {},
+
+       /**
+        * 查询参数
+        * @type {typeof DEFAULT_FORECAST_SUMMARY_QUERY}
+        */
+       query: { ...DEFAULT_FORECAST_SUMMARY_QUERY },
+
+       /**
+        * 加载状态
+        * @type {boolean}
+        */
+       loading: true,
+
+       /**
+        * 分页配置
+        * @type {PageConfig}
+        */
+       page: {
+         pageSize: 10,
+         currentPage: 1,
+         total: 0
+       },
+
+       /**
+        * 表格数据(主表记录)
+        * @type {Array<SalesForecastMainRecord>}
+        */
+       data: [],
+
+       /**
+        * 选中的行数据(主表记录)
+        * @type {Array<SalesForecastMainRecord>}
+        */
+       selectionList: [],
+
+       /**
+        * avue表格配置
+        * @type {AvueCrudOption}
+        */
+       option: {
+         height: 'auto',
+         calcHeight: 30,
+         tip: false,
+         searchShow: true,
+         searchMenuSpan: 6,
+         border: true,
+         index: true,
+         viewBtn: false,
+         editBtn: false,
+         delBtn: false,
+         addBtn: false,
+         selection: false,
+         dialogClickModal: false,
+         menu: false,
+         // 启用行展开,展示子表格
+         expand: true,
+         expandRowKeys: [],
+         defaultExpandAll: false,
+         column: [
+           {
+             label: '年月',
+             prop: 'yearMonth',
+             minWidth: 100,
+             sortable: true,
+             slot: true,
+             search: true,
+             searchSpan: 6,
+             type: 'month',
+             format: 'yyyy-MM',
+             valueFormat: 'yyyy-MM',
+             searchslot: false,
+             hide: false
+           },
+           {
+             label: '年份',
+             prop: 'year',
+             minWidth: 80,
+             type: 'year',
+             format: 'yyyy',
+             valueFormat: 'yyyy',
+             search: true,
+             searchSpan: 6,
+             hide: true
+           },
+           {
+             label: '月份',
+             prop: 'month',
+             minWidth: 80,
+             type: 'select',
+             dicData: MONTH_OPTIONS,
+             search: true,
+             searchSpan: 6,
+             hide: true
+           },
+           {
+             label: '客户编码',
+             prop: 'customerCode',
+             minWidth: 120,
+             search: true,
+             searchSpan: 6
+           },
+           {
+             label: '客户名称',
+             prop: 'customerName',
+             minWidth: 200,
+             search: true,
+             searchSpan: 6
+           },
+           {
+             label: '审批状态',
+             prop: 'approvalStatus',
+             minWidth: 100,
+             type: 'select',
+             dicData: APPROVAL_STATUS_OPTIONS,
+             search: true,
+             searchSpan: 6,
+             slot: true
+           },
+           {
+             label: '审批人',
+             prop: 'approvedName',
+             minWidth: 100,
+             overHidden: true
+           },
+           {
+             label: '审批时间',
+             prop: 'approvedTime',
+             minWidth: 160,
+             sortable: true,
+             slot: true
+           },
+           {
+             label: '创建时间',
+             prop: 'createTime',
+             minWidth: 160,
+             sortable: true,
+             slot: true
+           }
+         ]
+       },
+       
+       // 子表(展开区)表格配置:展示 pcBladeSalesForecastSummaryList
+       childOption: {
+         height: 'auto',
+         calcHeight: 0,
+         tip: false,
+         searchShow: false,
+         border: true,
+         index: true,
+         viewBtn: false,
+         editBtn: false,
+         delBtn: false,
+         addBtn: false,
+         selection: false,
+         menu: false,
+         expand: false,
+         column: [
+           { label: '商品编码', prop: 'itemCode', minWidth: 120 },
+           { label: '商品名称', prop: 'itemName', minWidth: 180, overHidden: true },
+           { label: '规格型号', prop: 'specs', minWidth: 140, overHidden: true },
+           { label: '花型/图案', prop: 'pattern', minWidth: 120, overHidden: true },
+           { label: '品牌名称', prop: 'brandName', minWidth: 120, overHidden: true },
+           { label: '预测数量', prop: 'forecastQuantity', minWidth: 120, align: 'right', slot: true }
+         ]
+       }
+     }
+   },
+
+   computed: {
+     ...mapGetters(['permission']),
+
+     /**
+      * 权限列表
+      * @returns {Object} 权限配置对象
+      */
+     permissionList() {
+       return {
+         addBtn: false,
+         viewBtn: true,
+         editBtn: false,
+         delBtn: false
+       }
+     },
+
+     /**
+      * 是否为只读模式
+      * @returns {boolean} 只读模式标识
+      */
+     readOnlyMode() {
+       return isReadOnlyMode()
+     }
+   },
+
+   /**
+    * 组件创建时初始化数据
+    */
+   created() {
+     this.onLoad(this.page)
+   },
+
+   methods: {
+     /**
+      * 获取审批状态配置
+      * @param {number} status - 审批状态值
+      * @returns {{label: string, type: string}} 状态配置对象
+      */
+     getApprovalStatusConfig(status) {
+       const config = APPROVAL_STATUS_CONFIG[status]
+       if (!config) {
+         return {
+           label: '未知状态',
+           type: 'info'
+         }
+       }
+       return {
+         label: config.label,
+         type: config.type
+       }
+     },
+
+     /**
+      * 格式化数字显示
+      * @param {string|number} value - 数字值
+      * @returns {string} 格式化后的数字字符串
+      */
+     formatNumber(value) {
+       return formatNumber(/** @type {number} */ (value))
+     },
+
+     /**
+      * 格式化日期时间
+      * @param {string} dateTime - 日期时间字符串
+      * @returns {string} 格式化后的日期时间
+      */
+     formatDateTime(dateTime) {
+       return formatDateTime(dateTime)
+     },
+
+     /**
+      * 格式化年月显示
+      * @param {number} year - 年份
+      * @param {number} month - 月份
+      * @returns {string} 格式化后的年月字符串
+      */
+     formatYearMonth(year, month) {
+       return formatYearMonth(year, month)
+     },
+
+     /**
+      * 导出“销售预测汇总”模板(使用当前查询条件)
+      * @this {ForecastSummaryComponent & Vue}
+      * @returns {Promise<void>}
+      */
+     async onExportTemplate() {
+       try {
+         this.loading = true
+         const page = this.page || { currentPage: 1, pageSize: 10 }
+         const merged = { ...this.query }
+         const safeParams = {
+           year: merged.year,
+           month: merged.month,
+           customerName: merged.customerName
+         }
+         const res = await exportSalesForecastTemplate(
+           page.currentPage || 1,
+           page.pageSize || 10,
+           safeParams
+         )
+         // 处理文件名
+         const disposition = res?.headers?.['content-disposition'] || res?.headers?.['Content-Disposition']
+         let filename = '销售预测汇总模板.xlsx'
+         // attachment; filename*=UTF-8''xxxx.xlsx 或 filename="xxxx.xlsx"
+         const plainMatch = /filename="?([^;\n"]+)"?/i.exec(disposition)
+         if (disposition) {
+           // attachment; filename*=UTF-8''xxxx.xlsx 或 filename="xxxx.xlsx"
+           const utf8Match = /filename\*=UTF-8''([^;\n]+)/i.exec(disposition)
+           const raw = utf8Match ? decodeURIComponent(utf8Match[1]) : (plainMatch ? plainMatch[1] : '')
+           if (raw) filename = raw
+           // 将下载日期追加到文件名(YYYYMMDD),便于区分下载时间
+           const now = new Date()
+           const y = now.getFullYear()
+           const m = String(now.getMonth() + 1).padStart(2, '0')
+           const d = String(now.getDate()).padStart(2, '0')
+           const ymd = `${y}${m}${d}`
+           const dotIndex = filename.lastIndexOf('.')
+           if (dotIndex > 0) {
+             filename = `${filename.slice(0, dotIndex)}_${ymd}${filename.slice(dotIndex)}`
+           } else {
+             filename = `${filename}_${ymd}`
+           }
+         }
+         const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+         const url = window.URL.createObjectURL(blob)
+         const a = document.createElement('a')
+         a.href = url
+         a.download = filename
+         document.body.appendChild(a)
+         a.click()
+         document.body.removeChild(a)
+         window.URL.revokeObjectURL(url)
+       } catch (e) {
+         console.error('导出模板失败:', e)
+         this.$message.error('导出失败,请稍后重试')
+       } finally {
+         this.loading = false
+       }
+     },
+
+     /**
+      * 搜索条件变化处理
+      * @this {ForecastSummaryComponent & Vue}
+      * @param {Record<string, any>} params - 搜索参数
+      * @param {() => void} done - 完成回调
+      */
+     searchChange(params, done) {
+       // 处理年月字段拆分
+       const processedParams = { ...params }
+
+       // 如果有yearMonth字段,拆分为year和month
+       if (processedParams.yearMonth) {
+         const yearMonthValue = processedParams.yearMonth
+         if (yearMonthValue) {
+           // 解析年月格式 (yyyy-MM)
+           const [year, month] = yearMonthValue.split('-')
+           if (year) {
+             processedParams.year = parseInt(year, 10)
+           }
+           if (month) {
+             processedParams.month = parseInt(month, 10)
+           }
+         }
+         // 删除原始的yearMonth字段,避免传递到后端
+         delete processedParams.yearMonth
+       }
+
+       this.query = {
+         ...this.query,
+         ...processedParams
+       }
+       this.onLoad(this.page, processedParams)
+       done()
+     },
+
+     /**
+      * 搜索重置处理
+      * @this {ForecastSummaryComponent & Vue}
+      */
+     searchReset() {
+       this.query = { ...DEFAULT_FORECAST_SUMMARY_QUERY }
+       this.onLoad(this.page)
+     },
+
+     /**
+      * 选择变化处理
+      * @this {ForecastSummaryComponent & Vue}
+      * @param {Array<SalesForecastMainRecord>} selection - 选中的行数据
+      */
+     selectionChange(selection) {
+       this.selectionList = selection
+     },
+
+     /**
+      * 当前页变化处理
+      * @this {ForecastSummaryComponent & Vue}
+      * @param {number} currentPage - 当前页码
+      */
+     currentChange(currentPage) {
+       this.page.currentPage = currentPage
+     },
+
+     /**
+      * 页大小变化处理
+      * @this {ForecastSummaryComponent & Vue}
+      * @param {number} pageSize - 页大小
+      */
+     sizeChange(pageSize) {
+       this.page.pageSize = pageSize
+     },
+
+     /**
+      * 刷新处理
+      * @this {ForecastSummaryComponent & Vue}
+      */
+     refreshChange() {
+       this.onLoad(this.page, this.query)
+     },
+
+     /**
+      * 弹窗打开前处理
+      * @param {Function} done - 完成回调
+      * @param {string} type - 操作类型
+      */
+     beforeOpen(done, type) {
+       // 由于是只读模式,不需要处理弹窗
+       done()
+     },
+
+     /**
+      * 加载数据
+      * @this {ForecastSummaryComponent & Vue}
+      * @param {PageConfig} page - 分页参数
+      * @param {Record<string, any>} params - 查询参数
+      * @returns {Promise<void>}
+      */
+     async onLoad(page, params = {}) {
+       this.loading = true
+       try {
+         // 仅挑选 main-list 支持的查询参数,避免无效字段影响
+         const merged = { ...this.query, ...params }
+         const safeParams = {
+           year: merged.year,
+           month: merged.month,
+           customerName: merged.customerName
+         }
+
+         const response = await getSalesForecastMainList(
+           page.currentPage || 1,
+           page.pageSize || 10,
+           safeParams
+         )
+
+         if (response && response.data && response.data.code === 200 && response.data.data) {
+           const pageData = response.data.data
+           const records = Array.isArray(pageData.records) ? pageData.records : []
+           // 字段类型转换与BigInt处理
+           this.data = records.map(r => {
+             const children = Array.isArray(r.pcBladeSalesForecastSummaryList)
+               ? r.pcBladeSalesForecastSummaryList.map(it => ({
+                   ...it,
+                   idBigint: this.safeBigInt(it.id),
+                   forecastMainIdBigint: it.forecastMainId != null ? this.safeBigInt(it.forecastMainId) : null,
+                   year: Number(it.year),
+                   month: Number(it.month),
+                   forecastQuantity: typeof it.forecastQuantity === 'string' ? Number(it.forecastQuantity) : Number(it.forecastQuantity)
+                 }))
+               : []
+             return {
+               ...r,
+               idBigint: this.safeBigInt(r.id),
+               year: Number(r.year),
+               month: Number(r.month),
+               pcBladeSalesForecastSummaryList: children
+             }
+           })
+           this.page.total = Number(pageData.total) || 0
+           this.page.currentPage = Number(pageData.current) || 1
+           this.page.pageSize = Number(pageData.size) || 10
+         } else {
+           this.$message.error(response?.data?.message || '获取数据失败')
+           this.data = []
+           this.page.total = 0
+         }
+       } catch (error) {
+         console.error('获取预测主表分页列表失败:', error)
+         this.$message.error('获取数据失败,请稍后重试')
+         this.data = []
+         this.page.total = 0
+       } finally {
+         this.loading = false
+       }
+     },
+
+     /**
+      * 安全将值转换为BigInt
+      * @param {string|number|null|undefined} v
+      * @returns {bigint|null}
+      */
+     safeBigInt(v) {
+       try {
+         if (v === null || v === undefined || v === '') return null
+         return BigInt(v)
+       } catch (e) {
+         return null
+       }
+     },
+
+     /**
+      * 获取预测汇总详情
+      * @this {ForecastSummaryComponent & Vue}
+      * @param {string|number} id - 预测汇总ID
+      * @returns {Promise<import('@/api/forecast/types').ForecastSummaryRecord|null>} 详情数据
+      */
+     async getForecastSummaryDetail(id) {
+       try {
+         const response = await getForecastSummaryDetail(id)
+         if (response && response.data && response.data.code === 200) {
+           return response.data.data
+         } else {
+           this.$message.error(response.data.message || '获取详情失败')
+           return null
+         }
+       } catch (error) {
+         console.error('获取预测汇总详情失败:', error)
+         this.$message.error('获取详情失败,请稍后重试')
+         return null
+       }
+     }
+   }
+ }