项目-交付执行.md 69 KB

项目管理 - 交付执行阶段 PRD (基于Product表和NovaStorage)

1. 功能概述

1.1 阶段定位

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

1.2 核心目标

  • 多空间产品协同管理:基于Product表统一管理各空间设计产品的交付执行
  • 四阶段全流程管控:白模→软装→渲染→后期的完整执行链路
  • 文件管理与存储集成:基于NovaStorage的文件上传,prefixKeys=project/:pid/,Attachment与ProjectFile同步保存
  • ProjectTeam协作管理:根据团队成员角色和技能,合理分配各阶段任务
  • 交付物分类管理:按四大核心内容(白模\软装\渲染\后期)分类管理交付物
  • 质量把控与审核:支持组长审核和质量标准验证
  • 实时进度跟踪:多维度进度监控和状态可视化

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

1.5 四大核心交付内容

  1. 白模建模:空间结构建模、基础框架搭建
  2. 软装设计:家具配置、材质选择、色彩搭配
  3. 渲染输出:高清效果图、全景图、细节特写
  4. 后期处理:色彩调整、效果优化、最终成品

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

2.1 交付执行管理架构

2.1.1 核心数据关系

// Product产品表 - 空间设计产品核心
interface Product {
  objectId: string;
  project: Pointer<Project>;
  profile: Pointer<Profile>;          // 负责设计师
  productName: string;                // 产品名称:李总主卧设计
  productType: string;                // 空间类型:bedroom
  stage: string;                      // 当前阶段:modeling/softDecor/rendering/postProcess
  status: string;                     // 状态:not_started/in_progress/completed
  quotation: Object;                  // 产品报价信息
  requirements: Object;               // 设计需求
  space: Object;                      // 空间信息
  data: Object;                       // 扩展数据
}

// ProjectTeam项目团队表 - 团队成员管理
interface ProjectTeam {
  objectId: string;
  project: Pointer<Project>;
  profile: Pointer<Profile>;
  role: string;                       // designer/renderer/team_leader/technical
  workload: Number;                   // 工作量
  specialties: string[];              // 专业技能
  assignedProducts: string[];         // 分配的产品ID列表
}

// ProjectFile项目文件表 - 交付物管理
interface ProjectFile {
  objectId: string;
  project: Pointer<Project>;
  product: Pointer<Product>;          // 关联的空间产品
  attach: Pointer<Attachment>;        // NovaStorage附件
  category: string;                   // white_model/soft_decor/rendering/post_process
  stage: string;                      // modeling/softDecor/rendering/postProcess
  uploadedBy: Pointer<Profile>;
  data: Object;                       // 文件元数据、审核状态等
}

2.1.2 基于NovaStorage的文件管理

// 文件上传服务 - 使用NovaStorage
class DeliveryFileService {
  private storage: NovaStorage;

  constructor() {
    const cid = localStorage.getItem('company')!;
    this.storage = NovaStorage.withCid(cid);
  }

  // 上传交付文件
  async uploadDeliveryFile(
    projectId: string,
    productId: string,
    category: string,
    file: File
  ): Promise<{ attachment: Parse.Object; projectFile: Parse.Object }> {

    // 1. 上传到NovaStorage,使用项目前缀
    const novaFile: NovaFile = await this.storage.upload(file, {
      prefixKey: `project/${projectId}/`,
      onProgress: (progress) => {
        console.log('Upload progress:', progress.total.percent);
      }
    });

    // 2. 创建Attachment记录
    const attachment = new Parse.Object("Attachment");
    attachment.set("name", novaFile.name);
    attachment.set("url", novaFile.url);
    attachment.set("size", novaFile.size);
    attachment.set("mime", novaFile.type);
    attachment.set("md5", novaFile.md5);
    attachment.set("metadata", novaFile.metadata);
    attachment.set("company", { __type: "Pointer", className: "Company", objectId: cid });
    await attachment.save();

    // 3. 创建ProjectFile记录
    const projectFile = new Parse.Object("ProjectFile");
    projectFile.set("project", { __type: "Pointer", className: "Project", objectId: projectId });
    projectFile.set("product", { __type: "Pointer", className: "Product", objectId: productId });
    projectFile.set("attach", { __type: "Pointer", className: "Attachment", objectId: attachment.id });
    projectFile.set("category", category);      // white_model/soft_decor/rendering/post_process
    projectFile.set("stage", this.mapCategoryToStage(category));
    projectFile.set("uploadedBy", { __type: "Pointer", className: "Profile", objectId: currentUser.id });
    projectFile.set("data", {
      reviewStatus: "pending",
      uploadTime: new Date(),
      fileSize: novaFile.size,
      fileType: novaFile.type
    });
    await projectFile.save();

    return { attachment, projectFile };
  }

  // 映射category到stage
  private mapCategoryToStage(category: string): string {
    const mapping = {
      'white_model': 'modeling',
      'soft_decor': 'softDecor',
      'rendering': 'rendering',
      'post_process': 'postProcess'
    };
    return mapping[category] || 'modeling';
  }
}

2.2 基于ProjectTeam的团队协作管理

2.2.1 团队分配策略服务

class DeliveryTeamManagementService {
  // 智能分配团队成员到产品阶段
  async assignTeamMembers(
    projectId: string,
    productId: string,
    stage: string
  ): Promise<TeamAssignmentResult> {

    // 1. 获取项目团队成员
    const teamQuery = new Parse.Query("ProjectTeam");
    teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
    teamQuery.include("profile");
    const teamMembers = await teamQuery.find();

    // 2. 根据阶段和专业技能匹配
    const stageRequirements = this.getStageRequirements(stage);
    const suitableMembers = teamMembers.filter(member => {
      const specialties = member.get("data")?.specialties || [];
      const role = member.get("role");

      return this.isMemberSuitableForStage(role, specialties, stage);
    });

    // 3. 考虑当前工作量
    const availableMembers = await this.filterByWorkload(suitableMembers);

    // 4. 分配主负责人和协作者
    const assignment = this.createTeamAssignment(availableMembers, stageRequirements);

    // 5. 更新Product的负责人
    await this.updateProductAssignee(productId, assignment.primaryAssignee);

    return assignment;
  }

  // 阶段需求定义
  private getStageRequirements(stage: string): StageRequirements {
    const requirements = {
      modeling: {
        primaryRole: 'designer',
        requiredSkills: ['3d_modeling', 'autocad', 'sketchup'],
        minHours: 8,
        maxHours: 40,
        complexity: 'medium'
      },
      softDecor: {
        primaryRole: 'designer',
        requiredSkills: ['interior_design', 'material_selection', 'color_theory'],
        minHours: 6,
        maxHours: 32,
        complexity: 'high'
      },
      rendering: {
        primaryRole: 'renderer',
        requiredSkills: ['3ds_max', 'vray', 'corona', 'lumion', 'photoshop'],
        minHours: 4,
        maxHours: 24,
        complexity: 'high'
      },
      postProcess: {
        primaryRole: 'designer',
        requiredSkills: ['photoshop', 'lightroom', 'color_correction'],
        minHours: 3,
        maxHours: 16,
        complexity: 'low'
      }
    };

    return requirements[stage] || requirements.modeling;
  }

  // 检查成员是否适合阶段
  private isMemberSuitableForStage(
    role: string,
    specialties: string[],
    stage: string
  ): boolean {
    const requirements = this.getStageRequirements(stage);

    // 角色匹配
    if (role !== requirements.primaryRole && role !== 'team_leader') {
      return false;
    }

    // 技能匹配
    const hasRequiredSkills = requirements.requiredSkills.some(skill =>
      specialties.includes(skill)
    );

    return hasRequiredSkills;
  }
}

2.2.2 增强的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;
    category: 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")),
            category: file.get("category"),
            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("category", "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[],
    category: 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("category", category);
          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 交付执行主界面布局

3.1.1 主界面结构

<!-- 交付执行主界面 -->
<div class="delivery-execution-container">
  <!-- 顶部导航栏 -->
  <div class="delivery-header">
    <div class="project-info">
      <h2>{{ project.title }}</h2>
      <div class="project-meta">
        <span class="customer">{{ project.customer.name }}</span>
        <span class="deadline">截止:{{ formatDate(project.deadline) }}</span>
      </div>
    </div>

    <div class="header-actions">
      <button class="btn btn-primary" @click="showTeamManagement = true">
        <i class="fas fa-users"></i>
        团队管理
      </button>
      <button class="btn btn-secondary" @click="exportDeliveryReport">
        <i class="fas fa-download"></i>
        导出报告
      </button>
    </div>
  </div>

  <!-- 四阶段导航 -->
  <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,
             completed: stage.status === 'completed',
             current: stage.status === 'in_progress'
           }"
           @click="switchStage(stage.id)">

        <!-- 阶段图标和进度 -->
        <div class="stage-icon">
          <i :class="getStageIcon(stage.id)"></i>
          <div class="stage-progress-ring" :style="getProgressStyle(stage.progress)"></div>
        </div>

        <!-- 阶段信息 -->
        <div class="stage-info">
          <h4>{{ getStageDisplayName(stage.id) }}</h4>
          <div class="progress-info">
            <div class="progress-bar">
              <div class="progress-fill" :style="{ width: stage.progress + '%' }"></div>
            </div>
            <span class="progress-text">{{ stage.progress }}%</span>
          </div>
          <div class="stage-meta">
            <span class="product-count">{{ stage.productCount }}个产品</span>
            <span class="time-remaining">{{ stage.remainingDays }}天</span>
          </div>
        </div>

        <!-- 团队成员状态 -->
        <div class="team-status">
          <div class="team-avatars">
            <img v-for="member in stage.teamMembers"
                 :key="member.id"
                 :src="member.avatar"
                 :title="member.name"
                 class="team-avatar" />
          </div>
          <span class="team-status-text" :class="stage.teamStatus">
            {{ getTeamStatusText(stage.teamStatus) }}
          </span>
        </div>
      </div>
    </div>
  </div>

  <!-- 批量操作工具栏 -->
  <div class="batch-operations-toolbar">
    <div class="selection-controls">
      <label class="checkbox-wrapper">
        <input type="checkbox"
               v-model="selectAllProducts"
               @change="toggleSelectAll">
        <span class="checkmark"></span>
        <span>全选</span>
      </label>

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

    <div class="batch-actions" v-if="selectedProducts.length > 0">
      <!-- 文件上传 -->
      <div class="upload-group">
        <button class="btn btn-primary"
                @click="showBatchFileUpload = true">
          <i class="fas fa-upload"></i>
          批量上传文件
        </button>

        <div class="upload-categories">
          <button v-for="category in fileCategories"
                  :key="category.id"
                  class="btn-category"
                  :class="category.id"
                  @click="batchUploadCategory = category.id"
                  :title="category.description">
            <i :class="category.icon"></i>
            {{ category.name }}
          </button>
        </div>
      </div>

      <!-- 团队分配 -->
      <button class="btn btn-secondary"
              @click="showTeamAssignment = true">
        <i class="fas fa-user-plus"></i>
        分配团队成员
      </button>

      <!-- 状态更新 -->
      <button class="btn btn-info"
              @click="showBatchStatusUpdate = true">
        <i class="fas fa-edit"></i>
        批量更新状态
      </button>

      <!-- 质量检查 -->
      <button class="btn btn-warning"
              @click="startBatchQualityCheck">
        <i class="fas fa-check-circle"></i>
        批量质检
      </button>
    </div>
  </div>

  <!-- 产品管理区域 -->
  <div class="products-management-section">
    <div class="section-header">
      <h3>{{ getStageDisplayName(activeStage) }} - 产品管理</h3>

      <!-- 视图切换 -->
      <div class="view-controls">
        <div class="view-toggle">
          <button v-for="view in viewOptions"
                  :key="view.id"
                  class="view-btn"
                  :class="{ active: currentView === view.id }"
                  @click="currentView = view.id"
                  :title="view.description">
            <i :class="view.icon"></i>
            {{ view.name }}
          </button>
        </div>

        <!-- 筛选和排序 -->
        <div class="filter-controls">
          <select v-model="filterByStatus" class="filter-select">
            <option value="all">全部状态</option>
            <option value="not_started">未开始</option>
            <option value="in_progress">进行中</option>
            <option value="awaiting_review">待审核</option>
            <option value="completed">已完成</option>
          </select>

          <select v-model="sortBy" class="sort-select">
            <option value="priority">优先级</option>
            <option value="deadline">截止时间</option>
            <option value="progress">进度</option>
            <option value="assignee">负责人</option>
          </select>
        </div>
      </div>
    </div>

    <!-- 产品网格视图 -->
    <div v-if="currentView === 'grid'" class="products-grid">
      <div v-for="product in filteredProducts"
           :key="product.productId"
           class="product-card"
           :class="{
             selected: selectedProducts.includes(product.productId),
             expanded: product.isExpanded,
             'status-' + product.status
           }"
           @click="toggleProductSelection(product.productId, $event)">

        <!-- 产品头部信息 -->
        <div class="product-header">
          <div class="product-basic-info">
            <div class="product-icon">
              <i :class="getProductTypeIcon(product.productType)"></i>
            </div>

            <div class="product-details">
              <h4 class="product-name">{{ product.productName }}</h4>
              <span class="product-type">{{ getProductTypeName(product.productType) }}</span>
              <div class="space-info">
                <span>{{ product.space?.area }}m²</span>
                <span v-if="product.space?.priority === 'high'" class="priority-badge high">
                  高优先级
                </span>
              </div>
            </div>
          </div>

          <!-- 产品状态和操作 -->
          <div class="product-status-section">
            <div class="status-badge" :class="product.status">
              {{ getStatusText(product.status) }}
            </div>

            <div class="product-actions">
              <button class="action-btn"
                      @click.stop="toggleProductExpansion(product.productId)"
                      :title="product.isExpanded ? '收起' : '展开'">
                <i :class="product.isExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
              </button>

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

        <!-- 进度和团队信息 -->
        <div class="product-progress-section">
          <div class="progress-overview">
            <div class="progress-bar">
              <div class="progress-fill"
                   :style="{ width: product.stageProgress[activeStage] + '%' }"
                   :class="getProgressClass(product.stageProgress[activeStage])">
              </div>
            </div>
            <span class="progress-text">{{ product.stageProgress[activeStage] }}%</span>
          </div>

          <div class="team-info">
            <div class="assignee-info">
              <img :src="product.assignee?.avatar"
                   class="assignee-avatar"
                   :title="product.assignee?.name" />
              <span>{{ product.assignee?.name }}</span>
            </div>

            <div class="time-info">
              <span v-if="product.estimatedHours" class="time-estimated">
                预计 {{ product.estimatedHours }}h
              </span>
              <span v-if="product.actualHours" class="time-actual">
                实际 {{ product.actualHours }}h
              </span>
            </div>
          </div>
        </div>

        <!-- 文件管理预览 -->
        <div class="files-preview">
          <div class="file-categories-preview">
            <div v-for="category in fileCategories"
                 :key="category.id"
                 class="category-preview"
                 :class="category.id">
              <i :class="category.icon"></i>
              <span class="file-count">{{ getFileCount(product, category.id) }}</span>
            </div>
          </div>

          <div class="recent-files">
            <div v-for="file in getRecentFiles(product)"
                 :key="file.id"
                 class="recent-file"
                 :title="file.name">
              <i :class="getFileIcon(file.name)"></i>
            </div>
          </div>
        </div>

        <!-- 展开的详细内容 -->
        <div v-if="product.isExpanded" class="product-expanded-content">
          <!-- 文件管理区域 -->
          <div class="files-management">
            <div class="files-header">
              <h5>交付文件管理</h5>
              <div class="file-upload-btn">
                <input type="file"
                       :id="'file-upload-' + product.productId"
                       multiple
                       @change="handleFileUpload($event, product.productId)"
                       style="display: none;">
                <button class="btn btn-sm btn-primary"
                        @click="$event.stopPropagation(); triggerFileUpload(product.productId)">
                  <i class="fas fa-plus"></i>
                  上传文件
                </button>
              </div>
            </div>

            <!-- 分类文件列表 -->
            <div class="files-by-category">
              <div v-for="category in fileCategories"
                   :key="category.id"
                   class="category-section">
                <div class="category-header">
                  <h6>
                    <i :class="category.icon"></i>
                    {{ category.name }}
                    <span class="count">({{ getFileCount(product, category.id) }})</span>
                  </h6>
                  <button class="btn-xs"
                          @click="expandCategory(product.productId, category.id)"
                          v-if="getFileCount(product, category.id) > 0">
                    <i :class="expandedCategories[product.productId + '-' + category.id] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
                  </button>
                </div>

                <div v-if="expandedCategories[product.productId + '-' + category.id] || getFileCount(product, category.id) <= 3"
                     class="files-list">
                  <div v-for="file in getFilesByCategory(product, category.id)"
                       :key="file.id"
                       class="file-item"
                       :class="{
                         'status-approved': file.reviewStatus === 'approved',
                         'status-pending': file.reviewStatus === 'pending',
                         'status-rejected': file.reviewStatus === 'rejected'
                       }">
                    <div class="file-preview">
                      <img v-if="isImageFile(file.name)"
                           :src="file.url"
                           :alt="file.name"
                           @error="handleImageError" />
                      <i v-else :class="getFileIcon(file.name)"></i>
                    </div>

                    <div class="file-info">
                      <span class="file-name" :title="file.name">{{ file.name }}</span>
                      <span class="file-meta">
                        {{ formatFileSize(file.size) }} • {{ formatDate(file.uploadTime) }}
                      </span>
                    </div>

                    <div class="file-actions">
                      <button class="action-btn"
                              @click="previewFile(file)"
                              title="预览">
                        <i class="fas fa-eye"></i>
                      </button>
                      <button class="action-btn"
                              @click="downloadFile(file)"
                              title="下载">
                        <i class="fas fa-download"></i>
                      </button>

                      <!-- 审核操作 -->
                      <div v-if="canReviewFiles" class="review-actions">
                        <button class="action-btn approve"
                                @click="approveFile(file)"
                                title="审核通过"
                                :disabled="file.reviewStatus === 'approved'">
                          <i class="fas fa-check"></i>
                        </button>
                        <button class="action-btn reject"
                                @click="rejectFile(file)"
                                title="驳回"
                                :disabled="file.reviewStatus === 'rejected'">
                          <i class="fas fa-times"></i>
                        </button>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- 备注和日志 -->
          <div class="notes-and-logs">
            <div class="notes-section">
              <h6>阶段备注</h6>
              <textarea class="notes-textarea"
                        v-model="product.stageNotes[activeStage]"
                        @blur="updateStageNotes(product.productId)"
                        placeholder="添加当前阶段的备注信息..."
                        rows="3"></textarea>

              <div class="notes-meta">
                <span class="last-updated">
                  最后更新: {{ formatDateTime(product.lastUpdated) }}
                </span>
                <span class="updated-by">{{ product.lastUpdatedBy }}</span>
              </div>
            </div>

            <div class="activity-logs">
              <h6>活动日志</h6>
              <div class="log-list">
                <div v-for="log in getActivityLogs(product.productId)"
                     :key="log.id"
                     class="log-item">
                  <div class="log-time">{{ formatTime(log.timestamp) }}</div>
                  <div class="log-content">{{ log.message }}</div>
                  <div class="log-user">{{ log.user }}</div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 产品列表视图(表格) -->
    <div v-if="currentView === 'list'" class="products-table-container">
      <table class="products-table">
        <thead>
          <tr>
            <th class="checkbox-col">
              <input type="checkbox" v-model="selectAllProducts" @change="toggleSelectAll">
            </th>
            <th>产品名称</th>
            <th>空间类型</th>
            <th>负责人</th>
            <th>进度</th>
            <th>文件数量</th>
            <th>状态</th>
            <th>预计时间</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="product in filteredProducts"
              :key="product.productId"
              class="product-row"
              :class="{ 'status-' + product.status, selected: selectedProducts.includes(product.productId) }">
            <td class="checkbox-col">
              <input type="checkbox" :value="product.productId" v-model="selectedProducts">
            </td>
            <td>
              <div class="product-cell">
                <i :class="getProductTypeIcon(product.productType)"></i>
                <span>{{ product.productName }}</span>
              </div>
            </td>
            <td>{{ getProductTypeName(product.productType) }}</td>
            <td>
              <div class="assignee-cell">
                <img :src="product.assignee?.avatar" class="avatar-sm">
                <span>{{ product.assignee?.name }}</span>
              </div>
            </td>
            <td>
              <div class="progress-cell">
                <div class="progress-bar-sm">
                  <div class="progress-fill" :style="{ width: product.stageProgress[activeStage] + '%' }"></div>
                </div>
                <span>{{ product.stageProgress[activeStage] }}%</span>
              </div>
            </td>
            <td>{{ getTotalFileCount(product) }}</td>
            <td>
              <span class="status-badge" :class="product.status">
                {{ getStatusText(product.status) }}
              </span>
            </td>
            <td>{{ product.estimatedHours }}h</td>
            <td>
              <div class="action-buttons">
                <button class="action-btn" @click="viewProductDetails(product)" title="查看详情">
                  <i class="fas fa-eye"></i>
                </button>
                <button class="action-btn" @click="editProduct(product)" title="编辑">
                  <i class="fas fa-edit"></i>
                </button>
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

3.2 核心组件设计

3.2.1 文件上传组件

// 智能文件上传组件
@Component({
  selector: 'app-delivery-file-upload',
  standalone: true,
  imports: [CommonModule, FormsModule, IonIcon]
})
export class DeliveryFileUploadComponent {
  @Input() projectId: string = '';
  @Input() productId: string = '';
  @Input() category: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' = 'white_model';
  @Output() fileUploaded = new EventEmitter<UploadResult>();

  uploading: boolean = false;
  uploadProgress: number = 0;
  dragOver: boolean = false;

  constructor(private fileService: DeliveryFileService) {}

  async onFileSelect(event: Event): Promise<void> {
    const files = (event.target as HTMLInputElement).files;
    if (!files?.length) return;

    await this.uploadFiles(Array.from(files));
  }

  async uploadFiles(files: File[]): Promise<void> {
    this.uploading = true;
    this.uploadProgress = 0;

    try {
      for (const file of files) {
        const result = await this.fileService.uploadDeliveryFile(
          this.projectId,
          this.productId,
          this.category,
          file
        );

        this.fileUploaded.emit({
          file: result.projectFile,
          attachment: result.attachment,
          category: this.category
        });
      }
    } catch (error) {
      console.error('File upload failed:', error);
      // 显示错误提示
    } finally {
      this.uploading = false;
      this.uploadProgress = 0;
    }
  }

  getFileIcon(fileName: string): string {
    const ext = fileName.split('.').pop()?.toLowerCase();
    const iconMap = {
      'jpg': 'fas fa-image',
      'png': 'fas fa-image',
      'gif': 'fas fa-image',
      'pdf': 'fas fa-file-pdf',
      'dwg': 'fas fa-file-cad',
      'dxf': 'fas fa-file-cad',
      'skp': 'fas fa-file-cad',
      'max': 'fas fa-file-cad'
    };
    return iconMap[ext] || 'fas fa-file';
  }
}

3.2.2 团队分配组件

// 团队成员分配组件
@Component({
  selector: 'app-team-assignment',
  standalone: true,
  imports: [CommonModule, FormsModule, IonIcon]
})
export class TeamAssignmentComponent {
  @Input() projectId: string = '';
  @Input() stage: string = '';
  @Input() selectedProducts: string[] = [];

  availableTeamMembers: TeamMember[] = [];
  assignmentSuggestions: AssignmentSuggestion[] = [];

  constructor(private teamService: DeliveryTeamManagementService) {}

  async loadTeamMembers(): Promise<void> {
    const teamQuery = new Parse.Query("ProjectTeam");
    teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: this.projectId });
    teamQuery.include("profile");
    const members = await teamQuery.find();

    this.availableTeamMembers = members.map(member => ({
      id: member.id,
      name: member.get("profile").get("name"),
      avatar: member.get("profile").get("data")?.avatar,
      role: member.get("role"),
      specialties: member.get("data")?.specialties || [],
      currentWorkload: member.get("workload") || 0,
      assignedProducts: member.get("data")?.assignedProducts || []
    }));
  }

  async generateAssignmentSuggestions(): Promise<void> {
    const requirements = this.getStageRequirements(this.stage);

    this.assignmentSuggestions = this.selectedProducts.map(productId => {
      const suitableMembers = this.availableTeamMembers.filter(member =>
        this.isMemberSuitable(member, requirements)
      );

      return {
        productId,
        productName: this.getProductName(productId),
        primaryAssignee: this.selectBestMember(suitableMembers),
        collaborators: this.selectCollaborators(suitableMembers, 2),
        reasoning: this.generateAssignmentReasoning(suitableMembers[0], requirements)
      };
    });
  }

  async applyAssignments(): Promise<void> {
    for (const suggestion of this.assignmentSuggestions) {
      await this.teamService.assignTeamMembers(
        this.projectId,
        suggestion.productId,
        this.stage,
        suggestion.primaryAssignee,
        suggestion.collaborators
      );
    }
  }
}

3.2.3 进度跟踪组件

// 进度跟踪组件
@Component({
  selector: 'app-progress-tracker',
  standalone: true,
  imports: [CommonModule, FormsModule]
})
export class ProgressTrackerComponent {
  @Input() productId: string = '';
  @Input() stage: string = '';

  progressData: ProgressData = {
    current: 0,
    target: 100,
    milestones: [],
    activities: []
  };

  constructor(private progressService: ProductProgressService) {}

  async loadProgressData(): Promise<void> {
    this.progressData = await this.progressService.getProductProgress(
      this.productId,
      this.stage
    );
  }

  updateProgress(newProgress: number): void {
    this.progressService.updateProductProgress(
      this.productId,
      this.stage,
      { progress: newProgress }
    );
  }

  addMilestone(milestone: Milestone): void {
    this.progressData.milestones.push(milestone);
    this.progressService.addMilestone(this.productId, this.stage, milestone);
  }
}

3.3 交互流程设计

3.3.1 文件上传交互

  1. 拖拽上传:支持拖拽文件到指定区域
  2. 批量选择:支持多文件同时上传
  3. 进度显示:实时显示上传进度和状态
  4. 自动分类:根据文件类型自动选择category
  5. 重试机制:上传失败时支持重试

3.3.2 团队协作交互

  1. 智能推荐:根据阶段需求自动推荐合适的团队成员
  2. 工作量平衡:考虑当前工作量,避免过度分配
  3. 技能匹配:基于专业技能进行精准匹配
  4. 协作支持:支持主负责人+协作者模式

3.3.3 审核流程交互

  1. 预览功能:支持文件预览和快速查看
  2. 审核操作:通过/驳回/重新审核
  3. 批注功能:支持在文件上添加批注
  4. 版本管理:支持文件版本比较和回滚

3.4 数据结构定义

3.4.1 文件分类结构

interface category {
  id: 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
  name: string;
  icon: string;
  description: string;
  allowedTypes: string[];
  maxSize: number; // MB
}

export const FILE_CATEGORIES: category[] = [
  {
    id: 'white_model',
    name: '白模建模',
    icon: 'fas fa-cube',
    description: '空间结构建模、基础框架',
    allowedTypes: ['.skp', '.max', '.dwg', '.dxf'],
    maxSize: 50
  },
  {
    id: 'soft_decor',
    name: '软装设计',
    icon: 'fas fa-couch',
    description: '家具配置、材质选择、色彩搭配',
    allowedTypes: ['.jpg', '.png', '.pdf', '.psd'],
    maxSize: 20
  },
  {
    id: 'rendering',
    name: '渲染输出',
    icon: 'fas fa-image',
    description: '高清效果图、全景图、细节特写',
    allowedTypes: ['.jpg', '.png', '.tiff', '.hdr'],
    maxSize: 30
  },
  {
    id: 'post_process',
    name: '后期处理',
    icon: 'fas fa-magic',
    description: '色彩调整、效果优化、最终成品',
    allowedTypes: ['.jpg', '.png', '.psd', '.tiff'],
    maxSize: 25
  }
];

3.4.2 团队角色定义

interface TeamRole {
  id: string;
  name: string;
  description: string;
  requiredSkills: string[];
  icon: string;
  color: string;
}

export const TEAM_ROLES: TeamRole[] = [
  {
    id: 'designer',
    name: '设计师',
    description: '负责建模、软装、后期设计工作',
    requiredSkills: ['autocad', 'sketchup', '3ds_max', 'photoshop'],
    icon: 'fas fa-pencil-ruler',
    color: '#007bff'
  },
  {
    id: 'renderer',
    name: '渲染师',
    description: '负责效果图渲染和输出',
    requiredSkills: ['3ds_max', 'vray', 'corona', 'lumion'],
    icon: 'fas fa-palette',
    color: '#28a745'
  },
  {
    id: 'team_leader',
    name: '组长',
    description: '负责团队协调和质量把控',
    requiredSkills: ['project_management', 'quality_control'],
    icon: 'fas fa-users-cog',
    color: '#ffc107'
  },
  {
    id: 'technical',
    name: '技术',
    description: '负责技术验收和质量标准',
    requiredSkills: ['technical_review', 'standards'],
    icon: 'fas fa-tools',
    color: '#6c757d'
  }
];

4. 技术实现要点

4.1 性能优化策略

  • 虚拟滚动:处理大量产品时的性能优化
  • 懒加载:文件和详情按需加载
  • 缓存策略:产品状态和文件信息缓存
  • CDN加速:文件预览和下载使用CDN

4.2 用户体验优化

  • 响应式设计:适配不同屏幕尺寸
  • 离线支持:基本的离线操作和数据同步
  • 快捷键支持:提高操作效率
  • 实时通知:进度更新和状态变更通知

4.3 数据一致性保障

  • 事务处理:确保批量操作的原子性
  • 乐观锁:防止并发修改冲突
  • 版本控制:文件版本管理和回滚
  • 数据验证:严格的前后端数据验证

4.4 安全性考虑

  • 权限控制:基于角色的访问控制
  • 文件安全:文件上传安全检查和病毒扫描
  • 数据加密:敏感数据传输和存储加密
  • 审计日志:操作日志记录和追踪

文档版本: v4.0 (完整交付执行设计) 最后更新: 2025-10-21 维护者: YSS Development Team

       :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 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>

<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>

<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>


### 3.2 批量操作界面

html

<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.category">
        <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 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>

```

4. 技术实现要点

4.1 性能优化

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

4.2 用户体验优化

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

4.3 数据一致性

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

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