Browse Source

feat(forecast-summary): 添加行展开功能显示子表数据并优化类型定义

yz 1 month ago
parent
commit
78649afe38

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

@@ -175,4 +175,39 @@
     color: #909399;
     font-size: 13px;
   }
-}
+}
+
+  // 行展开区样式(复用订单页样式类名,便于统一风格)
+  .order-expand-content {
+    padding: 16px 24px;
+    background-color: #fafafa;
+    border-left: 3px solid #409eff;
+  }
+
+  .expand-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-bottom: 8px;
+    border-bottom: 1px solid #e4e7ed;
+
+    h4 {
+      margin: 0;
+      color: #303133;
+      font-size: 16px;
+      font-weight: 600;
+    }
+  }
+
+  .order-code {
+    color: #606266;
+    font-size: 14px;
+    background-color: #f0f2f5;
+    padding: 4px 8px;
+    border-radius: 4px;
+  }
+
+  .expand-body {
+    margin-top: 12px;
+  }

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

@@ -46,6 +46,27 @@
       <template slot="approvedTime" slot-scope="{row}">
         <span>{{ row.approvedTime ? formatDateTime(row.approvedTime) : '-' }}</span>
       </template>
+
+      <!-- 行展开插槽 - 显示主表对应的子表(pcBladeSalesForecastSummaryList) -->
+      <template slot="expand" slot-scope="{row}">
+        <div class="order-expand-content">
+          <div class="expand-header">
+            <h4>明细</h4>
+          </div>
+          <div class="expand-body">
+            <avue-crud
+              :option="childOption"
+              :data="row.pcBladeSalesForecastSummaryList || []"
+              :table-loading="false"
+            >
+              <!-- 子表中的预测数量右对齐显示复用主表slot -->
+              <template slot="forecastQuantity" slot-scope="{row: sub}">
+                <span>{{ formatNumber(sub.forecastQuantity) }}</span>
+              </template>
+            </avue-crud>
+          </div>
+        </div>
+      </template>
     </avue-crud>
   </basic-container>
 </template>

+ 70 - 68
src/views/forecast-summary/summaryIndex.js

@@ -2,7 +2,7 @@
 
 /**
  * @typedef {import('@/api/forecast/types').ForecastSummaryQueryParams} ForecastSummaryQueryParams
- * @typedef {import('@/api/forecast/types').SalesForecastMainListItemRecord} SalesForecastMainListItemRecord
+ * @typedef {import('@/api/forecast/types').SalesForecastMainRecord} SalesForecastMainRecord
  * @typedef {import('./types').PageConfig} PageConfig
  * @typedef {import('./types').ForecastSummaryComponent} ForecastSummaryComponent
  */
@@ -59,14 +59,14 @@ export default {
       },
 
       /**
-       * 表格数据
-       * @type {Array<SalesForecastMainListItemRecord>}
+       * 表格数据(主表记录)
+       * @type {Array<SalesForecastMainRecord>}
        */
       data: [],
 
       /**
-       * 选中的行数据
-       * @type {Array<SalesForecastMainListItemRecord>}
+       * 选中的行数据(主表记录)
+       * @type {Array<SalesForecastMainRecord>}
        */
       selectionList: [],
 
@@ -89,6 +89,10 @@ export default {
         selection: false,
         dialogClickModal: false,
         menu: false,
+        // 启用行展开,展示子表格
+        expand: true,
+        expandRowKeys: [],
+        defaultExpandAll: false,
         column: [
           {
             label: '年月',
@@ -135,59 +139,9 @@ export default {
           {
             label: '客户名称',
             prop: 'customerName',
-            minWidth: 180,
-            search: true,
-            searchSpan: 6,
-            overHidden: true
-          },
-          {
-            label: '品牌编码',
-            prop: 'brandCode',
-            minWidth: 120,
-            search: true,
-            searchSpan: 6
-          },
-          {
-            label: '品牌名称',
-            prop: 'brandName',
-            minWidth: 120,
-            search: true,
-            searchSpan: 6
-          },
-          {
-            label: '物料编码',
-            prop: 'itemCode',
-            minWidth: 140,
-            search: true,
-            searchSpan: 6
-          },
-          {
-            label: '物料名称',
-            prop: 'itemName',
             minWidth: 200,
             search: true,
-            searchSpan: 6,
-            overHidden: true
-          },
-          {
-            label: '规格',
-            prop: 'specs',
-            minWidth: 120,
-            overHidden: true
-          },
-          {
-            label: '花纹',
-            prop: 'pattern',
-            minWidth: 100,
-            overHidden: true
-          },
-          {
-            label: '预测数量',
-            prop: 'forecastQuantity',
-            minWidth: 120,
-            sortable: true,
-            slot: true,
-            align: 'right'
+            searchSpan: 6
           },
           {
             label: '审批状态',
@@ -220,6 +174,31 @@ export default {
             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 }
+        ]
       }
     }
   },
@@ -260,7 +239,7 @@ export default {
     /**
      * 获取审批状态配置
      * @param {number} status - 审批状态值
-     * @returns {Object} 状态配置对象
+     * @returns {{label: string, type: string}} 状态配置对象
      */
     getApprovalStatusConfig(status) {
       const config = APPROVAL_STATUS_CONFIG[status]
@@ -351,7 +330,7 @@ export default {
     /**
      * 选择变化处理
      * @this {ForecastSummaryComponent & Vue}
-     * @param {Array<SalesForecastMainListItemRecord>} selection - 选中的行数据
+     * @param {Array<SalesForecastMainRecord>} selection - 选中的行数据
      */
     selectionChange(selection) {
       this.selectionList = selection
@@ -420,17 +399,26 @@ export default {
         if (response && response.data && response.data.code === 200 && response.data.data) {
           const pageData = response.data.data
           const records = Array.isArray(pageData.records) ? pageData.records : []
-          // 扁平化子列表供表格展示(保持ID为后端返回的原样,避免大整数精度丢失)
-          const flat = []
-          for (const rec of records) {
-            const list = Array.isArray(rec.pcBladeSalesForecastSummaryList) ? rec.pcBladeSalesForecastSummaryList : []
-            for (const item of list) {
-              // 通过浅拷贝确保不会意外修改原对象
-              flat.push({ ...item })
+          // 字段类型转换与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.data = flat
+          })
           this.page.total = Number(pageData.total) || 0
           this.page.currentPage = Number(pageData.current) || 1
           this.page.pageSize = Number(pageData.size) || 10
@@ -450,6 +438,20 @@ export default {
     },
 
     /**
+     * 安全将值转换为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

+ 31 - 30
src/views/forecast-summary/types.d.ts

@@ -1,39 +1,40 @@
-// 销售预测汇总页面类型定义
+import { ForecastSummaryRecord, SalesForecastMainListItemRecord, SalesForecastMainRecord } from "@/api/forecast/types";
 
-import type { ForecastSummaryRecord, SalesForecastMainListItemRecord } from '@/api/forecast/types'
-
-// 页面配置类型
 export interface PageConfig {
-  pageSize: number
-  currentPage: number
-  total: number
+  pageSize: number;
+  currentPage: number;
+  total: number;
 }
 
-// 组件数据类型
 export interface ForecastSummaryComponentData {
-  form: Record<string, any>
-  // 由于 DEFAULT_FORECAST_SUMMARY_QUERY 中包含 null 值,这里放宽为 Record<string, any>
-  query: Record<string, any>
-  loading: boolean
-  page: PageConfig
-  data: Array<SalesForecastMainListItemRecord>
-  selectionList: Array<SalesForecastMainListItemRecord>
-  option: AvueCrudOption
+  form: Record<string, any>;
+  query: Record<string, any>;
+  loading: boolean;
+  page: PageConfig;
+  // 列表数据切换为主表记录,子表在展开行中展示
+  data: Array<SalesForecastMainRecord>;
+  // 选择列表也切换为主表记录
+  selectionList: Array<SalesForecastMainRecord>;
+  option: AvueCrudOption;
+  // 新增:子表格(展开区)配置
+  childOption: AvueCrudOption;
 }
 
-// 组件类型
 export interface ForecastSummaryComponent extends ForecastSummaryComponentData {
-  onLoad: (page: PageConfig, params?: Record<string, any>) => Promise<void>
-  getForecastSummaryDetail: (id: string | number) => Promise<ForecastSummaryRecord | null>
-  searchChange: (params: Record<string, any>, done: () => void) => void
-  searchReset: () => void
-  selectionChange: (selection: Array<SalesForecastMainListItemRecord>) => void
-  currentChange: (currentPage: number) => void
-  sizeChange: (pageSize: number) => void
-  refreshChange: () => void
-  getApprovalStatusConfig: (status: number) => { label: string; type: string }
-  formatNumber: (value: string | number) => string
-  formatDateTime: (dateTime: string) => string
-  formatYearMonth: (year: number, month: number) => string
-  $message: MessageInstance
+  onLoad: (page: PageConfig, params?: Record<string, any>) => Promise<void>;
+  searchChange: (params: Record<string, any>, done: () => void) => void;
+  searchReset: () => void;
+  selectionChange: (selection: Array<SalesForecastMainRecord>) => void;
+  currentChange: (currentPage: number) => void;
+  sizeChange: (pageSize: number) => void;
+  refreshChange: () => void;
+  beforeOpen: (done: Function, type: string) => void;
+  getForecastSummaryDetail: (id: string | number) => Promise<ForecastSummaryRecord | null>;
+  // 新增:BigInt 安全转换工具
+  safeBigInt: (v: string | number | null | undefined) => bigint | null;
+  // 模板调用的方法补充类型
+  getApprovalStatusConfig: (status: number) => { label: string; type: string };
+  formatNumber: (value: string | number) => string;
+  formatDateTime: (dateTime: string) => string;
+  formatYearMonth: (year: number, month: number) => string;
 }