项目-交付执行.md 35 KB

项目管理 - 交付执行阶段 PRD (Product表版本)

1. 功能概述

1.1 阶段定位

交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。

1.2 核心目标

  • 多产品设计协同管理:支持单产品设计到多产品设计项目的灵活管理
  • 按产品设计维度组织文件上传和进度管理
  • 实现四个执行阶段的串行推进
  • 跨产品设计协调与依赖管理:处理产品设计间的风格一致性、色彩流线、材质匹配
  • 提供实时进度跟踪和状态可视化
  • 支持组长审核和质量把控
  • 确保交付物符合质量标准

1.3 涉及角色

  • 设计师:负责建模、软装、后期等设计工作
  • 渲染师:负责渲染阶段的大图输出
  • 组长:审核各阶段交付物、把控质量
  • 技术:验收最终交付物、确认质量

1.4 四大执行子阶段

graph LR
    A[方案确认] --> B[建模]
    B --> C[软装]
    C --> D[渲染]
    D --> E[后期]
    E --> F[尾款结算]

    style B fill:#e3f2fd
    style C fill:#fff3e0
    style D fill:#fce4ec
    style E fill:#f3e5f5

2. 基于Product表的交付管理系统

2.1 产品交付管理架构

2.1.1 增强的DeliveryProcess接口

interface ProductDeliveryProcess {
  id: string;                           // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
  name: string;                         // 流程名称:建模/软装/渲染/后期
  type: 'modeling' | 'softDecor' | 'rendering' | 'postProcess';
  isExpanded: boolean;                  // 是否展开

  // 产品管理(基于Product表)
  products: ProductDelivery[];
  content: Record<string, ProductContent>;

  // 跨产品协调(基于Product表)
  crossProductCoordination: {
    dependencies: ProductDependency[];     // 产品依赖关系
    batchOperations: BatchOperation[];    // 批量操作
    qualityStandards: QualityStandard[];  // 质量标准
  };

  // 整体进度管理
  overallProgress: {
    total: number;                          // 总体进度
    byProduct: Record<string, number>;     // 各产品进度
    byStage: Record<string, number>;       // 各阶段进度
    estimatedCompletion: Date;
  };
}

interface ProductDelivery {
  productId: string;                     // 产品ID(与Product.objectId关联)
  productName: string;                   // 产品名称:李总主卧设计
  productType: string;                   // 产品类型:bedroom
  isExpanded: boolean;                  // 是否展开
  order: number;                         // 排序顺序
  priority: number;                      // 优先级
  status: ProductStatus;                 // 产品状态
  assigneeId: string;                    // 负责人ID(Product.profile)
  estimatedHours: number;                // 预估工时
  actualHours: number;                   // 实际工时

  // 产品报价信息
  quotation: {
    price: number;
    breakdown: {
      design: number;
      modeling: number;
      rendering: number;
      softDecor: number;
    };
    status: string;
  };
}

interface ProductContent {
  // 产品文件(基于ProjectFile分类)
  files: Array<{
    id: string;
    name: string;
    url: string;
    size?: string;
    fileCategory: string;               // 'delivery' | 'reference' | 'other'
    reviewStatus?: 'pending' | 'approved' | 'rejected';
    synced?: boolean;                    // 是否已同步到客户端
    uploadTime: Date;                     // 上传时间
    uploadedBy: string;                   // 上传人
  }>;

  progress: number;                      // 进度 0-100
  status: 'pending' | 'in_progress' | 'completed' | 'approved';
  notes: string;                         // 备注信息
  lastUpdated: Date;                     // 最后更新时间

  // 产品特定字段
  productSpecific: {
    // 建模阶段特有
    modelingComplexity?: 'simple' | 'medium' | 'complex';
    structuralConstraints?: string[];

    // 软装阶段特有
    furnitureList?: string[];
    materialSelection?: string[];

    // 渲染阶段特有
    renderingQuality?: 'standard' | 'high' | 'ultra';
    outputResolution?: string;

    // 后期阶段特有
    postProcessingTypes?: string[];
    finalTouches?: string[];
  };
}

2.1.2 基于Product表的交付管理服务

class ProductDeliveryService {
  // 获取项目的交付管理数据
  async getProjectDeliveryData(projectId: string): Promise<ProductDeliveryProcess[]> {
    // 1. 获取项目的所有产品
    const productQuery = new Parse.Query("Product");
    productQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
    productQuery.include("profile");
    productQuery.ascending("order");
    const products = await productQuery.find();

    // 2. 构建交付管理数据结构
    const deliveryProcesses: ProductDeliveryProcess[] = [];

    for (const stage of ['modeling', 'softDecor', 'rendering', 'postProcess']) {
      const process: ProductDeliveryProcess = {
        id: stage,
        name: this.getStageName(stage),
        type: stage as any,
        isExpanded: false,
        products: [],
        content: {},
        crossProductCoordination: {
          dependencies: [],
          batchOperations: [],
          qualityStandards: []
        },
        overallProgress: {
          total: 0,
          byProduct: {},
          byStage: {},
          estimatedCompletion: new Date()
        }
      };

      // 3. 为每个产品构建交付数据
      for (const product of products) {
        const productDelivery: ProductDelivery = {
          productId: product.id,
          productName: product.get("productName"),
          productType: product.get("productType"),
          isExpanded: false,
          order: product.get("order") || 0,
          priority: product.get("space")?.priority || 5,
          status: product.get("status") || "not_started",
          assigneeId: product.get("profile")?.id,
          estimatedHours: product.get("estimatedDuration") * 8, // 天数转小时
          actualHours: 0,
          quotation: product.get("quotation") || {}
        };

        // 4. 获取产品的交付文件
        const productFiles = await this.getProductDeliveryFiles(product.id, stage);

        // 5. 构建产品内容
        const productContent: ProductContent = {
          files: productFiles.map(file => ({
            id: file.id,
            name: file.get("fileName"),
            url: file.get("fileUrl"),
            size: this.formatFileSize(file.get("fileSize")),
            fileCategory: file.get("fileCategory"),
            reviewStatus: file.get("data")?.reviewStatus || "pending",
            uploadTime: file.get("createdAt"),
            uploadedBy: file.get("uploadedBy")?.get("name")
          })),
          progress: this.calculateProductProgress(product, stage),
          status: this.getProductStatus(product, stage),
          notes: product.get("data")?.deliveryNotes || "",
          lastUpdated: product.get("updatedAt"),
          productSpecific: this.getProductSpecificFields(product, stage)
        };

        process.products.push(productDelivery);
        process.content[product.id] = productContent;
        process.overallProgress.byProduct[product.id] = productContent.progress;
      }

      deliveryProcesses.push(process);
    }

    return deliveryProcesses;
  }

  // 获取产品的交付文件
  async getProductDeliveryFiles(productId: string, stage: string): Promise<Parse.Object[]> {
    const fileQuery = new Parse.Query("ProjectFile");
    fileQuery.equalTo("product", { __type: "Pointer", className: "Product", objectId: productId });
    fileQuery.equalTo("fileCategory", "delivery");
    fileQuery.equalTo("stage", stage);
    fileQuery.notEqualTo("isDeleted", true);
    fileQuery.descending("createdAt");

    return await fileQuery.find();
  }

  // 计算产品进度
  calculateProductProgress(product: Parse.Object, stage: string): number {
    const productFiles = product.get("deliveryFiles") || [];
    const stageFiles = productFiles.filter((file: any) => file.stage === stage);

    if (stageFiles.length === 0) return 0;

    const completedFiles = stageFiles.filter((file: any) =>
      file.reviewStatus === "approved"
    );

    return Math.round((completedFiles.length / stageFiles.length) * 100);
  }

  // 获取产品状态
  getProductStatus(product: Parse.Object, stage: string): string {
    const currentStage = product.get("stage");

    if (currentStage === stage) {
      return product.get("status") || "not_started";
    } else if (this.isStageCompleted(stage, currentStage)) {
      return "completed";
    } else {
      return "pending";
    }
  }

  // 获取产品特定字段
  getProductSpecificFields(product: Parse.Object, stage: string): any {
    const space = product.get("space") || {};
    const baseFields = {
      structuralConstraints: space.constraints || []
    };

    switch (stage) {
      case "modeling":
        return {
          ...baseFields,
          modelingComplexity: space.complexity || "medium"
        };

      case "softDecor":
        return {
          ...baseFields,
          furnitureList: product.get("data")?.furnitureList || [],
          materialSelection: product.get("data")?.materialSelection || []
        };

      case "rendering":
        return {
          ...baseFields,
          renderingQuality: product.get("data")?.renderingQuality || "standard",
          outputResolution: product.get("data")?.outputResolution || "1920x1080"
        };

      case "postProcess":
        return {
          ...baseFields,
          postProcessingTypes: product.get("data")?.postProcessingTypes || [],
          finalTouches: product.get("data")?.finalTouches || []
        };

      default:
        return baseFields;
    }
  }
}

2.2 产品交付进度管理

2.2.1 进度跟踪服务

class ProductProgressService {
  // 更新产品进度
  async updateProductProgress(
    productId: string,
    stage: string,
    progressData: ProgressUpdateData
  ): Promise<void> {
    const productQuery = new Parse.Query("Product");
    const product = await productQuery.get(productId);

    // 更新产品状态
    if (progressData.status) {
      product.set("status", progressData.status);
    }

    if (progressData.stage) {
      product.set("stage", progressData.stage);
    }

    // 更新产品数据
    const currentData = product.get("data") || {};
    const updatedData = {
      ...currentData,
      [`${stage}Progress`]: progressData.progress,
      [`${stage}Notes`]: progressData.notes,
      [`${stage}LastUpdated`]: new Date()
    };

    product.set("data", updatedData);
    await product.save();

    // 触发进度更新事件
    this.emitProgressUpdate(productId, stage, progressData);
  }

  // 批量更新产品进度
  async batchUpdateProgress(
    productIds: string[],
    stage: string,
    progressData: ProgressUpdateData
  ): Promise<void> {
    const productQuery = new Parse.Query("Product");
    productQuery.containedIn("objectId", productIds);
    const products = await productQuery.find();

    for (const product of products) {
      await this.updateProductProgress(product.id, stage, progressData);
    }
  }

  // 计算整体项目进度
  calculateOverallProjectProgress(
    deliveryProcesses: ProductDeliveryProcess[]
  ): ProjectProgress {
    const totalProducts = deliveryProcesses.reduce((sum, process) => sum + process.products.length, 0);

    if (totalProducts === 0) {
      return { total: 0, byStage: {}, byProduct: {} };
    }

    const progress: ProjectProgress = {
      total: 0,
      byStage: {},
      byProduct: {}
    };

    // 计算各阶段进度
    deliveryProcesses.forEach(process => {
      const stageProgress = process.products.reduce((sum, product) => {
        return sum + (process.content[product.productId]?.progress || 0);
      }, 0);

      progress.byStage[process.id] = Math.round(stageProgress / process.products.length);
    });

    // 计算各产品进度
    deliveryProcesses.forEach(process => {
      process.products.forEach(product => {
        const productProgress = process.content[product.productId]?.progress || 0;
        progress.byProduct[product.productId] = productProgress;
      });
    });

    // 计算总体进度
    const stageSum = Object.values(progress.byStage).reduce((sum, val) => sum + val, 0);
    progress.total = Math.round(stageSum / Object.keys(progress.byStage).length);

    return progress;
  }
}

2.3 跨产品协调管理

2.3.1 产品依赖管理

class ProductDependencyManager {
  // 分析产品间依赖关系
  analyzeProductDependencies(products: Product[]): ProductDependency[] {
    const dependencies: ProductDependency[] = [];

    // 基于产品类型分析依赖
    for (let i = 0; i < products.length; i++) {
      for (let j = i + 1; j < products.length; j++) {
        const fromProduct = products[i];
        const toProduct = products[j];

        const dependency = this.analyzeDependency(fromProduct, toProduct);
        if (dependency) {
          dependencies.push(dependency);
        }
      }
    }

    return dependencies;
  }

  // 分析两个产品间的依赖关系
  private analyzeDependency(
    fromProduct: Product,
    toProduct: Product
  ): ProductDependency | null {
    const fromType = fromProduct.productType;
    const toType = toProduct.productType;

    // 定义产品类型间的依赖关系
    const dependencyRules: Record<string, { dependsOn: string[]; reason: string }> = {
      'living_room': {
        dependsOn: [],
        reason: '客厅通常是风格参考的起点'
      },
      'dining_room': {
        dependsOn: ['living_room'],
        reason: '餐厅通常需要与客厅风格保持一致'
      },
      'kitchen': {
        dependsOn: ['dining_room'],
        reason: '厨房与餐厅在空间和功能上紧密相关'
      },
      'bedroom': {
        dependsOn: ['living_room', 'corridor'],
        reason: '卧室通常需要参考客厅的整体风格'
      },
      'bathroom': {
        dependsOn: ['bedroom'],
        reason: '卫生间与卧室在功能上紧密相关'
      },
      'balcony': {
        dependsOn: ['living_room', 'bedroom'],
        reason: '阳台通常连接客厅或卧室'
      }
    };

    const rule = dependencyRules[toType];
    if (rule && rule.dependsOn.includes(fromType)) {
      return {
        id: `${fromProduct.productId}-${toProduct.productId}`,
        fromProductId: fromProduct.productId,
        toProductId: toProduct.productId,
        fromProductName: fromProduct.productName,
        toProductName: toProduct.productName,
        type: 'style_reference',
        description: rule.reason,
        priority: this.calculateDependencyPriority(fromType, toType),
        confidence: 0.8
      };
    }

    return null;
  }

  // 生成协调建议
  generateCoordinationSuggestions(
    dependencies: ProductDependency[]
  ): CoordinationSuggestion[] {
    const suggestions: CoordinationSuggestion[] = [];

    dependencies.forEach(dep => {
      suggestions.push({
        id: `coord-${dep.id}`,
        dependencyId: dep.id,
        type: 'style_consistency',
        title: `风格一致性建议:${dep.fromProductName} → ${dep.toProductName}`,
        description: `建议${dep.toProductName}在设计时参考${dep.fromProductName}的整体风格,确保空间的协调统一`,
        actions: [
          `参考${dep.fromProductName}的色彩方案`,
          `保持材质选择的一致性`,
          `考虑空间功能的连续性`
        ],
        priority: dep.priority
      });
    });

    return suggestions;
  }
}

2.4 批量操作管理

2.4.1 批量操作服务

class BatchOperationService {
  // 批量上传文件
  async batchUploadFiles(
    productIds: string[],
    files: File[],
    fileCategory: string,
    stage: string,
    uploaderId: string
  ): Promise<BatchUploadResult> {
    const results: BatchUploadResult = {
      successful: [],
      failed: [],
      summary: {
        total: files.length,
        uploaded: 0,
        failed: 0
      }
    };

    for (const file of files) {
      try {
        // 为每个产品上传文件
        for (const productId of productIds) {
          const projectFile = new Parse.Object("ProjectFile");
          projectFile.set("product", { __type: "Pointer", className: "Product", objectId: productId });
          projectFile.set("fileCategory", fileCategory);
          projectFile.set("stage", stage);
          projectFile.set("uploadedBy", { __type: "Pointer", className: "Profile", objectId: uploaderId });

          // 设置文件基本信息
          projectFile.set("fileName", file.name);
          projectFile.set("fileSize", file.size);

          // 上传文件到存储
          const fileData = await this.uploadFile(file);
          projectFile.set("fileUrl", fileData.url);
          projectFile.set("attach", fileData.attachment);

          await projectFile.save();
          results.successful.push({
            productId,
            fileName: file.name,
            fileId: projectFile.id
          });
        }

        results.summary.uploaded++;
      } catch (error) {
        results.failed.push({
          fileName: file.name,
          error: error.message
        });
        results.summary.failed++;
      }
    }

    return results;
  }

  // 批量更新产品状态
  async batchUpdateProductStatus(
    productIds: string[],
    status: string,
    stage: string
  ): Promise<void> {
    const productQuery = new Parse.Query("Product");
    productQuery.containedIn("objectId", productIds);
    const products = await productQuery.find();

    for (const product of products) {
      product.set("status", status);
      product.set("stage", stage);
      await product.save();
    }
  }

  // 批量发送审核通知
  async batchSendReviewNotifications(
    productIds: string[],
    stage: string,
    reviewerIds: string[]
  ): Promise<void> {
    // 获取需要审核的产品
    const productQuery = new Parse.Query("Product");
    productQuery.containedIn("objectId", productIds);
    productQuery.include("profile");
    const products = await productQuery.find();

    // 发送通知给审核人员
    for (const reviewerId of reviewerIds) {
      for (const product of products) {
        await this.sendReviewNotification({
          reviewerId,
          productId: product.id,
          productName: product.get("productName"),
          stage,
          designerId: product.get("profile")?.id
        });
      }
    }
  }
}

3. 交付执行界面设计

3.1 产品交付管理主界面

<!-- 产品交付管理主界面 -->
<div class="product-delivery-container">
  <!-- 阶段导航 -->
  <div class="stage-navigation">
    <div class="stage-tabs">
      <div v-for="stage in deliveryStages"
           :key="stage.id"
           class="stage-tab"
           :class="{ active: activeStage === stage.id }"
           @click="switchStage(stage.id)">
        <div class="stage-icon">
          <i :class="getStageIcon(stage.id)"></i>
        </div>
        <div class="stage-info">
          <h4>{{ stage.name }}</h4>
          <div class="progress-bar">
            <div class="progress-fill"
                 :style="{ width: stage.progress + '%' }"></div>
          </div>
          <span class="progress-text">{{ stage.progress }}%</span>
        </div>
      </div>
    </div>
  </div>

  <!-- 批量操作工具栏 -->
  <div class="batch-operations">
    <div class="operation-group">
      <button class="btn btn-primary"
              @click="showBatchUpload = true"
              :disabled="selectedProducts.length === 0">
        <i class="fas fa-upload"></i>
        批量上传文件
      </button>

      <button class="btn btn-secondary"
              @click="showBatchStatusUpdate = true"
              :disabled="selectedProducts.length === 0">
        <i class="fas fa-edit"></i>
        批量更新状态
      </button>

      <button class="btn btn-info"
              @click="generateCoordinationReport">
        <i class="fas fa-project-diagram"></i>
        协调报告
      </button>
    </div>

    <div class="selection-info">
      <span v-if="selectedProducts.length > 0">
        已选择 {{ selectedProducts.length }} 个产品
      </span>
    </div>
  </div>

  <!-- 产品列表 -->
  <div class="product-delivery-section">
    <h3>产品交付管理 - {{ getStageName(activeStage) }}</h3>

    <div class="product-delivery-grid">
      <div v-for="product in filteredProducts"
           :key="product.productId"
           class="product-delivery-card"
           :class="{
             active: selectedProducts.includes(product.productId),
             expanded: product.isExpanded,
             'status-' + product.status
           }"
           @click="toggleProductExpansion(product.productId)">

        <!-- 产品基本信息 -->
        <div class="product-header">
          <div class="product-info">
            <h4>{{ product.productName }}</h4>
            <span class="product-type">{{ getProductTypeLabel(product.productType) }}</span>
            <div class="designer-info">
              <img :src="product.designerAvatar" class="designer-avatar" />
              <span>{{ product.designerName }}</span>
            </div>
          </div>

          <div class="product-status">
            <span class="status-badge" :class="product.status">
              {{ getStatusLabel(product.status) }}
            </span>
          </div>

          <div class="product-actions">
            <label class="checkbox-wrapper">
              <input type="checkbox"
                     :value="product.productId"
                     v-model="selectedProducts"
                     @click.stop>
              <span class="checkmark"></span>
            </label>
          </div>
        </div>

        <!-- 产品进度信息 -->
        <div class="product-progress">
          <div class="progress-stats">
            <div class="stat-item">
              <label>进度</label>
              <span class="progress-value">{{ product.content.progress }}%</span>
            </div>
            <div class="stat-item">
              <label>文件</label>
              <span class="file-count">{{ product.content.files.length }}</span>
            </div>
            <div class="stat-item">
              <label>工时</label>
              <span class="hours">{{ product.actualHours }}/{{ product.estimatedHours }}h</span>
            </div>
          </div>

          <div class="progress-bar">
            <div class="progress-fill"
                 :style="{ width: product.content.progress + '%' }"></div>
          </div>
        </div>

        <!-- 产品报价信息 -->
        <div class="product-quotation" v-if="product.quotation">
          <div class="quotation-header">
            <span class="quotation-price">¥{{ product.quotation.price.toLocaleString() }}</span>
            <span class="quotation-status" :class="product.quotation.status">
              {{ getQuotationStatusLabel(product.quotation.status) }}
            </span>
          </div>

          <div class="quotation-breakdown">
            <div v-for="(item, key) in product.quotation.breakdown"
                 :key="key"
                 class="breakdown-item">
              <span class="breakdown-type">{{ getBreakdownTypeLabel(key) }}:</span>
              <span class="breakdown-amount">¥{{ item.toLocaleString() }}</span>
            </div>
          </div>
        </div>

        <!-- 展开的详细内容 -->
        <div v-if="product.isExpanded" class="product-details">
          <!-- 文件管理 -->
          <div class="file-management">
            <h5>交付文件</h5>
            <div class="file-list">
              <div v-for="file in product.content.files"
                   :key="file.id"
                   class="file-item"
                   :class="'status-' + file.reviewStatus">
                <div class="file-info">
                  <i :class="getFileIcon(file.name)"></i>
                  <div class="file-details">
                    <span class="file-name">{{ file.name }}</span>
                    <span class="file-size">{{ file.size }}</span>
                  </div>
                </div>

                <div class="file-actions">
                  <button class="btn-sm"
                          @click="previewFile(file)"
                          title="预览">
                    <i class="fas fa-eye"></i>
                  </button>
                  <button class="btn-sm"
                          @click="downloadFile(file)"
                          title="下载">
                    <i class="fas fa-download"></i>
                  </button>
                  <button class="btn-sm btn-success"
                          v-if="file.reviewStatus === 'pending'"
                          @click="approveFile(file)"
                          title="审核通过">
                    <i class="fas fa-check"></i>
                  </button>
                </div>
              </div>
            </div>

            <!-- 文件上传按钮 -->
            <div class="file-upload">
              <button class="btn btn-sm btn-outline"
                      @click="showFileUpload = true">
                <i class="fas fa-plus"></i>
                上传文件
              </button>
            </div>
          </div>

          <!-- 产品特定字段 -->
          <div class="product-specific-fields">
            <h5>产品特定信息</h5>
            <div class="specific-fields-grid">
              <div v-for="(value, key) in product.content.productSpecific"
                   :key="key"
                   class="field-item">
                <label>{{ getFieldLabel(key) }}:</label>
                <span>{{ formatFieldValue(key, value) }}</span>
              </div>
            </div>
          </div>

          <!-- 备注和日志 -->
          <div class="notes-section">
            <h5>备注信息</h5>
            <textarea class="notes-textarea"
                      v-model="product.content.notes"
                      @blur="updateProductNotes(product.productId)"
                      placeholder="添加备注信息..."></textarea>

            <div class="update-log">
              <small>最后更新: {{ formatDateTime(product.content.lastUpdated) }}</small>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- 协调报告 -->
  <div class="coordination-report" v-if="coordinationReport">
    <h3>产品协调报告</h3>
    <div class="report-summary">
      <div class="summary-item">
        <label>依赖关系:</label>
        <span>{{ coordinationReport.dependencies.length }}个</span>
      </div>
      <div class="summary-item">
        <label>协调建议:</label>
        <span>{{ coordinationReport.suggestions.length }}个</span>
      </div>
    </div>

    <div class="report-details">
      <!-- 依赖关系 -->
      <div class="dependencies-section">
        <h4>产品依赖关系</h4>
        <div class="dependency-list">
          <div v-for="dep in coordinationReport.dependencies"
               :key="dep.id"
               class="dependency-item">
            <div class="dependency-arrow">
              <i class="fas fa-arrow-right"></i>
            </div>
            <div class="dependency-content">
              <span class="from-product">{{ dep.fromProductName }}</span>
              <span class="dependency-type">→</span>
              <span class="to-product">{{ dep.toProductName }}</span>
              <div class="dependency-description">{{ dep.description }}</div>
            </div>
          </div>
        </div>
      </div>

      <!-- 协调建议 -->
      <div class="suggestions-section">
        <h4>协调建议</h4>
        <div class="suggestion-list">
          <div v-for="suggestion in coordinationReport.suggestions"
               :key="suggestion.id"
               class="suggestion-item"
               :class="'priority-' + suggestion.priority">
            <div class="suggestion-header">
              <i class="fas fa-lightbulb"></i>
              <span class="suggestion-title">{{ suggestion.title }}</span>
            </div>
            <div class="suggestion-description">{{ suggestion.description }}</div>
            <div class="suggestion-actions">
              <span v-for="action in suggestion.actions" :key="action" class="action-item">
                • {{ action }}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

3.2 批量操作界面

<!-- 批量上传文件弹窗 -->
<div v-if="showBatchUpload" class="modal-overlay" @click="showBatchUpload = false">
  <div class="modal-content" @click.stop>
    <div class="modal-header">
      <h3>批量上传文件</h3>
      <button class="close-button" @click="showBatchUpload = false">×</button>
    </div>

    <div class="modal-body">
      <div class="upload-area"
           @dragover.prevent="onDragOver"
           @drop.prevent="onDrop"
           :class="{ 'drag-over': isDragOver }">
        <i class="fas fa-cloud-upload-alt upload-icon"></i>
        <p>拖拽文件到此处或点击选择文件</p>
        <input type="file"
               multiple
               ref="fileInput"
               @change="onFileSelect"
               style="display: none;">
        <button class="btn btn-primary"
                @click="$refs.fileInput.click()">
          选择文件
        </button>
      </div>

      <div class="file-list" v-if="selectedFiles.length > 0">
        <h4>选择的文件 ({{ selectedFiles.length }})</h4>
        <div class="file-items">
          <div v-for="(file, index) in selectedFiles"
               :key="index"
               class="file-item">
            <div class="file-info">
              <i :class="getFileIcon(file.name)"></i>
              <span class="file-name">{{ file.name }}</span>
              <span class="file-size">{{ formatFileSize(file.size) }}</span>
            </div>
            <button class="btn-sm btn-danger"
                    @click="removeFile(index)">
              <i class="fas fa-times"></i>
            </button>
          </div>
        </div>
      </div>

      <div class="upload-options">
        <div class="form-group">
          <label>文件分类:</label>
          <select v-model="batchUploadOptions.fileCategory">
            <option value="delivery">交付物文件</option>
            <option value="reference">参考文件</option>
            <option value="document">文档资料</option>
          </select>
        </div>

        <div class="form-group">
          <label>目标阶段:</label>
          <select v-model="batchUploadOptions.stage">
            <option value="modeling">建模</option>
            <option value="softDecor">软装</option>
            <option value="rendering">渲染</option>
            <option value="postProcess">后期</option>
          </select>
        </div>

        <div class="form-group">
          <label>目标产品:</label>
          <select v-model="batchUploadOptions.targetProducts" multiple>
            <option v-for="product in products"
                    :key="product.productId"
                    :value="product.productId">
              {{ product.productName }}
            </option>
          </select>
        </div>
      </div>
    </div>

    <div class="modal-footer">
      <button class="btn btn-secondary"
              @click="showBatchUpload = false">
        取消
      </button>
      <button class="btn btn-primary"
              :disabled="selectedFiles.length === 0 || batchUploadOptions.targetProducts.length === 0"
              @click="executeBatchUpload">
        开始上传
      </button>
    </div>
  </div>
</div>

<!-- 批量状态更新弹窗 -->
<div v-if="showBatchStatusUpdate" class="modal-overlay" @click="showBatchStatusUpdate = false">
  <div class="modal-content" @click.stop>
    <div class="modal-header">
      <h3>批量更新状态</h3>
      <button class="close-button" @click="showBatchStatusUpdate = false">×</button>
    </div>

    <div class="modal-body">
      <div class="form-group">
        <label>更新状态:</label>
        <select v-model="batchStatusOptions.status">
          <option value="not_started">未开始</option>
          <option value="in_progress">进行中</option>
          <option value="awaiting_review">待审核</option>
          <option value="completed">已完成</option>
        </select>
      </div>

      <div class="form-group">
        <label>更新阶段:</label>
        <select v-model="batchStatusOptions.stage">
          <option value="modeling">建模</option>
          <option value="softDecor">软装</option>
          <option value="rendering">渲染</option>
          <option value="postProcess">后期</option>
        </select>
      </div>

      <div class="form-group">
        <label>备注:</label>
        <textarea v-model="batchStatusOptions.notes"
                  placeholder="批量更新备注..."
                  rows="3"></textarea>
      </div>

      <div class="affected-products">
        <h4>将更新的产品 ({{ selectedProducts.length }})</h4>
        <div class="product-list">
          <div v-for="product in selectedProducts"
               :key="product.productId"
               class="affected-product">
            <span class="product-name">{{ product.productName }}</span>
            <span class="product-type">{{ product.productType }}</span>
            <span class="current-status">{{ product.status }}</span>
          </div>
        </div>
      </div>
    </div>

    <div class="modal-footer">
      <button class="btn btn-secondary"
              @click="showBatchStatusUpdate = false">
        取消
      </button>
      <button class="btn btn-primary"
              @click="executeBatchStatusUpdate">
        确认更新
      </button>
    </div>
  </div>
</div>

4. 技术实现要点

4.1 性能优化

  • 懒加载:按需加载产品详情和文件列表
  • 虚拟滚动:处理大量产品时的性能问题
  • 缓存机制:缓存产品状态和进度数据

4.2 用户体验优化

  • 拖拽上传:支持文件拖拽批量上传
  • 实时同步:进度更新实时推送到界面
  • 离线支持:基本的离线操作支持

4.3 数据一致性

  • 事务处理:确保批量操作的数据一致性
  • 冲突检测:检测并发修改冲突
  • 版本控制:文件版本管理和回滚

文档版本: v3.0 (Product表统一空间管理) 最后更新: 2025-10-20 维护者: YSS Development Team