Quellcode durchsuchen

feat(forecast): 添加按年月导出销售预测功能

yz vor 1 Woche
Ursprung
Commit
ab12b8cde0
3 geänderte Dateien mit 210 neuen und 0 gelöschten Zeilen
  1. 117 0
      src/views/forecast/forecastIndex.js
  2. 50 0
      src/views/forecast/index.vue
  3. 43 0
      src/views/forecast/types.d.ts

+ 117 - 0
src/views/forecast/forecastIndex.js

@@ -2,6 +2,7 @@ import { getForecastList, addForecast, updateForecast } from '@/api/forecast'
 import { getCustomerList } from '@/api/common'
 import { getItemList } from '@/api/common'
 import { getSalesForecastMainList } from '@/api/forecast/forecast-summary'
+import { exportUserSalesForecastByMonth } from '@/api/forecast/forecast-summary'
 import {
   APPROVAL_STATUS,
   APPROVAL_STATUS_CONFIG,
@@ -214,6 +215,35 @@ export default {
           { label: '预测数量', prop: 'forecastQuantity', minWidth: 120, align: 'right', slot: true }
         ]
       },
+      // 导出(按年月)选择弹层状态与表单
+      exportDialogVisible: false,
+      exportLoading: false,
+      exportForm: {
+        year: '',
+        month: ''
+      },
+      exportFormRules: {
+        year: [
+          { required: true, message: '请选择年份', trigger: 'change' }
+        ],
+        month: [
+          { required: true, message: '请选择月份', trigger: 'change' }
+        ]
+      },
+      monthOptions: [
+        { label: '1月', value: 1 },
+        { label: '2月', value: 2 },
+        { label: '3月', value: 3 },
+        { label: '4月', value: 4 },
+        { label: '5月', value: 5 },
+        { label: '6月', value: 6 },
+        { label: '7月', value: 7 },
+        { label: '8月', value: 8 },
+        { label: '9月', value: 9 },
+        { label: '10月', value: 10 },
+        { label: '11月', value: 11 },
+        { label: '12月', value: 12 }
+      ],
       /** @type {Record<string, Array<ValidationRule>>} 表单验证规则 */
       formRules: FORECAST_FORM_RULES
     }
@@ -260,6 +290,7 @@ export default {
   },
 
   methods: {
+    
     /**
      * 检查是否可以编辑
      * @param {ForecastRecord} row - 预测记录行数据
@@ -747,6 +778,92 @@ export default {
       this.dialogVisible = false
     },
 
+    /** 打开导出弹窗(按年月) */
+    openExportDialog() {
+      const now = new Date()
+      // 仅在为空时设置默认年/月,避免覆盖用户选择
+      if (!this.exportForm.year) this.exportForm.year = String(now.getFullYear())
+      if (!this.exportForm.month) this.exportForm.month = now.getMonth() + 1
+      this.exportDialogVisible = true
+    },
+
+    /** 确认导出(按年月) */
+    async confirmExportByMonth() {
+      if (!this.$refs || !this.$refs.exportFormRef) {
+        // 若未使用校验表单,直接做基础校验
+        if (!this.exportForm.year || !this.exportForm.month) {
+          this.$message.warning('请选择年份和月份')
+          return
+        }
+      }
+
+      const doExport = async () => {
+        try {
+          this.exportLoading = true
+          const year = this.exportForm.year
+          const month = this.exportForm.month
+          const res = await exportUserSalesForecastByMonth(year, month)
+          const blob = res && (res.data || res)
+          const filename = this.getExportFilenameFromResponse(res, year, month)
+          this.downloadBlob(blob, filename)
+          this.$message.success('导出成功')
+          this.exportDialogVisible = false
+        } catch (e) {
+          console.error('导出失败:', e)
+          this.$message.error('导出失败,请稍后重试')
+        } finally {
+          this.exportLoading = false
+        }
+      }
+
+      if (this.$refs && this.$refs.exportFormRef && this.$refs.exportFormRef.validate) {
+        this.$refs.exportFormRef.validate(valid => {
+          if (valid) doExport()
+        })
+      } else {
+        await doExport()
+      }
+    },
+
+    /** 从响应头解析导出文件名,失败则回退默认名 */
+    getExportFilenameFromResponse(response, year, month) {
+      try {
+        const headers = response && response.headers ? response.headers : {}
+        const cd = headers['content-disposition'] || headers['Content-Disposition'] || ''
+        // 优先解析 RFC 5987 扩展写法
+        const starMatch = /filename\*=([^']*)''([^;\n]+)/i.exec(cd)
+        if (starMatch && starMatch[2]) {
+          return decodeURIComponent(starMatch[2])
+        }
+        // 常规 filename="..."
+        const normalMatch = /filename=\"?([^\";\n]+)\"?/i.exec(cd)
+        if (normalMatch && normalMatch[1]) {
+          return decodeURI(normalMatch[1])
+        }
+      } catch (e) {
+        // ignore
+      }
+      const m = String(month).padStart(2, '0')
+      return `销售预测提报_用户_${year}-${m}.xlsx`
+    },
+
+    /** 触发下载 */
+    downloadBlob(blob, filename) {
+      try {
+        const url = window.URL.createObjectURL(blob)
+        const a = document.createElement('a')
+        a.href = url
+        a.download = filename || '导出文件.xlsx'
+        document.body.appendChild(a)
+        a.click()
+        document.body.removeChild(a)
+        window.URL.revokeObjectURL(url)
+      } catch (e) {
+        console.error('下载失败:', e)
+        this.$message.error('浏览器下载失败')
+      }
+    },
+
     /**
      * 生成预测编码
      * @this {Vue & {form: ForecastFormModel}}

+ 50 - 0
src/views/forecast/index.vue

@@ -32,6 +32,15 @@
           >
             新增预测申报
           </el-button>
+          <el-button
+            type="success"
+            size="small"
+            icon="el-icon-download"
+            style="margin-left: 8px;"
+            @click="openExportDialog"
+          >
+            导出
+          </el-button>
         </template>
 
         <!-- 自定义审批状态显示 -->
@@ -61,6 +70,47 @@
           <el-button type="text" size="small" disabled>不可编辑</el-button>
         </template>
       </avue-crud>
+
+      <!-- 导出(按年月)弹窗 -->
+      <el-dialog
+        title="导出"
+        :visible.sync="exportDialogVisible"
+        width="420px"
+        append-to-body
+        :close-on-click-modal="false"
+      >
+        <el-form
+          ref="exportFormRef"
+          :model="exportForm"
+          :rules="exportFormRules"
+          label-width="80px"
+          size="small"
+        >
+          <el-form-item label="年份" prop="year">
+            <el-date-picker
+              v-model="exportForm.year"
+              type="year"
+              value-format="yyyy"
+              placeholder="请选择年份"
+              style="width: 100%;"
+            />
+          </el-form-item>
+          <el-form-item label="月份" prop="month">
+            <el-select v-model="exportForm.month" placeholder="请选择月份" style="width: 100%;">
+              <el-option
+                v-for="m in monthOptions"
+                :key="m.value"
+                :label="m.label"
+                :value="m.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button @click="exportDialogVisible = false">取 消</el-button>
+          <el-button type="primary" :loading="exportLoading" @click="confirmExportByMonth">导 出</el-button>
+        </div>
+      </el-dialog>
     </div>
 
     <!-- 预测表单页面 -->

+ 43 - 0
src/views/forecast/types.d.ts

@@ -91,6 +91,20 @@ export interface ForecastComponentData {
   option: AvueCrudOption
   /** 表单验证规则 */
   formRules: Record<string, Array<ValidationRule>>
+
+  /** 导出(按年月)弹窗可见性 */
+  exportDialogVisible: boolean
+  /** 导出加载状态 */
+  exportLoading: boolean
+  /** 导出表单 */
+  exportForm: {
+    year: string
+    month: number | string
+  }
+  /** 导出表单校验规则 */
+  exportFormRules: Record<string, Array<ValidationRule>>
+  /** 月份选项 */
+  monthOptions: Array<{ label: string; value: number }>
 }
 
 /**
@@ -136,13 +150,42 @@ export interface ForecastComponent {
   option: AvueCrudOption;
   /** 表单验证规则 */
   formRules: Record<string, Array<ValidationRule>>;
+
+  /** 导出(按年月)弹窗可见性 */
+  exportDialogVisible: boolean;
+  /** 导出加载状态 */
+  exportLoading: boolean;
+  /** 导出表单 */
+  exportForm: { year: string; month: number | string };
+  /** 导出表单校验规则 */
+  exportFormRules: Record<string, Array<ValidationRule>>;
+  /** 月份选项 */
+  monthOptions: Array<{ label: string; value: number }>;
+
   /** 组件引用 */
   $refs: {
     forecastForm?: {
       handleSubmit(): void;
     };
+    exportFormRef?: {
+      validate?: (cb: (valid: boolean) => void) => void;
+    };
   };
 
+  /** 工具方法:数字格式化 */
+  formatNumber(value: number | string): string;
+  /** 工具方法:安全BigInt转换 */
+  safeBigInt(v: string | number | null | undefined): bigint | null;
+
+  /** 打开导出弹窗 */
+  openExportDialog(): void;
+  /** 确认导出 */
+  confirmExportByMonth(): Promise<void>;
+  /** 解析导出文件名 */
+  getExportFilenameFromResponse(response: any, year: string | number, month: string | number): string;
+  /** 下载Blob */
+  downloadBlob(blob: Blob, filename?: string): void;
+
   /** 加载客户选项 */
   loadCustomerOptions(keyword?: string): Promise<void>;
   /** 加载物料选项 */