Просмотр исходного кода

feat(订单导出): 实现工厂订单导出功能

yz 3 дней назад
Родитель
Сommit
9cc6fce033

+ 248 - 0
src/api/export-service.js

@@ -0,0 +1,248 @@
+/**
+ * 导出服务类
+ * @fileoverview 协调整个导出流程的核心业务逻辑
+ */
+
+import { excelExportUtil } from '@/utils/export/excel-export-util';
+import { dataProcessor } from '@/utils/export/data-processor';
+import { progressManager } from '@/utils/export/progress-manager';
+import { getList as getOrderItemList } from '@/api/order/order-item';
+
+/**
+ * 导出服务类
+ * 协调整个导出流程的核心服务
+ */
+class ExportService {
+  constructor() {
+    this.dataProcessor = dataProcessor;
+    this.excelUtil = excelExportUtil;
+    this.progressManager = progressManager;
+    this.isExporting = false;
+    this.cancelRequested = false;
+  }
+
+  /**
+   * 执行导出主流程
+   * @param {Object} options 导出选项
+   * @param {Array} selectedOrders 选中的订单列表(直接传入,避免依赖store)
+   */
+  async executeExport(options = {}, selectedOrders = []) {
+    if (this.isExporting) {
+      throw new Error('导出正在进行中,请稍后重试');
+    }
+
+    const startTime = Date.now();
+    this.isExporting = true;
+    this.cancelRequested = false;
+
+    try {
+      // 1. 使用传入的订单列表
+      if (selectedOrders.length === 0) {
+        throw new Error('请先选择要导出的订单');
+      }
+
+      // 检查数量限制
+      if (selectedOrders.length > 100) {
+        throw new Error('单次导出订单数量不能超过100个');
+      }
+
+      // 2. 更新进度状态
+      this.progressManager.start(selectedOrders.length);
+
+      // 3. 分批获取订单明细数据
+      const ordersWithDetails = await this.fetchOrdersWithDetails(selectedOrders);
+
+      if (this.cancelRequested) {
+        throw new Error('导出已取消');
+      }
+
+      // 4. 数据格式转换和处理
+      const processedData = this.dataProcessor.transformOrderData(ordersWithDetails);
+
+      // 5. 生成Excel文件
+      const excelBuffer = await this.excelUtil.exportToMultiSheets(processedData, {
+        fileName: this.generateFileName(),
+        progressCallback: this.progressManager.updateProgress.bind(this.progressManager)
+      });
+
+      if (this.cancelRequested) {
+        throw new Error('导出已取消');
+      }
+
+      // 6. 下载文件
+      this.downloadFile(excelBuffer, this.generateFileName());
+
+      // 7. 完成提示
+      this.handleExportComplete(selectedOrders.length, Date.now() - startTime);
+
+    } catch (error) {
+      this.handleExportError(error);
+      throw error;
+    } finally {
+      this.isExporting = false;
+      this.cancelRequested = false;
+    }
+  }
+
+  /**
+   * 取消导出
+   */
+  cancelExport() {
+    if (this.isExporting) {
+      this.cancelRequested = true;
+      this.progressManager.error(new Error('导出已取消'));
+    }
+  }
+
+  /**
+   * 获取导出进度
+   */
+  getProgress() {
+    return this.progressManager.getProgress();
+  }
+
+  
+  /**
+   * 分批获取订单明细数据
+   * @param {Array} selectedOrders 选中的订单列表
+   */
+  async fetchOrdersWithDetails(selectedOrders) {
+    const BATCH_SIZE = 10; // 每批处理10个订单
+    const results = [];
+
+    for (let i = 0; i < selectedOrders.length; i += BATCH_SIZE) {
+      if (this.cancelRequested) {
+        break;
+      }
+
+      const batch = selectedOrders.slice(i, i + BATCH_SIZE);
+
+      // 并行获取当前批次的明细数据
+      const batchPromises = batch.map(order =>
+        this.fetchOrderDetails(order.id)
+          .then(details => ({ ...order, details }))
+          .catch(error => {
+            console.warn(`获取订单 ${order.orderCode} 明细失败:`, error);
+            return { ...order, details: [] };
+          })
+      );
+
+      const batchResults = await Promise.all(batchPromises);
+      results.push(...batchResults);
+
+      // 更新进度
+      this.progressManager.updateDataFetchProgress(
+        Math.min(i + BATCH_SIZE, selectedOrders.length),
+        selectedOrders.length
+      );
+    }
+
+    return results;
+  }
+
+  /**
+   * 获取单个订单的明细数据
+   * @param {string|number} orderId 订单ID
+   */
+  async fetchOrderDetails(orderId) {
+    try {
+      const response = await getOrderItemList(1, 100, { orderId });
+      return response.data?.data?.records || [];
+    } catch (error) {
+      console.error('获取订单明细失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 生成文件名
+   */
+  generateFileName() {
+    const now = new Date();
+    const timestamp = now.toISOString().slice(0, 19).replace(/[:-]/g, '');
+    return `工厂订单导出_${timestamp}.xlsx`;
+  }
+
+  /**
+   * 下载文件
+   * @param {ArrayBuffer} buffer 文件缓冲区
+   * @param {string} fileName 文件名
+   */
+  downloadFile(buffer, fileName) {
+    const blob = new Blob([buffer], {
+      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = fileName;
+    document.body.appendChild(a);
+    a.click();
+
+    // 清理
+    setTimeout(() => {
+      document.body.removeChild(a);
+      URL.revokeObjectURL(url);
+    }, 100);
+  }
+
+  /**
+   * 处理导出完成
+   * @param {number} orderCount 订单数量
+   * @param {number} duration 耗时(毫秒)
+   */
+  handleExportComplete(orderCount, duration) {
+    this.progressManager.complete();
+
+    console.log(`导出完成: ${orderCount}个订单,耗时${Math.round(duration/1000)}秒`);
+
+    // 这里可以添加成功通知
+    if (typeof this.$message !== 'undefined') {
+      this.$message.success(`成功导出${orderCount}个订单`);
+    }
+  }
+
+  /**
+   * 处理导出错误
+   * @param {Error} error 错误对象
+   */
+  handleExportError(error) {
+    this.progressManager.error(error);
+    console.error('导出失败:', error);
+
+    // 这里可以添加错误通知
+    if (typeof this.$message !== 'undefined') {
+      this.$message.error('导出失败: ' + error.message);
+    }
+  }
+
+  /**
+   * 检查是否正在导出
+   */
+  isCurrentlyExporting() {
+    return this.isExporting;
+  }
+
+  /**
+   * 检查是否可以导出
+   */
+  canExport() {
+    return !this.isExporting;
+  }
+}
+
+// 导出单例实例
+let exportServiceInstance = null;
+
+/**
+ * 获取导出服务实例
+ */
+export function getExportService() {
+  if (!exportServiceInstance) {
+    exportServiceInstance = new ExportService();
+  }
+  return exportServiceInstance;
+}
+
+// 导出类
+export default ExportService;

+ 143 - 0
src/components/export/ExportButton.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="export-button-wrapper">
+    <el-button
+      :type="buttonType"
+      :icon="icon"
+      :disabled="disabled || selectedCount === 0"
+      :loading="loading"
+      :size="size"
+      @click="handleClick"
+    >
+      <span v-if="!loading">{{ buttonText }}</span>
+      <span v-else>{{ loadingText }}</span>
+    </el-button>
+
+    <!-- 导出选项下拉菜单 -->
+    <el-dropdown v-if="showOptions" trigger="click" @command="handleCommand">
+      <span class="el-dropdown-link">
+        <i class="el-icon-arrow-down el-icon--right"></i>
+      </span>
+      <el-dropdown-menu slot="dropdown">
+        <el-dropdown-item command="current">导出当前页</el-dropdown-item>
+        <el-dropdown-item command="selected" :disabled="selectedCount === 0">
+          导出选中项 ({{ selectedCount }})
+        </el-dropdown-item>
+        <el-dropdown-item command="all" :disabled="!canExportAll">
+          导出全部数据
+        </el-dropdown-item>
+      </el-dropdown-menu>
+    </el-dropdown>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ExportButton',
+  props: {
+    // 选中的订单数量
+    selectedCount: {
+      type: Number,
+      default: 0
+    },
+    // 是否正在导出
+    loading: {
+      type: Boolean,
+      default: false
+    },
+    // 按钮类型
+    type: {
+      type: String,
+      default: 'primary',
+      validator: value => ['primary', 'success', 'warning', 'danger', 'info', 'text'].includes(value)
+    },
+    // 按钮大小
+    size: {
+      type: String,
+      default: 'medium',
+      validator: value => ['medium', 'small', 'mini'].includes(value)
+    },
+    // 是否显示选项下拉
+    showOptions: {
+      type: Boolean,
+      default: true
+    },
+    // 禁用状态
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    // 按钮图标
+    icon: {
+      type: String,
+      default: 'el-icon-download'
+    },
+    // 按钮文本
+    text: {
+      type: String,
+      default: ''
+    },
+    // 加载文本
+    loadingText: {
+      type: String,
+      default: '导出中...'
+    },
+    // 是否可以导出全部
+    canExportAll: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    // 按钮类型
+    buttonType() {
+      return this.type;
+    },
+    // 按钮文本
+    buttonText() {
+      if (this.text) {
+        return this.text;
+      }
+      return `导出Excel${this.selectedCount > 0 ? ` (${this.selectedCount})` : ''}`;
+    }
+  },
+  methods: {
+    // 处理按钮点击
+    handleClick() {
+      if (!this.showOptions) {
+        // 如果不显示选项下拉,直接触发导出选中项
+        this.$emit('export', 'selected');
+      } else {
+        // 如果显示选项下拉,但不触发下拉菜单,默认导出选中项
+        this.$emit('export', 'selected');
+      }
+    },
+    // 处理下拉菜单命令
+    handleCommand(command) {
+      this.$emit('export', command);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.export-button-wrapper {
+  display: inline-flex;
+  align-items: center;
+
+  .el-dropdown-link {
+    margin-left: -5px;
+    padding: 0 5px;
+    cursor: pointer;
+
+    &:hover {
+      color: #409EFF;
+    }
+  }
+}
+
+// 禁用状态下的下拉菜单样式
+.el-button.is-disabled + .el-dropdown-link {
+  cursor: not-allowed;
+  color: #c0c4cc;
+}
+</style>

+ 198 - 0
src/components/export/ExportProgressDialog.vue

@@ -0,0 +1,198 @@
+<template>
+  <el-dialog
+    title="Excel导出中..."
+    :visible.sync="visible"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    :append-to-body="true"
+    :modal-append-to-body="true"
+    width="400px"
+    center
+  >
+    <div class="export-progress-content">
+      <!-- 进度条 -->
+      <el-progress
+        :percentage="progress.percentage"
+        :status="progress.status"
+        :stroke-width="12"
+      ></el-progress>
+
+      <!-- 进度信息 -->
+      <div class="progress-info">
+        <p class="progress-text">{{ progress.currentText }}</p>
+        <p class="progress-detail">{{ progress.detail }}</p>
+        <p v-if="progress.remainingTime" class="progress-time">
+          预计剩余时间: {{ progress.remainingTime }}
+        </p>
+      </div>
+
+      <!-- 状态图标 -->
+      <div class="progress-status">
+        <i v-if="progress.status === 'active'" class="el-icon-loading status-icon loading"></i>
+        <i v-else-if="progress.status === 'success'" class="el-icon-circle-check status-icon success"></i>
+        <i v-else-if="progress.status === 'exception'" class="el-icon-circle-close status-icon error"></i>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="progress-actions" v-if="progress.cancellable">
+        <el-button
+          size="small"
+          @click="handleCancel"
+        >
+          取消导出
+        </el-button>
+      </div>
+
+      <!-- 完成状态下的按钮 -->
+      <div class="progress-actions" v-else-if="progress.status === 'success'">
+        <el-button
+          type="primary"
+          size="small"
+          @click="handleClose"
+        >
+          确定
+        </el-button>
+      </div>
+
+      <!-- 错误状态下的按钮 -->
+      <div class="progress-actions" v-else-if="progress.status === 'exception'">
+        <el-button
+          size="small"
+          @click="handleRetry"
+        >
+          重试
+        </el-button>
+        <el-button
+          size="small"
+          @click="handleClose"
+        >
+          关闭
+        </el-button>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'ExportProgressDialog',
+  props: {
+    // 是否显示对话框
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    // 进度信息
+    progress: {
+      type: Object,
+      default: () => ({
+        percentage: 0,
+        status: 'active',
+        currentText: '',
+        detail: '',
+        cancellable: true,
+        current: 0,
+        total: 0,
+        remainingTime: ''
+      })
+    }
+  },
+  methods: {
+    // 处理取消
+    handleCancel() {
+      this.$emit('cancel');
+    },
+    // 处理重试
+    handleRetry() {
+      this.$emit('retry');
+    },
+    // 处理关闭
+    handleClose() {
+      this.$emit('close');
+    }
+  },
+  watch: {
+    // 监听进度状态变化
+    'progress.status'(newStatus) {
+      if (newStatus === 'success') {
+        // 导出成功,3秒后自动关闭
+        setTimeout(() => {
+          this.handleClose();
+        }, 3000);
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.export-progress-content {
+  padding: 20px 0;
+  text-align: center;
+
+  .progress-info {
+    margin: 20px 0;
+
+    .progress-text {
+      font-size: 16px;
+      font-weight: 500;
+      color: #303133;
+      margin: 0 0 8px 0;
+    }
+
+    .progress-detail {
+      font-size: 14px;
+      color: #606266;
+      margin: 0 0 4px 0;
+    }
+
+    .progress-time {
+      font-size: 12px;
+      color: #909399;
+      margin: 0;
+    }
+  }
+
+  .progress-status {
+    margin: 20px 0;
+
+    .status-icon {
+      font-size: 32px;
+
+      &.loading {
+        color: #409EFF;
+        animation: rotating 2s linear infinite;
+      }
+
+      &.success {
+        color: #67C23A;
+      }
+
+      &.error {
+        color: #F56C6C;
+      }
+    }
+  }
+
+  .progress-actions {
+    margin-top: 24px;
+    display: flex;
+    justify-content: center;
+    gap: 10px;
+
+    .el-button {
+      min-width: 80px;
+    }
+  }
+}
+
+@keyframes rotating {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 240 - 0
src/types/export.d.ts

@@ -0,0 +1,240 @@
+/**
+ * 订单导出功能相关类型定义
+ * @fileoverview 导出功能的TypeScript接口定义
+ */
+
+/**
+ * 导出选项配置
+ */
+export interface ExportOptions {
+  /** 导出类型 */
+  type?: 'selected' | 'current' | 'all';
+  /** 文件名前缀 */
+  fileNamePrefix?: string;
+  /** 批次大小 */
+  batchSize?: number;
+  /** 是否显示进度 */
+  showProgress?: boolean;
+}
+
+/**
+ * 导出进度信息
+ */
+export interface ExportProgress {
+  /** 百分比 */
+  percentage: number;
+  /** 状态 */
+  status: 'active' | 'success' | 'exception';
+  /** 当前文本 */
+  currentText: string;
+  /** 详细信息 */
+  detail: string;
+  /** 是否可取消 */
+  cancellable: boolean;
+  /** 当前进度 */
+  current: number;
+  /** 总数 */
+  total: number;
+  /** 剩余时间 */
+  remainingTime?: string;
+}
+
+/**
+ * 导出订单数据
+ */
+export interface ExportOrderData {
+  /** 订单号 */
+  orderCode: string;
+  /** 项目名称 */
+  orgName: string;
+  /** 供应商名称 */
+  supplierName: string;
+  /** 采购数量 */
+  totalQuantity: number;
+  /** 采购单价 */
+  unitPrice: number;
+  /** 采购金额 */
+  totalAmount: number;
+  /** 付款金额 */
+  paymentAmount: number;
+  /** 订单明细 */
+  orderItems: ExportOrderItemData[];
+}
+
+/**
+ * 导出订单明细数据
+ */
+export interface ExportOrderItemData {
+  /** 物料编码 */
+  itemCode: string;
+  /** 物料名称 */
+  itemName: string;
+  /** 规格 */
+  specs: string;
+  /** 数量 */
+  quantity: number;
+  /** 单价 */
+  unitPrice: number;
+  /** 金额 */
+  totalAmount: number;
+}
+
+/**
+ * Excel导出配置
+ */
+export interface ExcelExportConfig {
+  /** 文件名 */
+  fileName: string;
+  /** 进度回调 */
+  progressCallback?: (current: number, total: number, text: string) => void;
+  /** 样式配置 */
+  styleConfig?: {
+    /** 表头颜色 */
+    headerColor?: string;
+    /** 标题字体大小 */
+    titleFontSize?: number;
+    /** 数据行高 */
+    dataRowHeight?: number;
+  };
+}
+
+/**
+ * 导出服务接口
+ */
+export interface IExportService {
+  /**
+   * 执行导出
+   * @param options 导出选项
+   */
+  executeExport(options?: ExportOptions): Promise<void>;
+
+  /**
+   * 取消导出
+   */
+  cancelExport(): void;
+
+  /**
+   * 获取导出进度
+   */
+  getProgress(): ExportProgress;
+}
+
+/**
+ * 导出按钮组件Props
+ */
+export interface ExportButtonProps {
+  /** 选中的订单数量 */
+  selectedCount: number;
+  /** 是否正在导出 */
+  loading?: boolean;
+  /** 按钮类型 */
+  type?: 'primary' | 'success' | 'warning' | 'danger';
+  /** 按钮大小 */
+  size?: 'medium' | 'small' | 'mini';
+  /** 是否显示选项下拉 */
+  showOptions?: boolean;
+  /** 禁用状态 */
+  disabled?: boolean;
+}
+
+/**
+ * 导出进度对话框Props
+ */
+export interface ExportProgressDialogProps {
+  /** 是否显示对话框 */
+  visible: boolean;
+  /** 进度信息 */
+  progress: ExportProgress;
+  /** 取消回调 */
+  onCancel?: () => void;
+}
+
+/**
+ * Vuex Store状态接口
+ */
+export interface ExportSelectionState {
+  /** 选中的订单列表 */
+  selectedOrders: any[];
+  /** 最后更新时间 */
+  lastUpdateTime: string | null;
+  /** 数据是否需要同步 */
+  isDirty: boolean;
+  /** 当前筛选条件 */
+  currentFilter: any;
+  /** 当前页码 */
+  currentPage: number;
+  /** 已选择数据的页面集合 */
+  allSelectedPages: Set<number>;
+}
+
+/**
+ * Excel样式配置
+ */
+export interface ExcelStyles {
+  /** 表头样式 */
+  header: {
+    fill: {
+      type: 'pattern';
+      pattern: 'solid';
+      fgColor: { argb: string };
+    };
+    font: {
+      color: { argb: string };
+      bold: boolean;
+      size: number;
+    };
+    alignment: {
+      horizontal: 'center';
+      vertical: 'middle';
+    };
+    border: {
+      top: { style: string; color: { argb: string } };
+      left: { style: string; color: { argb: string } };
+      bottom: { style: string; color: { argb: string } };
+      right: { style: string; color: { argb: string } };
+    };
+  };
+  /** 数据行样式 */
+  data: {
+    fill: {
+      type: 'pattern';
+      pattern: 'solid';
+      fgColor: { argb: string };
+    };
+    font: {
+      size: number;
+    };
+    alignment: {
+      horizontal: 'left';
+      vertical: 'middle';
+    };
+    border: {
+      top: { style: string; color: { argb: string } };
+      left: { style: string; color: { argb: string } };
+      bottom: { style: string; color: { argb: string } };
+      right: { style: string; color: { argb: string } };
+    };
+  };
+  /** 合计行样式 */
+  total: {
+    fill: {
+      type: 'pattern';
+      pattern: 'solid';
+      fgColor: { argb: string };
+    };
+    font: {
+      size: number;
+      bold: boolean;
+    };
+    alignment: {
+      horizontal: 'right';
+      vertical: 'middle';
+    };
+    border: {
+      top: { style: string; color: { argb: string } };
+      left: { style: string; color: { argb: string } };
+      bottom: { style: string; color: { argb: string } };
+      right: { style: string; color: { argb: string } };
+    };
+  };
+}

+ 86 - 0
src/utils/export/data-processor.js

@@ -0,0 +1,86 @@
+/**
+ * 数据处理工具类
+ * @fileoverview 订单数据格式转换、字段映射和计算逻辑
+ */
+
+/**
+ * 数据处理器
+ * 负责订单数据的格式转换、字段映射和计算逻辑
+ */
+class DataProcessor {
+  constructor() {
+    this.fieldMapping = {
+      '订单号': 'orderCode',
+      '项目名称': 'orgName',
+      '供应商': 'customerName',
+      '采购数量': 'totalQuantity',
+      '采购单价': 'unitPrice',
+      '采购金额': 'totalAmount',
+      '付款金额': 'totalAmount'
+    };
+  }
+
+  /**
+   * 转换订单数据格式
+   * @param {Array} ordersWithDetails 包含明细的订单数据
+   */
+  transformOrderData(ordersWithDetails) {
+    return ordersWithDetails.map(order => ({
+      orderCode: order.orderCode,
+      orgName: order.orgName,
+      supplierName: order.customerName, // 工厂订单中客户=供应商
+      totalQuantity: this.parseNumber(order.totalQuantity),
+      unitPrice: this.calculateAveragePrice(order.details),
+      totalAmount: this.parseNumber(order.totalAmount),
+      paymentAmount: this.parseNumber(order.totalAmount), // 暂使用总金额
+      orderItems: this.transformOrderItems(order.details)
+    }));
+  }
+
+  /**
+   * 转换订单明细数据
+   * @param {Array} details 订单明细
+   */
+  transformOrderItems(details) {
+    return details.map(item => ({
+      itemCode: item.itemCode,
+      itemName: item.itemName,
+      specs: item.specs,
+      quantity: this.parseNumber(item.orderQuantity),
+      unitPrice: this.parseNumber(item.unitPrice),
+      totalAmount: this.parseNumber(item.totalAmount)
+    }));
+  }
+
+  /**
+   * 计算平均单价
+   * @param {Array} details 订单明细
+   */
+  calculateAveragePrice(details) {
+    if (!details || details.length === 0) return 0;
+
+    const totalAmount = details.reduce((sum, item) =>
+      sum + this.parseNumber(item.totalAmount), 0
+    );
+    const totalQuantity = details.reduce((sum, item) =>
+      sum + this.parseNumber(item.orderQuantity), 0
+    );
+
+    return totalQuantity > 0 ? totalAmount / totalQuantity : 0;
+  }
+
+  /**
+   * 安全解析数字
+   * @param {string|number} value 待解析的值
+   */
+  parseNumber(value) {
+    const num = parseFloat(value);
+    return isNaN(num) ? 0 : num;
+  }
+}
+
+// 导出单例实例
+export const dataProcessor = new DataProcessor();
+
+// 导出类
+export default DataProcessor;

+ 235 - 0
src/utils/export/excel-export-util.js

@@ -0,0 +1,235 @@
+/**
+ * Excel导出工具类
+ * @fileoverview 核心Excel文件生成功能,独立可复用
+ * @description 专门处理Excel文件生成和样式设置,无业务逻辑依赖
+ */
+
+import ExcelJS from 'exceljs';
+
+/**
+ * Excel导出工具类
+ * 负责生成多Sheet Excel文件,支持样式设置和格式化
+ */
+class ExcelExportUtil {
+  constructor() {
+    this.defaultStyles = {
+      header: {
+        fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF165DFF' } },
+        font: { color: { argb: 'FFFFFFFF' }, bold: true, size: 11 },
+        alignment: { horizontal: 'center', vertical: 'middle' }
+      },
+      data: {
+        fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFFFF' } },
+        font: { size: 11 },
+        alignment: { horizontal: 'left', vertical: 'middle' }
+      },
+      total: {
+        fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFF7E6' } },
+        font: { size: 11, bold: true },
+        alignment: { horizontal: 'right', vertical: 'middle' }
+      }
+    };
+  }
+
+  /**
+   * 生成多Sheet Excel文件
+   * @param {Array} ordersData 处理后的订单数据
+   * @param {Object} options 导出选项
+   */
+  async exportToMultiSheets(ordersData, options = {}) {
+    const workbook = new ExcelJS.Workbook();
+
+    // 设置工作簿属性
+    workbook.creator = 'Gubersail工厂订单系统';
+    workbook.lastModifiedBy = '系统';
+    workbook.created = new Date();
+    workbook.modified = new Date();
+
+    // 为每个订单创建工作表
+    for (let i = 0; i < ordersData.length; i++) {
+      const orderData = ordersData[i];
+      const worksheet = workbook.addWorksheet(this.sanitizeSheetName(orderData.orderCode));
+
+      // 生成订单Sheet内容
+      this.generateOrderSheet(worksheet, orderData);
+
+      // 更新进度回调
+      if (options.progressCallback) {
+        options.progressCallback(i + 1, ordersData.length, '生成Excel工作表');
+      }
+    }
+
+    // 生成Excel文件缓冲区
+    const buffer = await workbook.xlsx.writeBuffer();
+    return buffer;
+  }
+
+  /**
+   * 生成单个订单的工作表
+   * @param {ExcelJS.Worksheet} worksheet 工作表对象
+   * @param {Object} orderData 订单数据
+   */
+  generateOrderSheet(worksheet, orderData) {
+    let rowIndex = 1;
+    const maxColumns = 7; // 7个字段
+
+    // 1. 设置列宽
+    worksheet.columns = [
+      { width: 15 }, // 订单号
+      { width: 20 }, // 项目名称
+      { width: 15 }, // 供应商
+      { width: 12 }, // 采购数量
+      { width: 12 }, // 采购单价
+      { width: 15 }, // 采购金额
+      { width: 15 }  // 付款金额
+    ];
+
+    // 2. 标题行
+    const titleRow = worksheet.getRow(rowIndex++);
+    titleRow.values = [`订单采购汇总 - ${orderData.orderCode}`];
+    titleRow.font = { size: 14, bold: true };
+    titleRow.alignment = { horizontal: 'center', vertical: 'middle' };
+    titleRow.height = 40;
+    worksheet.mergeCells(1, 1, 1, maxColumns);
+
+    // 3. 空行
+    rowIndex++;
+
+    // 4. 表头行
+    const headerRow = worksheet.getRow(rowIndex++);
+    headerRow.values = ['订单号', '项目名称', '供应商', '采购数量', '采购单价', '采购金额', '付款金额'];
+    this.applyHeaderStyle(headerRow);
+
+    // 5. 订单主信息行
+    const mainInfoRow = worksheet.getRow(rowIndex++);
+    mainInfoRow.values = [
+      orderData.orderCode,
+      orderData.orgName,
+      orderData.supplierName,
+      orderData.totalQuantity,
+      orderData.unitPrice.toFixed(2),
+      orderData.totalAmount.toFixed(2),
+      orderData.paymentAmount.toFixed(2)
+    ];
+    this.applyDataStyle(mainInfoRow);
+
+    // 6. 空行
+    rowIndex++;
+
+    // 7. 明细标题行
+    const detailTitleRow = worksheet.getRow(rowIndex++);
+    detailTitleRow.values = ['订单明细信息'];
+    detailTitleRow.font = { size: 12, bold: true };
+    detailTitleRow.height = 30;
+    worksheet.mergeCells(rowIndex - 1, 1, rowIndex - 1, maxColumns);
+
+    // 8. 空行
+    rowIndex++;
+
+    // 9. 明细表头行
+    const detailHeaderRow = worksheet.getRow(rowIndex++);
+    detailHeaderRow.values = ['物料编码', '物料名称', '规格', '数量', '单价', '金额'];
+    this.applyHeaderStyle(detailHeaderRow);
+
+    // 10. 明细数据行
+    orderData.orderItems.forEach(item => {
+      const dataRow = worksheet.getRow(rowIndex++);
+      dataRow.values = [
+        item.itemCode,
+        item.itemName,
+        item.specs,
+        item.quantity,
+        item.unitPrice.toFixed(2),
+        item.totalAmount.toFixed(2)
+      ];
+      this.applyDataStyle(dataRow);
+
+      // 设置数字格式
+      dataRow.getCell(4).numFmt = '#,##0';    // 数量
+      dataRow.getCell(5).numFmt = '#,##0.00'; // 单价
+      dataRow.getCell(6).numFmt = '#,##0.00'; // 金额
+    });
+
+    // 11. 合计行
+    const totalRow = worksheet.getRow(rowIndex++);
+    const totalQuantity = orderData.orderItems.reduce((sum, item) => sum + item.quantity, 0);
+    const totalAmount = orderData.orderItems.reduce((sum, item) => sum + item.totalAmount, 0);
+
+    totalRow.values = ['合计', '', '', totalQuantity, '', totalAmount.toFixed(2), ''];
+    this.applyTotalStyle(totalRow);
+    totalRow.getCell(4).numFmt = '#,##0';
+    totalRow.getCell(6).numFmt = '#,##0.00';
+  }
+
+  /**
+   * 应用表头样式
+   * @param {ExcelJS.Row} row 行对象
+   */
+  applyHeaderStyle(row) {
+    row.height = 30;
+    row.eachCell(cell => {
+      cell.fill = this.defaultStyles.header.fill;
+      cell.font = this.defaultStyles.header.font;
+      cell.alignment = this.defaultStyles.header.alignment;
+      cell.border = this.getStandardBorder();
+    });
+  }
+
+  /**
+   * 应用数据行样式
+   * @param {ExcelJS.Row} row 行对象
+   */
+  applyDataStyle(row) {
+    row.height = 25;
+    row.eachCell(cell => {
+      cell.fill = this.defaultStyles.data.fill;
+      cell.font = this.defaultStyles.data.font;
+      cell.alignment = this.defaultStyles.data.alignment;
+      cell.border = this.getStandardBorder();
+    });
+  }
+
+  /**
+   * 应用合计行样式
+   * @param {ExcelJS.Row} row 行对象
+   */
+  applyTotalStyle(row) {
+    row.height = 30;
+    row.eachCell(cell => {
+      cell.fill = this.defaultStyles.total.fill;
+      cell.font = this.defaultStyles.total.font;
+      cell.alignment = this.defaultStyles.total.alignment;
+      cell.border = this.getStandardBorder();
+    });
+  }
+
+  /**
+   * 获取标准边框样式
+   */
+  getStandardBorder() {
+    return {
+      top: { style: 'thin', color: { argb: 'FFD0D7E3' } },
+      left: { style: 'thin', color: { argb: 'FFD0D7E3' } },
+      bottom: { style: 'thin', color: { argb: 'FFD0D7E3' } },
+      right: { style: 'thin', color: { argb: 'FFD0D7E3' } }
+    };
+  }
+
+  /**
+   * 清理Sheet名称,确保Excel兼容性
+   * @param {string} name 原始名称
+   */
+  sanitizeSheetName(name) {
+    // Excel Sheet名称不能包含特殊字符,且长度不超过31
+    return name
+      .replace(/[\\/:*?"<>|]/g, '_') // 替换非法字符
+      .substring(0, 31) // 限制长度
+      .trim();
+  }
+}
+
+// 导出单例实例
+export const excelExportUtil = new ExcelExportUtil();
+
+// 导出类
+export default ExcelExportUtil;

+ 147 - 0
src/utils/export/progress-manager.js

@@ -0,0 +1,147 @@
+/**
+ * 进度管理器
+ * @fileoverview 导出进度的跟踪、计算和状态管理
+ */
+
+/**
+ * 进度管理器
+ * 负责导出进度的跟踪、计算和状态管理
+ */
+class ProgressManager {
+  constructor() {
+    this.progress = {
+      percentage: 0,
+      status: 'active',
+      currentText: '',
+      detail: '',
+      cancellable: true,
+      current: 0,
+      total: 0
+    };
+
+    this.startTime = null;
+    this.callbacks = [];
+  }
+
+  /**
+   * 开始进度跟踪
+   * @param {number} total 总数
+   */
+  start(total) {
+    this.progress.total = total;
+    this.progress.current = 0;
+    this.progress.percentage = 0;
+    this.progress.status = 'active';
+    this.progress.currentText = '准备导出...';
+    this.progress.detail = `共 ${total} 个订单`;
+    this.progress.cancellable = true;
+    this.startTime = Date.now();
+
+    this.notifyCallbacks();
+  }
+
+  /**
+   * 更新进度
+   * @param {number} current 当前进度
+   * @param {number} total 总数
+   * @param {string} text 进度文本
+   */
+  updateProgress(current, total, text = '') {
+    this.progress.current = current;
+    this.progress.total = total;
+    this.progress.percentage = Math.round((current / total) * 100);
+    this.progress.currentText = text;
+    this.progress.detail = `已处理 ${current}/${total} 个订单`;
+
+    // 估算剩余时间
+    if (current > 0 && this.startTime) {
+      const elapsed = Date.now() - this.startTime;
+      const avgTime = elapsed / current;
+      const remaining = (total - current) * avgTime;
+      this.progress.remainingTime = this.formatTime(remaining);
+    }
+
+    this.notifyCallbacks();
+  }
+
+  /**
+   * 更新数据获取进度
+   * @param {number} fetched 已获取数量
+   * @param {number} total 总数量
+   */
+  updateDataFetchProgress(fetched, total) {
+    this.updateProgress(fetched, total, '获取订单明细数据...');
+  }
+
+  /**
+   * 完成进度
+   */
+  complete() {
+    this.progress.percentage = 100;
+    this.progress.status = 'success';
+    this.progress.currentText = '导出完成';
+    this.progress.detail = `成功导出 ${this.progress.total} 个订单`;
+    this.progress.cancellable = false;
+
+    this.notifyCallbacks();
+  }
+
+  /**
+   * 错误状态
+   * @param {Error} error 错误对象
+   */
+  error(error) {
+    this.progress.status = 'exception';
+    this.progress.currentText = '导出失败';
+    this.progress.detail = error.message;
+    this.progress.cancellable = false;
+
+    this.notifyCallbacks();
+  }
+
+  /**
+   * 注册进度回调
+   * @param {Function} callback 回调函数
+   */
+  onProgress(callback) {
+    this.callbacks.push(callback);
+  }
+
+  /**
+   * 通知所有回调
+   */
+  notifyCallbacks() {
+    this.callbacks.forEach(callback => {
+      try {
+        callback({ ...this.progress });
+      } catch (error) {
+        console.warn('Progress callback error:', error);
+      }
+    });
+  }
+
+  /**
+   * 格式化时间
+   * @param {number} ms 毫秒数
+   */
+  formatTime(ms) {
+    const seconds = Math.floor(ms / 1000);
+    if (seconds < 60) return `${seconds}秒`;
+    const minutes = Math.floor(seconds / 60);
+    const remainingSeconds = seconds % 60;
+    return `${minutes}分${remainingSeconds}秒`;
+  }
+
+  /**
+   * 获取当前进度
+   */
+  getProgress() {
+    return { ...this.progress };
+  }
+}
+
+// 导出单例实例
+export const progressManager = new ProgressManager();
+
+// 导出类
+export default ProgressManager;

+ 119 - 3
src/views/order/factory/index.vue

@@ -16,6 +16,14 @@
       @refresh-change="refreshChange"
       @on-load="onLoad"
     >
+      <!-- 工具栏左侧按钮 -->
+      <template slot="menuLeft">
+        <ExportButton
+          :selected-count="selectedCount"
+          :loading="isExporting"
+          @export="handleExport"
+        />
+      </template>
       <!-- 订单类型显示 -->
       <template slot="orderType" slot-scope="{row}">
         <el-tag :type="getOrderTypeTagType(row.orderType)">
@@ -59,6 +67,15 @@
         </div>
       </template>
     </avue-crud>
+
+    <!-- 导出进度对话框 -->
+    <ExportProgressDialog
+      :visible.sync="showProgressDialog"
+      :progress="exportProgress"
+      @cancel="handleExportCancel"
+      @retry="handleExportRetry"
+      @close="handleExportClose"
+    />
   </basic-container>
 </template>
 
@@ -74,10 +91,17 @@ import { getOrderTypeLabel, getOrderTypeTagType, getOrderStatusLabel, getOrderSt
 import OrderItemTable from '@/components/order-item-table/index.vue'
 import { safeBigInt } from '@/util/util'
 import { getList as getOrderItemList } from '@/api/order/order-item'
+import ExportButton from '@/components/export/ExportButton.vue'
+import ExportProgressDialog from '@/components/export/ExportProgressDialog.vue'
+import { getExportService } from '@/api/export-service'
 
 export default {
   name: 'FactoryOrderList',
-  components: { OrderItemTable },
+  components: {
+    OrderItemTable,
+    ExportButton,
+    ExportProgressDialog
+  },
   data() {
     // 深拷贝基础配置,禁用新增/编辑/查看/删除,保持与订单模块一致的列和搜索
     const opt = JSON.parse(JSON.stringify(baseOption || {}))
@@ -102,15 +126,37 @@ export default {
       selectionList: [],
       page: { pageSize: 10, currentPage: 1, total: 0 },
       // 记录最后一次有效的搜索参数,保证分页时参数不丢失
-      lastQuery: {}
+      lastQuery: {},
+      // 导出相关状态
+      isExporting: false,
+      showProgressDialog: false,
+      exportProgress: {
+        percentage: 0,
+        status: 'active',
+        currentText: '',
+        detail: '',
+        cancellable: true,
+        current: 0,
+        total: 0,
+        remainingTime: ''
+      },
+      exportService: null
     }
   },
   computed: {
     // 禁用所有内置操作按钮
     permissionList() {
       return { addBtn: false, viewBtn: false, editBtn: false, delBtn: false }
+    },
+    // 选中的订单数量(简化为只使用本地选择)
+    selectedCount() {
+      return this.selectionList.length;
     }
   },
+  created() {
+    // 初始化导出服务(无需store依赖)
+    this.exportService = getExportService();
+  },
   methods: {
     // 常量/标签方法透传,供插槽使用
     getOrderTypeLabel,
@@ -201,7 +247,8 @@ export default {
     /** 选择变化 */
     /** @param {FactoryOrderRecord[]} list */
     selectionChange(list) {
-      this.selectionList = list
+      console.log('Selection changed:', list.length, 'items selected');
+      this.selectionList = list;
     },
     /** 页码变化:保持搜索条件不丢失并重新加载 */
     /** @param {number} currentPage */
@@ -218,6 +265,75 @@ export default {
     /** 刷新 */
     refreshChange() {
       this.onLoad(this.page, this.lastQuery)
+    },
+
+    // ========== 导出功能相关方法 ==========
+
+    /**
+     * 处理导出操作
+     * @param {string} type 导出类型
+     */
+    async handleExport(type = 'selected') {
+      if (this.isExporting) {
+        this.$message.warning('导出正在进行中,请稍后重试');
+        return;
+      }
+
+      // 使用本地选择的订单
+      const selectedOrders = this.selectionList;
+      if (!selectedOrders || selectedOrders.length === 0) {
+        this.$message.warning('请先选择要导出的订单');
+        return;
+      }
+
+      console.log('开始导出', selectedOrders.length, '个订单');
+
+      try {
+        this.isExporting = true;
+        this.showProgressDialog = true;
+
+        // 注册进度回调
+        this.exportService.progressManager.onProgress((progress) => {
+          this.exportProgress = progress;
+        });
+
+        // 执行导出(直接传入选中的订单)
+        await this.exportService.executeExport({ type }, selectedOrders);
+
+      } catch (error) {
+        console.error('导出失败:', error);
+        this.$message.error(error.message || '导出失败,请稍后重试');
+      } finally {
+        this.isExporting = false;
+      }
+    },
+
+    /**
+     * 处理导出取消
+     */
+    handleExportCancel() {
+      if (this.exportService) {
+        this.exportService.cancelExport();
+      }
+      this.showProgressDialog = false;
+    },
+
+    /**
+     * 处理导出重试
+     */
+    handleExportRetry() {
+      this.showProgressDialog = false;
+      // 延迟一秒后重试
+      setTimeout(() => {
+        this.handleExport('selected');
+      }, 1000);
+    },
+
+    /**
+     * 处理导出对话框关闭
+     */
+    handleExportClose() {
+      this.showProgressDialog = false;
     }
   }
 }