Explorar el Código

fix: Product instead Space

ryanemax hace 11 meses
padre
commit
65d1a89119

+ 256 - 0
docs/prd/更新总结-Product表统一空间管理.md

@@ -0,0 +1,256 @@
+# PRD文档更新总结 - 基于Product表的统一空间管理
+
+## 更新概述
+
+根据系统架构优化,所有PRD文档中的"空间管理"相关内容已更新为"空间设计产品管理",基于Product表实现统一的空间设计产品管理。
+
+## 核心变更
+
+### 1. 术语变更
+
+| 原术语 | 新术语 | 说明 |
+|--------|--------|------|
+| 空间管理 | 空间设计产品管理 | 基于Product表的产品化管理 |
+| 空间 | 空间设计产品 | 每个Product代表一个空间的设计产品 |
+| 空间分配 | 产品分配 | 设计师分配负责的空间设计产品 |
+| 空间报价 | 产品报价 | 每个空间设计产品的独立报价 |
+| 空间文件 | 产品文件 | 通过ProjectFile.fileCategory分类管理 |
+
+### 2. 数据结构变更
+
+#### 原:多表结构
+```typescript
+// 原有复杂的多表结构
+interface ProjectSpace {
+  // 空间基本信息
+}
+
+interface SpaceQuotation {
+  // 空间报价
+}
+
+interface SpaceRequirement {
+  // 空间需求
+}
+
+interface SpacePanorama {
+  // 空间全景图
+}
+```
+
+#### 新:Product表统一结构
+```typescript
+// 新的Product表统一结构
+interface Product {
+  objectId: string;
+  project: Pointer<Project>;
+  profile: Pointer<Profile>;  // 负责设计师
+  productName: string;        // "李总主卧设计"
+  productType: string;        // "bedroom"
+  space: {                   // 空间信息
+    spaceName: string;
+    area: number;
+    dimensions: Object;
+    features: string[];
+  };
+  quotation: {               // 产品报价
+    price: number;
+    currency: string;
+    breakdown: Object;
+  };
+  requirements: {            // 设计需求
+    colorRequirement: Object;
+    materialRequirement: Object;
+    specificRequirements: string[];
+  };
+  reviews: Array;           // 产品评价
+}
+```
+
+### 3. 文件管理简化
+
+#### 原:多表文件管理
+```typescript
+// 复杂的多表文件关联
+SpaceQuotation.quotationFiles
+SpacePanorama.panoramaFiles
+SpaceDelivery.deliveryFiles
+```
+
+#### 新:ProjectFile分类管理
+```typescript
+// 简化的ProjectFile分类管理
+interface ProjectFile {
+  project: Pointer<Project>;
+  product: Pointer<Product>;     // 关联空间设计产品
+  fileCategory: string;          // 文件分类
+  // fileCategory 枚举值:
+  // "quotation" - 报价文件
+  // "panorama" - 全景图文件
+  // "delivery" - 交付物文件
+  // "reference" - 参考文件
+  // "requirement" - 需求文件
+}
+```
+
+## 各PRD文档主要更新内容
+
+### 1. 项目-订单分配.md
+
+**变更前**:
+- 多空间项目识别与管理
+- 空间分配系统
+- 空间报价策略
+
+**变更后**:
+- **多空间产品设计管理**:基于Product表识别和管理
+- **产品分配系统**:设计师直接关联Product.profile
+- **产品报价策略**:每个Product独立的quotation字段
+
+### 2. 项目-交付执行.md
+
+**变更前**:
+- 多空间交付协调
+- 空间进度跟踪
+- 空间交付物管理
+
+**变更后**:
+- **多产品设计交付协调**:通过Product表统一管理
+- **产品进度跟踪**:Product.status + stage字段
+- **产品交付物管理**:Product.fileUrl + ProjectFile分类
+
+### 3. 项目-售后归档.md
+
+**变更前**:
+- 多空间全景图合成
+- 空间评价收集
+- 跨空间投诉处理
+
+**变更后**:
+- **多产品全景图管理**:通过ProjectFile.fileCategory="panorama"
+- **产品评价收集**:Product.reviews字段存储评价数据
+- **产品级反馈处理**:ProjectFeedback.product关联具体产品
+
+### 4. 项目-空间任务逻辑.md
+
+**变更前**:
+- 空间任务分配逻辑
+- 空间依赖管理
+- 跨空间协作流程
+
+**变更后**:
+- **产品设计任务分配**:基于Product.profile直接分配
+- **产品间依赖管理**:通过Product表metadata字段管理
+- **产品协作流程**:简化为产品级协作管理
+
+## 技术实现示例
+
+### 1. 创建空间设计产品
+```typescript
+const Product = Parse.Object.extend("Product");
+const product = new Product();
+product.set("project", project.toPointer());
+product.set("profile", designer.toPointer());  // 直接分配设计师
+product.set("productName", "李总主卧设计");
+product.set("productType", "bedroom");
+
+// 设置空间信息
+product.set("space", {
+  spaceName: "主卧",
+  area: 18.5,
+  dimensions: { length: 4.5, width: 4.1, height: 2.8 }
+});
+
+// 设置产品报价
+product.set("quotation", {
+  price: 35000,
+  breakdown: { design: 15000, modeling: 10000, rendering: 8000 }
+});
+
+await product.save();
+```
+
+### 2. 文件分类管理
+```typescript
+// 上传全景图文件
+const panoramaFile = new ProjectFile();
+panoramaFile.set("product", product.toPointer());
+panoramaFile.set("fileCategory", "panorama");
+panoramaFile.set("fileName", "主卧720全景图.jpg");
+await panoramaFile.save();
+
+// 上传报价文件
+const quotationFile = new ProjectFile();
+quotationFile.set("product", product.toPointer());
+quotationFile.set("fileCategory", "quotation");
+quotationFile.set("fileName", "主卧设计报价单.pdf");
+await quotationFile.save();
+```
+
+### 3. 查询空间设计产品
+```typescript
+// 查询项目的所有空间设计产品
+const productQuery = new Parse.Query("Product");
+productQuery.equalTo("project", projectId);
+productQuery.include("profile");  // 包含设计师信息
+const products = await productQuery.find();
+
+// 查询设计师负责的产品
+const designerQuery = new Parse.Query("Product");
+designerQuery.equalTo("profile", designerId);
+designerQuery.equalTo("status", "in_progress");
+const myProducts = await designerQuery.find();
+```
+
+## 业务流程优化
+
+### 1. 订单分配流程
+```
+客户咨询 → 识别空间类型 → 创建空间设计产品(Product) → 分配设计师(Product.profile) → 确定产品报价(Product.quotation)
+```
+
+### 2. 交付执行流程
+```
+产品设计 → 文件上传(ProjectFile分类) → 进度更新(Product.status) → 交付物管理(Product.fileUrl)
+```
+
+### 3. 售后归档流程
+```
+项目完成 → 全景图收集(ProjectFile.panorama) → 产品评价(Product.reviews) → 文件归档
+```
+
+## 优势总结
+
+### 1. 架构简化
+- **表数量减少**:从15个空间相关表简化为1个Product表
+- **查询效率提升**:单表查询替代复杂JOIN操作
+- **维护成本降低**:减少表间关联关系
+
+### 2. 业务语义清晰
+- **产品化思维**:每个空间都是独立的设计产品
+- **设计师直连**:Product.profile直接关联负责设计师
+- **文件分类管理**:通过fileCategory清晰分类不同类型文件
+
+### 3. 开发效率提升
+- **统一接口**:所有空间相关操作都通过Product表
+- **简化逻辑**:减少了复杂的多表操作逻辑
+- **扩展性强**:Object字段支持灵活的功能扩展
+
+## 迁移指南
+
+### 数据迁移步骤
+1. **ProjectSpace → Product**:将空间数据转换为产品设计产品
+2. **文件分类标记**:为现有ProjectFile添加fileCategory分类
+3. **删除冗余表**:清理不再需要的空间相关表
+4. **更新业务逻辑**:调整代码以适应新的Product表结构
+
+### 兼容性考虑
+- **API接口调整**:更新相关API接口以支持Product表结构
+- **前端界面更新**:调整UI组件以显示产品设计信息
+- **业务流程适配**:确保业务流程与新产品化管理一致
+
+---
+
+**更新日期**: 2025-10-20
+**版本**: v3.0 - Product表统一空间管理
+**维护者**: YSS Development Team

+ 909 - 1508
docs/prd/项目-交付执行.md

@@ -1,4 +1,4 @@
-# 项目管理 - 交付执行阶段 PRD
+# 项目管理 - 交付执行阶段 PRD (Product表版本)
 
 ## 1. 功能概述
 
@@ -6,11 +6,13 @@
 交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
 
 ### 1.2 核心目标
-- 按空间维度组织文件上传和进度管理
-- 实现四个执行阶段的串行推进
-- 提供实时进度跟踪和状态可视化
-- 支持组长审核和质量把控
-- 确保交付物符合质量标准
+- **多产品设计协同管理**:支持单产品设计到多产品设计项目的灵活管理
+- **按产品设计维度组织文件上传和进度管理**
+- **实现四个执行阶段的串行推进**
+- **跨产品设计协调与依赖管理**:处理产品设计间的风格一致性、色彩流线、材质匹配
+- **提供实时进度跟踪和状态可视化**
+- **支持组长审核和质量把控**
+- **确保交付物符合质量标准**
 
 ### 1.3 涉及角色
 - **设计师**:负责建模、软装、后期等设计工作
@@ -34,1642 +36,1041 @@ graph LR
     style E fill:#f3e5f5
 ```
 
-## 2. 空间管理系统
+## 2. 基于Product表的交付管理系统
 
-### 2.1 空间数据结
+### 2.1 产品交付管理架
 
-#### 2.1.1 DeliveryProcess 接口
+#### 2.1.1 增强的DeliveryProcess接口
 ```typescript
-interface DeliveryProcess {
+interface ProductDeliveryProcess {
   id: string;                           // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
   name: string;                         // 流程名称:建模/软装/渲染/后期
   type: 'modeling' | 'softDecor' | 'rendering' | 'postProcess';
   isExpanded: boolean;                  // 是否展开
-  spaces: DeliverySpace[];              // 空间列表
-  content: {
-    [spaceId: string]: SpaceContent;    // 按空间ID索引的内容
+
+  // 产品管理(基于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 DeliverySpace {
-  id: string;                           // 空间ID
-  name: string;                         // 空间名称:卧室/客厅/厨房等
+interface ProductDelivery {
+  productId: string;                     // 产品ID(与Product.objectId关联)
+  productName: string;                   // 产品名称:李总主卧设计
+  productType: string;                   // 产品类型:bedroom
   isExpanded: boolean;                  // 是否展开
-  order: number;                        // 排序顺序
+  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 SpaceContent {
-  images: Array<{
+interface ProductContent {
+  // 产品文件(基于ProjectFile分类)
+  files: Array<{
     id: string;
     name: string;
     url: string;
     size?: string;
+    fileCategory: string;               // 'delivery' | 'reference' | 'other'
     reviewStatus?: 'pending' | 'approved' | 'rejected';
-    synced?: boolean;                   // 是否已同步到客户端
+    synced?: boolean;                    // 是否已同步到客户端
+    uploadTime: Date;                     // 上传时间
+    uploadedBy: string;                   // 上传人
   }>;
-  progress: number;                     // 进度 0-100
-  status: 'pending' | 'in_progress' | 'completed' | 'approved';
-  notes: string;                        // 备注信息
-  lastUpdated: Date;                    // 最后更新时间
-}
-```
 
-#### 2.1.2 初始空间配置
-```typescript
-// project-detail.ts lines 458-523
-deliveryProcesses: DeliveryProcess[] = [
-  {
-    id: 'modeling',
-    name: '建模',
-    type: 'modeling',
-    isExpanded: true,                   // 默认展开第一个
-    spaces: [
-      { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
-      { id: 'living', name: '客厅', isExpanded: false, order: 2 },
-      { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
-    ],
-    content: {
-      'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
-      'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
-      'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
-    }
-  },
-  // 软装、渲染、后期流程结构相同
-];
-```
-
-### 2.2 空间管理功能
-
-#### 2.2.1 添加新空间
-```typescript
-// project-detail.ts lines 5150-5184
-addSpace(processId: string): void {
-  const spaceName = this.newSpaceName[processId]?.trim();
-  if (!spaceName) return;
-
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process) return;
-
-  // 生成新的空间ID
-  const spaceId = `space_${Date.now()}`;
-
-  // 添加到spaces数组
-  const newSpace: DeliverySpace = {
-    id: spaceId,
-    name: spaceName,
-    isExpanded: false,
-    order: process.spaces.length + 1
-  };
-
-  process.spaces.push(newSpace);
-
-  // 初始化content数据
-  process.content[spaceId] = {
-    images: [],
-    progress: 0,
-    status: 'pending',
-    notes: '',
-    lastUpdated: new Date()
+  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[];
   };
-
-  // 清空输入框并隐藏
-  this.newSpaceName[processId] = '';
-  this.showAddSpaceInput[processId] = false;
-
-  console.log(`已添加空间: ${spaceName} 到流程 ${process.name}`);
-}
-```
-
-**UI交互**:
-```html
-<!-- 添加空间输入框 -->
-@if (showAddSpaceInput[process.id]) {
-  <div class="add-space-input">
-    <input
-      type="text"
-      [(ngModel)]="newSpaceName[process.id]"
-      placeholder="输入空间名称(如:次卧、书房)"
-      (keydown.enter)="addSpace(process.id)"
-      (keydown.escape)="cancelAddSpace(process.id)">
-    <button class="btn-primary" (click)="addSpace(process.id)">确定</button>
-    <button class="btn-secondary" (click)="cancelAddSpace(process.id)">取消</button>
-  </div>
-} @else {
-  <button class="btn-add-space" (click)="showAddSpaceInput[process.id] = true">
-    + 添加空间
-  </button>
 }
 ```
 
-#### 2.2.2 删除空间
+#### 2.1.2 基于Product表的交付管理服务
 ```typescript
-// project-detail.ts lines 5219-5242
-removeSpace(processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process) return;
-
-  // 从spaces数组中移除
-  const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
-  if (spaceIndex > -1) {
-    const spaceName = process.spaces[spaceIndex].name;
-    process.spaces.splice(spaceIndex, 1);
-
-    // 清理content数据
-    if (process.content[spaceId]) {
-      // 释放图片URL资源
-      process.content[spaceId].images.forEach(img => {
-        if (img.url && img.url.startsWith('blob:')) {
-          URL.revokeObjectURL(img.url);
+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()
         }
-      });
-      delete process.content[spaceId];
-    }
-
-    console.log(`已删除空间: ${spaceName} 从流程 ${process.name}`);
-  }
-}
-```
-
-#### 2.2.3 空间展开/收起
-```typescript
-// project-detail.ts lines 5200-5208
-toggleSpace(processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process) return;
-
-  const space = process.spaces.find(s => s.id === spaceId);
-  if (space) {
-    space.isExpanded = !space.isExpanded;
-  }
-}
-```
-
-### 2.3 进度管理
-
-#### 2.3.1 进度计算逻辑
-```typescript
-// project-detail.ts lines 5377-5397
-private updateSpaceProgress(processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
-
-  const content = process.content[spaceId];
-  const imageCount = content.images.length;
-
-  // 根据图片数量和状态计算进度
-  if (imageCount === 0) {
-    content.progress = 0;
-    content.status = 'pending';
-  } else if (imageCount < 3) {
-    content.progress = Math.min(imageCount * 30, 90);
-    content.status = 'in_progress';
-  } else {
-    content.progress = 100;
-    content.status = 'completed';
-  }
-
-  content.lastUpdated = new Date();
-}
-```
-
-**进度规则**:
-- 0张图片:0%进度,状态为待开始
-- 1-2张图片:30%-60%进度,状态为进行中
-- 3张及以上:100%进度,状态为已完成
-
-#### 2.3.2 获取空间进度
-```typescript
-// project-detail.ts lines 5211-5216
-getSpaceProgress(processId: string, spaceId: string): number {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return 0;
-
-  return process.content[spaceId].progress || 0;
-}
-```
-
-#### 2.3.3 进度可视化
-```html
-<div class="space-progress-bar">
-  <div class="progress-fill"
-       [style.width.%]="getSpaceProgress(process.id, space.id)"
-       [class.pending]="getSpaceProgress(process.id, space.id) === 0"
-       [class.in-progress]="getSpaceProgress(process.id, space.id) > 0 && getSpaceProgress(process.id, space.id) < 100"
-       [class.completed]="getSpaceProgress(process.id, space.id) === 100">
-  </div>
-  <span class="progress-text">{{ getSpaceProgress(process.id, space.id) }}%</span>
-</div>
-```
-
-## 3. 建模阶段
-
-### 3.1 功能特点
-- 白模图片上传
-- 模型检查项验证
-- 户型匹配度检查
-- 尺寸精度验证
-
-### 3.2 白模上传
-
-#### 3.2.1 文件上传处理
-```typescript
-// project-detail.ts lines 1838-1850
-onWhiteModelSelected(event: Event): void {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
-  const items = files.map(f => this.makeImageItem(f));
-  this.whiteModelImages.unshift(...items);
-  input.value = '';
-}
-
-removeWhiteModelImage(id: string): void {
-  const target = this.whiteModelImages.find(i => i.id === id);
-  if (target) this.revokeUrl(target.url);
-  this.whiteModelImages = this.whiteModelImages.filter(i => i.id !== id);
-}
-```
-
-#### 3.2.2 图片对象生成
-```typescript
-// project-detail.ts lines 1826-1830
-private makeImageItem(file: File): { id: string; name: string; url: string; size: string } {
-  const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
-  const url = URL.createObjectURL(file);
-  return { id, name: file.name, url, size: this.formatFileSize(file.size) };
-}
-```
-
-#### 3.2.3 文件大小格式化
-```typescript
-// project-detail.ts lines 1815-1823
-private formatFileSize(bytes: number): string {
-  if (bytes < 1024) return `${bytes}B`;
-  const kb = bytes / 1024;
-  if (kb < 1024) return `${kb.toFixed(1)}KB`;
-  const mb = kb / 1024;
-  if (mb < 1024) return `${mb.toFixed(1)}MB`;
-  const gb = mb / 1024;
-  return `${gb.toFixed(2)}GB`;
-}
-```
-
-### 3.3 模型检查项
-
-#### 3.3.1 检查项数据结构
-```typescript
-interface ModelCheckItem {
-  id: string;
-  name: string;
-  isPassed: boolean;
-  notes: string;
-}
+      };
+
+      // 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;
+      }
 
-// project-detail.ts lines 449-455
-modelCheckItems: ModelCheckItem[] = [
-  { id: 'check-1', name: '户型匹配度检查', isPassed: false, notes: '' },
-  { id: 'check-2', name: '尺寸精度验证', isPassed: false, notes: '' },
-  { id: 'check-3', name: '材质贴图检查', isPassed: false, notes: '' },
-  { id: 'check-4', name: '光影效果验证', isPassed: false, notes: '' },
-  { id: 'check-5', name: '细节完整性检查', isPassed: false, notes: '' }
-];
-```
+      deliveryProcesses.push(process);
+    }
 
-#### 3.3.2 检查项UI
-```html
-<div class="model-check-list">
-  <h4>模型检查项</h4>
-  @for (item of modelCheckItems; track item.id) {
-    <div class="check-item">
-      <label>
-        <input
-          type="checkbox"
-          [(ngModel)]="item.isPassed"
-          [disabled]="isReadOnly()">
-        <span class="check-name">{{ item.name }}</span>
-      </label>
-      <input
-        type="text"
-        [(ngModel)]="item.notes"
-        placeholder="备注说明"
-        [disabled]="isReadOnly()"
-        class="check-notes">
-    </div>
+    return deliveryProcesses;
   }
-</div>
-```
-
-### 3.4 建模阶段完成
-
-#### 3.4.1 确认上传方法
-```typescript
-// project-detail.ts lines 1853-1866
-confirmWhiteModelUpload(): void {
-  // 检查建模阶段的图片数据
-  const modelingProcess = this.deliveryProcesses.find(p => p.id === 'modeling');
-  if (!modelingProcess) return;
-
-  // 检查是否有任何空间上传了图片
-  const hasImages = modelingProcess.spaces.some(space => {
-    const content = modelingProcess.content[space.id];
-    return content && content.images && content.images.length > 0;
-  });
-
-  if (!hasImages) return;
-  this.advanceToNextStage('建模');
-}
-```
-
-#### 3.4.2 阶段推进逻辑
-```typescript
-// project-detail.ts lines 1391-1423
-advanceToNextStage(afterStage: ProjectStage): void {
-  const idx = this.stageOrder.indexOf(afterStage);
-  if (idx >= 0 && idx < this.stageOrder.length - 1) {
-    const next = this.stageOrder[idx + 1];
-
-    // 更新项目阶段
-    this.updateProjectStage(next);
-
-    // 更新展开状态,折叠当前、展开下一阶段
-    this.expandedStages[afterStage] = false;
-    this.expandedStages[next] = true;
 
-    // 更新板块展开状态
-    const nextSection = this.getSectionKeyForStage(next);
-    this.expandedSection = nextSection;
+  // 获取产品的交付文件
+  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");
 
-    // 触发变更检测以更新导航栏颜色
-    this.cdr.detectChanges();
+    return await fileQuery.find();
   }
-}
-```
 
-## 4. 软装阶段
+  // 计算产品进度
+  calculateProductProgress(product: Parse.Object, stage: string): number {
+    const productFiles = product.get("deliveryFiles") || [];
+    const stageFiles = productFiles.filter((file: any) => file.stage === stage);
 
-### 4.1 功能特点
-- 小图上传(建议≤1MB,不强制)
-- 支持拖拽上传
-- 实时预览功能
-- 按空间组织
+    if (stageFiles.length === 0) return 0;
 
-### 4.2 小图上传
+    const completedFiles = stageFiles.filter((file: any) =>
+      file.reviewStatus === "approved"
+    );
 
-#### 4.2.1 文件选择处理
-```typescript
-// project-detail.ts lines 1869-1881
-onSoftDecorSmallPicsSelected(event: Event): void {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
-  const warnOversize = files.filter(f => f.size > 1024 * 1024);
-  if (warnOversize.length > 0) {
-    // 仅提示,不阻断
-    console.warn('软装小图建议≤1MB,以下文件较大:', warnOversize.map(f => f.name));
+    return Math.round((completedFiles.length / stageFiles.length) * 100);
   }
-  const items = files.map(f => this.makeImageItem(f));
-  this.softDecorImages.unshift(...items);
-  input.value = '';
-}
-```
-
-**文件大小校验**:
-- 建议≤1MB,超过仅警告不阻断
-- 支持 JPG、JPEG、PNG 格式
-- 自动过滤非图片文件
 
-#### 4.2.2 拖拽上传支持
-```typescript
-// project-detail.ts lines 1956-1998
-onDragOver(event: DragEvent): void {
-  event.preventDefault();
-  event.stopPropagation();
-  this.isDragOver = true;
-}
-
-onDragLeave(event: DragEvent): void {
-  event.preventDefault();
-  event.stopPropagation();
-  this.isDragOver = false;
-}
+  // 获取产品状态
+  getProductStatus(product: Parse.Object, stage: string): string {
+    const currentStage = product.get("stage");
 
-onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
-  event.preventDefault();
-  event.stopPropagation();
-  this.isDragOver = false;
-
-  const files = event.dataTransfer?.files;
-  if (!files || files.length === 0) return;
-
-  // 创建模拟的input事件
-  const mockEvent = {
-    target: {
-      files: files
+    if (currentStage === stage) {
+      return product.get("status") || "not_started";
+    } else if (this.isStageCompleted(stage, currentStage)) {
+      return "completed";
+    } else {
+      return "pending";
     }
-  } as any;
-
-  // 根据类型调用相应的处理方法
-  switch (type) {
-    case 'softDecor':
-      this.onSoftDecorSmallPicsSelected(mockEvent);
-      break;
-    // ... 其他类型
   }
-}
-```
-
-**拖拽区域样式**:
-```html
-<div class="upload-zone"
-     [class.drag-over]="isDragOver"
-     (dragover)="onDragOver($event)"
-     (dragleave)="onDragLeave($event)"
-     (drop)="onFileDrop($event, 'softDecor')">
-  <div class="upload-prompt">
-    <i class="icon-upload"></i>
-    <p>拖拽图片到此处上传</p>
-    <p class="hint">或点击选择文件(建议≤1MB)</p>
-  </div>
-</div>
-```
-
-### 4.3 图片预览
 
-#### 4.3.1 预览功能
-```typescript
-// project-detail.ts lines 1890-1903
-previewImage(img: any): void {
-  const isRenderLarge = !!this.renderLargeImages.find(i => i.id === img?.id);
-  if (isRenderLarge && img?.locked) {
-    alert('该渲染大图已加锁,需完成尾款结算并上传/识别支付凭证后方可预览。');
-    return;
-  }
-  this.previewImageData = img;
-  this.showImagePreview = true;
-}
-
-closeImagePreview(): void {
-  this.showImagePreview = false;
-  this.previewImageData = null;
-}
-```
-
-#### 4.3.2 预览弹窗
-```html
-@if (showImagePreview && previewImageData) {
-  <div class="image-preview-modal">
-    <div class="modal-overlay" (click)="closeImagePreview()"></div>
-    <div class="modal-content">
-      <div class="modal-header">
-        <h3>{{ previewImageData.name }}</h3>
-        <button class="close-btn" (click)="closeImagePreview()">×</button>
-      </div>
-      <div class="modal-body">
-        <img [src]="previewImageData.url" [alt]="previewImageData.name">
-      </div>
-      <div class="modal-footer">
-        <button class="btn-secondary" (click)="downloadImage(previewImageData)">
-          下载图片
-        </button>
-        <button class="btn-danger" (click)="removeImageFromPreview()">
-          删除图片
-        </button>
-      </div>
-    </div>
-  </div>
-}
-```
-
-### 4.4 软装阶段完成
-
-```typescript
-// project-detail.ts lines 2098-2111
-confirmSoftDecorUpload(): void {
-  // 检查软装阶段的图片数据
-  const softDecorProcess = this.deliveryProcesses.find(p => p.id === 'soft-decoration');
-  if (!softDecorProcess) return;
-
-  // 检查是否有任何空间上传了图片
-  const hasImages = softDecorProcess.spaces.some(space => {
-    const content = softDecorProcess.content[space.id];
-    return content && content.images && content.images.length > 0;
-  });
-
-  if (!hasImages) return;
-  this.advanceToNextStage('软装');
-}
-```
-
-## 5. 渲染阶段
-
-### 5.1 功能特点
-- 4K图片强制校验(最大边≥4000像素)
-- 渲染大图自动加锁
-- 渲染进度监控
-- 异常反馈系统
-
-### 5.2 4K图片校验
-
-#### 5.2.1 图片尺寸验证
-```typescript
-// 4K校验方法
-private async validateImage4K(file: File): Promise<boolean> {
-  return new Promise((resolve, reject) => {
-    const img = new Image();
-    const url = URL.createObjectURL(file);
-
-    img.onload = () => {
-      URL.revokeObjectURL(url);
-      const maxDimension = Math.max(img.width, img.height);
-
-      // 4K标准:最大边需≥4000像素
-      if (maxDimension >= 4000) {
-        resolve(true);
-      } else {
-        resolve(false);
-      }
+  // 获取产品特定字段
+  getProductSpecificFields(product: Parse.Object, stage: string): any {
+    const space = product.get("space") || {};
+    const baseFields = {
+      structuralConstraints: space.constraints || []
     };
 
-    img.onerror = () => {
-      URL.revokeObjectURL(url);
-      reject(new Error('图片加载失败'));
-    };
-
-    img.src = url;
-  });
-}
-```
-
-#### 5.2.2 渲染大图上传
-```typescript
-// project-detail.ts lines 2142-2164
-async onRenderLargePicsSelected(event: Event): Promise<void> {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
-
-  for (const f of files) {
-    const ok = await this.validateImage4K(f).catch(() => false);
-    if (!ok) {
-      alert(`图片不符合4K标准(最大边需≥4000像素):${f.name}`);
-      continue;
+    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;
     }
-    const item = this.makeImageItem(f);
-    // 直接添加到正式列表,渲染大图默认加锁
-    this.renderLargeImages.unshift({
-      id: item.id,
-      name: item.name,
-      url: item.url,
-      size: this.formatFileSize(f.size),
-      locked: true  // 渲染大图默认加锁
-    });
   }
-  input.value = '';
-}
-```
-
-**校验规则**:
-- 支持 JPG、JPEG、PNG 格式
-- 最大边(宽或高)必须≥4000像素
-- 不符合标准的图片拒绝上传并提示
-
-### 5.3 渲染大图加锁机制
-
-#### 5.3.1 加锁逻辑
-```typescript
-// 渲染大图默认加锁
-renderLargeImages: Array<{
-  id: string;
-  name: string;
-  url: string;
-  size?: string;
-  locked?: boolean;  // 加锁标记
-  reviewStatus?: 'pending' | 'approved' | 'rejected';
-  synced?: boolean;
-}> = [];
-```
-
-**加锁规则**:
-- 所有渲染大图上传后自动加锁
-- 加锁状态下不可预览和下载
-- 需完成尾款结算后自动解锁
-
-#### 5.3.2 解锁逻辑
-```typescript
-// 尾款到账后自动解锁
-onPaymentReceived(paymentInfo?: any): void {
-  // 更新结算状态
-  this.settlementRecord.status = 'completed';
-  this.settlementRecord.paidAmount = paymentInfo?.amount || this.settlementRecord.remainingAmount;
-  this.settlementRecord.paidAt = new Date();
-
-  // 解锁渲染大图
-  this.autoUnlockAndSendImages();
-
-  // 发送支付确认通知
-  this.sendPaymentConfirmationNotifications();
-}
-
-private autoUnlockAndSendImages(): void {
-  // 解锁所有渲染大图
-  this.renderLargeImages.forEach(img => {
-    img.locked = false;
-  });
-
-  console.log('✅ 渲染大图已自动解锁');
-  alert('尾款已到账,渲染大图已解锁!客服可发送给客户。');
 }
 ```
 
-### 5.4 渲染异常反馈
-
-#### 5.4.1 异常类型
-```typescript
-type ExceptionType = 'failed' | 'stuck' | 'quality' | 'other';
-
-interface ExceptionHistory {
-  id: string;
-  type: ExceptionType;
-  description: string;
-  submitTime: Date;
-  status: '待处理' | '处理中' | '已解决';
-  screenshotUrl?: string;
-  resolver?: string;
-  resolvedAt?: Date;
-}
-```
+### 2.2 产品交付进度管理
 
-#### 5.4.2 提交异常反馈
+#### 2.2.1 进度跟踪服务
 ```typescript
-// project-detail.ts lines 1715-1749
-submitExceptionFeedback(): void {
-  if (!this.exceptionDescription.trim() || this.isSubmittingFeedback) {
-    alert('请填写异常类型和描述');
-    return;
-  }
+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);
+    }
 
-  this.isSubmittingFeedback = true;
+    if (progressData.stage) {
+      product.set("stage", progressData.stage);
+    }
 
-  // 模拟提交反馈到服务器
-  setTimeout(() => {
-    const newException: ExceptionHistory = {
-      id: `exception-${Date.now()}`,
-      type: this.exceptionType,
-      description: this.exceptionDescription,
-      submitTime: new Date(),
-      status: '待处理'
+    // 更新产品数据
+    const currentData = product.get("data") || {};
+    const updatedData = {
+      ...currentData,
+      [`${stage}Progress`]: progressData.progress,
+      [`${stage}Notes`]: progressData.notes,
+      [`${stage}LastUpdated`]: new Date()
     };
 
-    // 添加到历史记录中
-    this.exceptionHistories.unshift(newException);
-
-    // 通知客服和技术支持
-    this.notifyTechnicalSupport(newException);
+    product.set("data", updatedData);
+    await product.save();
 
-    // 清空表单
-    this.exceptionDescription = '';
-    this.clearExceptionScreenshot();
-    this.showExceptionForm = false;
-
-    // 显示成功消息
-    alert('异常反馈已提交,技术支持将尽快处理');
-
-    this.isSubmittingFeedback = false;
-  }, 1000);
-}
-```
-
-#### 5.4.3 异常反馈UI
-```html
-<div class="exception-feedback-section">
-  <h4>渲染异常反馈</h4>
-
-  <button class="btn-report-exception" (click)="showExceptionForm = true">
-    报告渲染异常
-  </button>
-
-  @if (showExceptionForm) {
-    <div class="exception-form">
-      <div class="form-group">
-        <label>异常类型</label>
-        <select [(ngModel)]="exceptionType">
-          <option value="failed">渲染失败</option>
-          <option value="stuck">渲染卡顿</option>
-          <option value="quality">渲染质量问题</option>
-          <option value="other">其他问题</option>
-        </select>
-      </div>
-
-      <div class="form-group">
-        <label>问题描述</label>
-        <textarea
-          [(ngModel)]="exceptionDescription"
-          placeholder="请详细描述遇到的问题..."
-          rows="4">
-        </textarea>
-      </div>
-
-      <div class="form-group">
-        <label>上传截图(可选)</label>
-        <input
-          type="file"
-          id="screenshot-upload"
-          accept="image/*"
-          (change)="uploadExceptionScreenshot($event)">
-        @if (exceptionScreenshotUrl) {
-          <img [src]="exceptionScreenshotUrl" class="screenshot-preview">
-          <button class="btn-remove" (click)="clearExceptionScreenshot()">移除</button>
-        }
-      </div>
-
-      <div class="form-actions">
-        <button class="btn-primary"
-                (click)="submitExceptionFeedback()"
-                [disabled]="isSubmittingFeedback">
-          {{ isSubmittingFeedback ? '提交中...' : '提交反馈' }}
-        </button>
-        <button class="btn-secondary" (click)="showExceptionForm = false">
-          取消
-        </button>
-      </div>
-    </div>
+    // 触发进度更新事件
+    this.emitProgressUpdate(productId, stage, progressData);
   }
 
-  <!-- 异常历史记录 -->
-  <div class="exception-history">
-    <h5>异常记录</h5>
-    @for (exception of exceptionHistories; track exception.id) {
-      <div class="exception-item" [class.resolved]="exception.status === '已解决'">
-        <div class="exception-header">
-          <span class="type-badge">{{ getExceptionTypeText(exception.type) }}</span>
-          <span class="status-badge">{{ exception.status }}</span>
-        </div>
-        <div class="exception-content">
-          <p>{{ exception.description }}</p>
-          <span class="time">{{ formatDateTime(exception.submitTime) }}</span>
-        </div>
-      </div>
+  // 批量更新产品进度
+  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);
     }
-  </div>
-</div>
-```
-
-### 5.5 渲染阶段完成
-
-```typescript
-// project-detail.ts lines 2114-2127
-confirmRenderUpload(): void {
-  // 检查渲染阶段的图片数据
-  const renderProcess = this.deliveryProcesses.find(p => p.id === 'rendering');
-  if (!renderProcess) return;
-
-  // 检查是否有任何空间上传了图片
-  const hasImages = renderProcess.spaces.some(space => {
-    const content = renderProcess.content[space.id];
-    return content && content.images && content.images.length > 0;
-  });
-
-  if (!hasImages) return;
-  this.advanceToNextStage('渲染');
-}
-```
-
-## 6. 后期阶段
-
-### 6.1 功能特点
-- 最终图片处理
-- 色彩校正确认
-- 细节优化验证
-- 交付物整理
-
-### 6.2 后期图片上传
-
-```typescript
-// project-detail.ts lines 2030-2051
-async onPostProcessPicsSelected(event: Event): Promise<void> {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
-
-  for (const f of files) {
-    const item = this.makeImageItem(f);
-    this.postProcessImages.unshift({
-      id: item.id,
-      name: item.name,
-      url: item.url,
-      size: this.formatFileSize(f.size)
-    });
-  }
-  input.value = '';
-}
-
-removePostProcessImage(id: string): void {
-  const target = this.postProcessImages.find(i => i.id === id);
-  if (target) this.revokeUrl(target.url);
-  this.postProcessImages = this.postProcessImages.filter(i => i.id !== id);
-}
-```
-
-### 6.3 后期处理项
-
-**常见后期处理任务**:
-- 色彩校正和调整
-- 亮度/对比度优化
-- 细节锐化
-- 瑕疵修复
-- 水印添加(可选)
-- 文件格式转换
-
-### 6.4 后期阶段完成
-
-```typescript
-// project-detail.ts lines 2054-2067
-confirmPostProcessUpload(): void {
-  // 检查后期阶段的图片数据
-  const postProcessProcess = this.deliveryProcesses.find(p => p.id === 'post-processing');
-  if (!postProcessProcess) return;
-
-  // 检查是否有任何空间上传了图片
-  const hasImages = postProcessProcess.spaces.some(space => {
-    const content = postProcessProcess.content[space.id];
-    return content && content.images && content.images.length > 0;
-  });
-
-  if (!hasImages) return;
-  this.advanceToNextStage('后期');
-}
-```
-
-## 7. 统一空间文件处理
-
-### 7.1 空间文件上传
-
-#### 7.1.1 触发文件选择
-```typescript
-// project-detail.ts lines 5244-5251
-triggerSpaceFileInput(processId: string, spaceId: string): void {
-  const inputId = `space-file-input-${processId}-${spaceId}`;
-  const input = document.getElementById(inputId) as HTMLInputElement;
-  if (input) {
-    input.click();
   }
-}
-```
 
-#### 7.1.2 处理空间文件
-```typescript
-// project-detail.ts lines 5265-5284
-private handleSpaceFiles(files: File[], processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
-
-  files.forEach(file => {
-    if (/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)) {
-      const imageItem = this.makeImageItem(file);
-      process.content[spaceId].images.push({
-        id: imageItem.id,
-        name: imageItem.name,
-        url: imageItem.url,
-        size: this.formatFileSize(file.size),
-        reviewStatus: 'pending'
-      });
+  // 计算整体项目进度
+  calculateOverallProjectProgress(
+    deliveryProcesses: ProductDeliveryProcess[]
+  ): ProjectProgress {
+    const totalProducts = deliveryProcesses.reduce((sum, process) => sum + process.products.length, 0);
 
-      // 更新进度
-      this.updateSpaceProgress(processId, spaceId);
+    if (totalProducts === 0) {
+      return { total: 0, byStage: {}, byProduct: {} };
     }
-  });
-}
-```
-
-#### 7.1.3 空间文件拖拽
-```typescript
-// project-detail.ts lines 5254-5262
-onSpaceFileDrop(event: DragEvent, processId: string, spaceId: string): void {
-  event.preventDefault();
-  event.stopPropagation();
 
-  const files = event.dataTransfer?.files;
-  if (!files || files.length === 0) return;
-
-  this.handleSpaceFiles(Array.from(files), processId, spaceId);
-}
-```
-
-### 7.2 获取空间图片列表
-
-```typescript
-// project-detail.ts lines 5287-5292
-getSpaceImages(processId: string, spaceId: string): Array<{
-  id: string;
-  name: string;
-  url: string;
-  size?: string;
-  reviewStatus?: 'pending' | 'approved' | 'rejected'
-}> {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return [];
-
-  return process.content[spaceId].images || [];
-}
-```
-
-### 7.3 空间图片删除
-
-```typescript
-// 从空间中删除图片
-removeSpaceImage(processId: string, spaceId: string, imageId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
-
-  const images = process.content[spaceId].images;
-  const imageIndex = images.findIndex(img => img.id === imageId);
-
-  if (imageIndex > -1) {
-    // 释放URL资源
-    const image = images[imageIndex];
-    if (image.url && image.url.startsWith('blob:')) {
-      URL.revokeObjectURL(image.url);
-    }
-
-    // 从数组中移除
-    images.splice(imageIndex, 1);
-
-    // 更新进度
-    this.updateSpaceProgress(processId, spaceId);
-  }
-}
-```
-
-## 8. 审核流程
-
-### 8.1 审核状态管理
-
-#### 8.1.1 审核状态枚举
-```typescript
-type ReviewStatus = 'pending' | 'approved' | 'rejected';
-
-interface ImageWithReview {
-  id: string;
-  name: string;
-  url: string;
-  size?: string;
-  reviewStatus?: ReviewStatus;
-  reviewNotes?: string;
-  reviewedBy?: string;
-  reviewedAt?: Date;
-  synced?: boolean;  // 是否已同步到客户端
-}
-```
+    const progress: ProjectProgress = {
+      total: 0,
+      byStage: {},
+      byProduct: {}
+    };
 
-#### 8.1.2 审核操作
-```typescript
-// 组长审核图片
-reviewSpaceImage(
-  processId: string,
-  spaceId: string,
-  imageId: string,
-  status: ReviewStatus,
-  notes?: string
-): void {
-  if (!this.isTeamLeaderView()) {
-    alert('仅组长可以审核图片');
-    return;
-  }
+    // 计算各阶段进度
+    deliveryProcesses.forEach(process => {
+      const stageProgress = process.products.reduce((sum, product) => {
+        return sum + (process.content[product.productId]?.progress || 0);
+      }, 0);
 
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
+      progress.byStage[process.id] = Math.round(stageProgress / process.products.length);
+    });
 
-  const image = process.content[spaceId].images.find(img => img.id === imageId);
-  if (!image) return;
+    // 计算各产品进度
+    deliveryProcesses.forEach(process => {
+      process.products.forEach(product => {
+        const productProgress = process.content[product.productId]?.progress || 0;
+        progress.byProduct[product.productId] = productProgress;
+      });
+    });
 
-  // 更新审核状态
-  image.reviewStatus = status;
-  image.reviewNotes = notes;
-  image.reviewedBy = this.getCurrentUserName();
-  image.reviewedAt = new Date();
+    // 计算总体进度
+    const stageSum = Object.values(progress.byStage).reduce((sum, val) => sum + val, 0);
+    progress.total = Math.round(stageSum / Object.keys(progress.byStage).length);
 
-  // 如果审核通过,标记为已同步
-  if (status === 'approved') {
-    image.synced = true;
+    return progress;
   }
-
-  console.log(`图片审核完成: ${image.name} - ${status}`);
 }
 ```
 
-### 8.2 批量审核
+### 2.3 跨产品协调管理
 
+#### 2.3.1 产品依赖管理
 ```typescript
-// 批量审核空间内所有图片
-batchReviewSpaceImages(
-  processId: string,
-  spaceId: string,
-  status: ReviewStatus
-): void {
-  if (!this.isTeamLeaderView()) {
-    alert('仅组长可以批量审核图片');
-    return;
-  }
-
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
-
-  const images = process.content[spaceId].images;
-  const pendingImages = images.filter(img => img.reviewStatus === 'pending');
+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);
+        }
+      }
+    }
 
-  if (pendingImages.length === 0) {
-    alert('没有待审核的图片');
-    return;
+    return dependencies;
   }
 
-  const confirmed = confirm(
-    `确定要批量${status === 'approved' ? '通过' : '驳回'}${pendingImages.length}张图片吗?`
-  );
-
-  if (!confirmed) return;
+  // 分析两个产品间的依赖关系
+  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: '阳台通常连接客厅或卧室'
+      }
+    };
 
-  pendingImages.forEach(image => {
-    image.reviewStatus = status;
-    image.reviewedBy = this.getCurrentUserName();
-    image.reviewedAt = new Date();
-    if (status === 'approved') {
-      image.synced = true;
+    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
+      };
     }
-  });
-
-  alert(`已批量审核${pendingImages.length}张图片`);
-}
-```
 
-### 8.3 审核统计
-
-```typescript
-// 获取空间审核统计
-getSpaceReviewStats(processId: string, spaceId: string): {
-  total: number;
-  pending: number;
-  approved: number;
-  rejected: number;
-} {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) {
-    return { total: 0, pending: 0, approved: 0, rejected: 0 };
+    return null;
   }
 
-  const images = process.content[spaceId].images;
-
-  return {
-    total: images.length,
-    pending: images.filter(img => img.reviewStatus === 'pending').length,
-    approved: images.filter(img => img.reviewStatus === 'approved').length,
-    rejected: images.filter(img => img.reviewStatus === 'rejected').length
-  };
-}
-```
-
-## 9. 权限控制
-
-### 9.1 角色权限矩阵
-
-| 操作 | 设计师 | 渲染师 | 组长 | 技术 |
-|-----|--------|--------|------|------|
-| 查看交付执行板块 | ✅ | ✅ | ✅ | ✅ |
-| 上传建模图片 | ✅ | ❌ | ✅ | ❌ |
-| 上传软装图片 | ✅ | ❌ | ✅ | ❌ |
-| 上传渲染图片 | ❌ | ✅ | ✅ | ❌ |
-| 上传后期图片 | ✅ | ❌ | ✅ | ❌ |
-| 添加/删除空间 | ✅ | ✅ | ✅ | ❌ |
-| 审核图片 | ❌ | ❌ | ✅ | ❌ |
-| 确认阶段完成 | ✅ | ✅ | ✅ | ❌ |
-| 报告渲染异常 | ❌ | ✅ | ✅ | ❌ |
-| 最终验收 | ❌ | ❌ | ❌ | ✅ |
-
-### 9.2 权限检查方法
-
-```typescript
-// project-detail.ts lines 911-936
-isDesignerView(): boolean {
-  return this.roleContext === 'designer';
-}
-
-isTeamLeaderView(): boolean {
-  return this.roleContext === 'team-leader';
-}
-
-isTechnicalView(): boolean {
-  return this.roleContext === 'technical';
-}
-
-canEditSection(sectionKey: SectionKey): boolean {
-  if (this.isCustomerServiceView()) {
-    return sectionKey === 'order' ||
-           sectionKey === 'requirements' ||
-           sectionKey === 'aftercare';
-  }
-  return true; // 设计师和组长可以编辑所有板块
-}
+  // 生成协调建议
+  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
+      });
+    });
 
-canEditStage(stage: ProjectStage): boolean {
-  if (this.isCustomerServiceView()) {
-    const editableStages: ProjectStage[] = [
-      '订单分配', '需求沟通', '方案确认',
-      '尾款结算', '客户评价', '投诉处理'
-    ];
-    return editableStages.includes(stage);
+    return suggestions;
   }
-  return true;
-}
-```
-
-### 9.3 UI权限控制
-
-```html
-<!-- 上传按钮权限 -->
-@if (!isReadOnly() && canEditStage('建模')) {
-  <button class="btn-upload" (click)="triggerSpaceFileInput(process.id, space.id)">
-    上传图片
-  </button>
-}
-
-<!-- 审核按钮权限 -->
-@if (isTeamLeaderView()) {
-  <button class="btn-review" (click)="reviewSpaceImage(process.id, space.id, image.id, 'approved')">
-    通过
-  </button>
-  <button class="btn-reject" (click)="reviewSpaceImage(process.id, space.id, image.id, 'rejected')">
-    驳回
-  </button>
-}
-
-<!-- 删除按钮权限 -->
-@if (!isReadOnly() && (isDesignerView() || isTeamLeaderView())) {
-  <button class="btn-delete" (click)="removeSpaceImage(process.id, space.id, image.id)">
-    删除
-  </button>
 }
 ```
 
-## 10. 数据流转
-
-### 10.1 阶段推进流程
-
-```mermaid
-sequenceDiagram
-    participant Designer as 设计师
-    participant System as 系统
-    participant Leader as 组长
-    participant Next as 下一阶段
-
-    Designer->>System: 上传图片到空间
-    System->>System: 更新空间进度
-    System->>System: 计算阶段完成度
-    Designer->>System: 确认阶段上传
-    System->>System: 验证图片数量
-    alt 有图片
-        System->>Leader: 通知审核
-        Leader->>System: 审核图片
-        System->>Next: 推进到下一阶段
-    else 无图片
-        System->>Designer: 提示先上传图片
-    end
-```
-
-### 10.2 进度同步机制
+### 2.4 批量操作管理
 
+#### 2.4.1 批量操作服务
 ```typescript
-// 更新空间进度后同步到项目
-private syncProgressToProject(processId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process) return;
-
-  // 计算整体进度
-  const spaces = process.spaces;
-  const totalProgress = spaces.reduce((sum, space) => {
-    return sum + (process.content[space.id]?.progress || 0);
-  }, 0);
-
-  const averageProgress = spaces.length > 0
-    ? Math.round(totalProgress / spaces.length)
-    : 0;
-
-  // 更新项目进度
-  if (this.project) {
-    this.project.progress = averageProgress;
-  }
-
-  // 触发变更检测
-  this.cdr.detectChanges();
-}
-```
+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
+      }
+    };
 
-### 10.3 客户端数据同步
+    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
+          });
+        }
 
-```typescript
-// 审核通过后同步到客户端
-private syncApprovedImagesToClient(processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
-
-  const approvedImages = process.content[spaceId].images
-    .filter(img => img.reviewStatus === 'approved' && !img.synced);
-
-  if (approvedImages.length === 0) return;
-
-  // 调用API同步到客户端
-  this.projectService.syncImagesToClient(
-    this.projectId,
-    processId,
-    spaceId,
-    approvedImages.map(img => img.id)
-  ).subscribe({
-    next: (result) => {
-      if (result.success) {
-        // 标记为已同步
-        approvedImages.forEach(img => {
-          img.synced = true;
+        results.summary.uploaded++;
+      } catch (error) {
+        results.failed.push({
+          fileName: file.name,
+          error: error.message
         });
-        console.log(`已同步${approvedImages.length}张图片到客户端`);
+        results.summary.failed++;
       }
-    },
-    error: (error) => {
-      console.error('同步图片失败:', error);
     }
-  });
-}
-```
 
-## 11. 异常处理
-
-### 11.1 文件上传失败
-
-```typescript
-// 文件上传错误处理
-private handleFileUploadError(error: any, fileName: string): void {
-  let errorMessage = '文件上传失败';
-
-  if (error.status === 413) {
-    errorMessage = `文件过大:${fileName}(最大10MB)`;
-  } else if (error.status === 415) {
-    errorMessage = `不支持的文件格式:${fileName}`;
-  } else if (error.status === 500) {
-    errorMessage = '服务器错误,请稍后重试';
+    return results;
   }
 
-  alert(errorMessage);
-  console.error('文件上传失败:', error);
-}
-```
-
-### 11.2 4K校验失败
-
-```typescript
-// 4K校验失败处理
-private handle4KValidationFailure(file: File, dimensions: {width: number; height: number}): void {
-  const maxDimension = Math.max(dimensions.width, dimensions.height);
-
-  const message = `
-    图片不符合4K标准
-
-    文件名: ${file.name}
-    当前尺寸: ${dimensions.width} × ${dimensions.height}
-    最大边: ${maxDimension}px
-    要求: 最大边 ≥ 4000px
-
-    请使用符合4K标准的图片重新上传。
-  `;
-
-  alert(message);
-  console.warn('4K校验失败:', file.name, dimensions);
-}
-```
-
-### 11.3 渲染异常处理
-
-```typescript
-// 渲染超时预警
-checkRenderTimeout(): void {
-  if (!this.renderProgress || !this.project) return;
-
-  const deliveryTime = new Date(this.project.deadline);
-  const currentTime = new Date();
-  const timeDifference = deliveryTime.getTime() - currentTime.getTime();
-  const hoursRemaining = Math.floor(timeDifference / (1000 * 60 * 60));
-
-  if (hoursRemaining <= 3 && hoursRemaining > 0) {
-    alert('渲染进度预警:交付前3小时,请关注渲染进度');
-  }
-
-  if (hoursRemaining <= 1 && hoursRemaining > 0) {
-    alert('渲染进度严重预警:交付前1小时,渲染可能无法按时完成!');
-    this.notifyTeamLeader('render-failed');
-  }
-}
-```
-
-### 11.4 空间操作失败
-
-```typescript
-// 删除空间时的安全检查
-removeSpace(processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process) return;
-
-  const space = process.spaces.find(s => s.id === spaceId);
-  if (!space) return;
-
-  // 检查空间是否有图片
-  const hasImages = process.content[spaceId]?.images?.length > 0;
-
-  if (hasImages) {
-    const confirmed = confirm(
-      `空间"${space.name}"中有${process.content[spaceId].images.length}张图片,确定要删除吗?\n删除后图片将无法恢复。`
-    );
-    if (!confirmed) return;
+  // 批量更新产品状态
+  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();
+    }
   }
 
-  // 执行删除
-  const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
-  if (spaceIndex > -1) {
-    process.spaces.splice(spaceIndex, 1);
-
-    // 清理资源
-    if (process.content[spaceId]) {
-      process.content[spaceId].images.forEach(img => {
-        if (img.url && img.url.startsWith('blob:')) {
-          URL.revokeObjectURL(img.url);
-        }
-      });
-      delete process.content[spaceId];
+  // 批量发送审核通知
+  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
+        });
+      }
     }
-
-    console.log(`已删除空间: ${space.name}`);
   }
 }
 ```
 
-## 12. 性能优化
-
-### 12.1 Blob URL管理
-
-```typescript
-// 组件销毁时清理所有Blob URL
-ngOnDestroy(): void {
-  // 释放所有 blob 预览 URL
-  const revokeList: string[] = [];
-
-  // 收集所有Blob URL
-  this.deliveryProcesses.forEach(process => {
-    Object.values(process.content).forEach(content => {
-      content.images.forEach(img => {
-        if (img.url && img.url.startsWith('blob:')) {
-          revokeList.push(img.url);
-        }
-      });
-    });
-  });
-
-  // 批量释放
-  revokeList.forEach(url => URL.revokeObjectURL(url));
-
-  console.log(`已释放${revokeList.length}个Blob URL`);
-}
-```
-
-### 12.2 图片懒加载
-
-```typescript
-// 使用Intersection Observer实现懒加载
-private setupImageLazyLoading(): void {
-  if (!('IntersectionObserver' in window)) return;
-
-  const observer = new IntersectionObserver((entries) => {
-    entries.forEach(entry => {
-      if (entry.isIntersecting) {
-        const img = entry.target as HTMLImageElement;
-        const src = img.dataset['src'];
-        if (src) {
-          img.src = src;
-          observer.unobserve(img);
-        }
-      }
-    });
-  }, {
-    rootMargin: '50px'  // 提前50px开始加载
-  });
-
-  // 观察所有懒加载图片
-  document.querySelectorAll('img[data-src]').forEach(img => {
-    observer.observe(img);
-  });
-}
-```
+## 3. 交付执行界面设计
 
-### 12.3 进度计算优化
+### 3.1 产品交付管理主界面
+```html
+<!-- 产品交付管理主界面 -->
+<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>
 
-```typescript
-// 使用防抖避免频繁计算
-private progressUpdateDebounce: any;
+  <!-- 批量操作工具栏 -->
+  <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>
 
-private updateSpaceProgress(processId: string, spaceId: string): void {
-  // 清除之前的定时器
-  if (this.progressUpdateDebounce) {
-    clearTimeout(this.progressUpdateDebounce);
-  }
+    <div class="selection-info">
+      <span v-if="selectedProducts.length > 0">
+        已选择 {{ selectedProducts.length }} 个产品
+      </span>
+    </div>
+  </div>
 
-  // 延迟300ms执行
-  this.progressUpdateDebounce = setTimeout(() => {
-    this.doUpdateSpaceProgress(processId, spaceId);
-  }, 300);
-}
+  <!-- 产品列表 -->
+  <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>
 
-private doUpdateSpaceProgress(processId: string, spaceId: string): void {
-  const process = this.deliveryProcesses.find(p => p.id === processId);
-  if (!process || !process.content[spaceId]) return;
-
-  const content = process.content[spaceId];
-  const imageCount = content.images.length;
-
-  // 计算进度
-  if (imageCount === 0) {
-    content.progress = 0;
-    content.status = 'pending';
-  } else if (imageCount < 3) {
-    content.progress = Math.min(imageCount * 30, 90);
-    content.status = 'in_progress';
-  } else {
-    content.progress = 100;
-    content.status = 'completed';
-  }
+        <!-- 产品进度信息 -->
+        <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>
 
-  content.lastUpdated = new Date();
+        <!-- 产品报价信息 -->
+        <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>
 
-  // 同步到项目
-  this.syncProgressToProject(processId);
-}
-```
+        <!-- 展开的详细内容 -->
+        <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>
 
-## 13. 测试用例
+  <!-- 协调报告 -->
+  <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>
 
-### 13.1 空间管理测试
+    <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>
 
-```typescript
-describe('Space Management', () => {
-  it('should add new space to process', () => {
-    component.newSpaceName['modeling'] = '书房';
-    component.addSpace('modeling');
-
-    const modelingProcess = component.deliveryProcesses.find(p => p.id === 'modeling');
-    expect(modelingProcess?.spaces.length).toBe(4);
-    expect(modelingProcess?.spaces[3].name).toBe('书房');
-    expect(modelingProcess?.content['space_*']).toBeDefined();
-  });
-
-  it('should remove space and clean up resources', () => {
-    const process = component.deliveryProcesses[0];
-    const spaceId = process.spaces[0].id;
-
-    // 添加一些图片
-    process.content[spaceId].images = [
-      { id: '1', name: 'test.jpg', url: 'blob:test', size: '1MB' }
-    ];
-
-    component.removeSpace(process.id, spaceId);
-
-    expect(process.spaces.length).toBe(2);
-    expect(process.content[spaceId]).toBeUndefined();
-  });
-
-  it('should toggle space expansion', () => {
-    const process = component.deliveryProcesses[0];
-    const space = process.spaces[0];
-    const initialState = space.isExpanded;
-
-    component.toggleSpace(process.id, space.id);
-
-    expect(space.isExpanded).toBe(!initialState);
-  });
-});
+      <!-- 协调建议 -->
+      <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>
 ```
 
-### 13.2 文件上传测试
-
-```typescript
-describe('File Upload', () => {
-  it('should validate 4K images correctly', async () => {
-    const file = new File([''], 'test-4k.jpg', { type: 'image/jpeg' });
-
-    // Mock image dimensions
-    spyOn<any>(component, 'validateImage4K').and.returnValue(Promise.resolve(true));
+### 3.2 批量操作界面
+```html
+<!-- 批量上传文件弹窗 -->
+<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>
 
-    await component.onRenderLargePicsSelected({
-      target: { files: [file] }
-    } as any);
+    <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>
 
-    expect(component.renderLargeImages.length).toBeGreaterThan(0);
-    expect(component.renderLargeImages[0].locked).toBe(true);
-  });
+      <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>
 
-  it('should reject non-4K images', async () => {
-    const file = new File([''], 'small.jpg', { type: 'image/jpeg' });
+      <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>
 
-    spyOn<any>(component, 'validateImage4K').and.returnValue(Promise.resolve(false));
-    spyOn(window, 'alert');
+        <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>
 
-    await component.onRenderLargePicsSelected({
-      target: { files: [file] }
-    } as any);
+        <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>
 
-    expect(window.alert).toHaveBeenCalledWith(jasmine.stringContaining('不符合4K标准'));
-  });
+    <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>
 
-  it('should handle soft decor upload with size warning', () => {
-    const largeFile = new File(['x'.repeat(2 * 1024 * 1024)], 'large.jpg', { type: 'image/jpeg' });
+<!-- 批量状态更新弹窗 -->
+<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>
 
-    spyOn(console, 'warn');
+    <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>
 
-    component.onSoftDecorSmallPicsSelected({
-      target: { files: [largeFile] }
-    } as any);
+      <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>
 
-    expect(console.warn).toHaveBeenCalled();
-    expect(component.softDecorImages.length).toBeGreaterThan(0);
-  });
-});
-```
+      <div class="form-group">
+        <label>备注:</label>
+        <textarea v-model="batchStatusOptions.notes"
+                  placeholder="批量更新备注..."
+                  rows="3"></textarea>
+      </div>
 
-### 13.3 进度更新测试
+      <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>
 
-```typescript
-describe('Progress Tracking', () => {
-  it('should update space progress based on image count', () => {
-    const process = component.deliveryProcesses[0];
-    const spaceId = process.spaces[0].id;
-
-    // 添加2张图片
-    process.content[spaceId].images = [
-      { id: '1', name: 'img1.jpg', url: 'blob:1' },
-      { id: '2', name: 'img2.jpg', url: 'blob:2' }
-    ];
-
-    component['updateSpaceProgress'](process.id, spaceId);
-
-    expect(process.content[spaceId].progress).toBe(60);
-    expect(process.content[spaceId].status).toBe('in_progress');
-  });
-
-  it('should mark as completed with 3+ images', () => {
-    const process = component.deliveryProcesses[0];
-    const spaceId = process.spaces[0].id;
-
-    process.content[spaceId].images = [
-      { id: '1', name: 'img1.jpg', url: 'blob:1' },
-      { id: '2', name: 'img2.jpg', url: 'blob:2' },
-      { id: '3', name: 'img3.jpg', url: 'blob:3' }
-    ];
-
-    component['updateSpaceProgress'](process.id, spaceId);
-
-    expect(process.content[spaceId].progress).toBe(100);
-    expect(process.content[spaceId].status).toBe('completed');
-  });
-});
+    <div class="modal-footer">
+      <button class="btn btn-secondary"
+              @click="showBatchStatusUpdate = false">
+        取消
+      </button>
+      <button class="btn btn-primary"
+              @click="executeBatchStatusUpdate">
+        确认更新
+      </button>
+    </div>
+  </div>
+</div>
 ```
 
-### 13.4 阶段推进测试
-
-```typescript
-describe('Stage Progression', () => {
-  it('should advance to next stage after confirmation', () => {
-    // 设置建模阶段有图片
-    const modelingProcess = component.deliveryProcesses.find(p => p.id === 'modeling');
-    if (modelingProcess) {
-      modelingProcess.content['bedroom'].images = [
-        { id: '1', name: 'test.jpg', url: 'blob:test' }
-      ];
-    }
+## 4. 技术实现要点
 
-    component.currentStage = '建模';
-    component.confirmWhiteModelUpload();
+### 4.1 性能优化
+- **懒加载**:按需加载产品详情和文件列表
+- **虚拟滚动**:处理大量产品时的性能问题
+- **缓存机制**:缓存产品状态和进度数据
 
-    expect(component.currentStage).toBe('软装');
-    expect(component.expandedStages['软装']).toBe(true);
-    expect(component.expandedStages['建模']).toBe(false);
-  });
+### 4.2 用户体验优化
+- **拖拽上传**:支持文件拖拽批量上传
+- **实时同步**:进度更新实时推送到界面
+- **离线支持**:基本的离线操作支持
 
-  it('should not advance without images', () => {
-    component.currentStage = '建模';
-    const initialStage = component.currentStage;
-
-    component.confirmWhiteModelUpload();
-
-    expect(component.currentStage).toBe(initialStage);
-  });
-});
-```
+### 4.3 数据一致性
+- **事务处理**:确保批量操作的数据一致性
+- **冲突检测**:检测并发修改冲突
+- **版本控制**:文件版本管理和回滚
 
 ---
 
-**文档版本**:v1.0.0
-**创建日期**:2025-10-16
-**最后更新**:2025-10-16
-**维护人**:产品团队
+**文档版本**: v3.0 (Product表统一空间管理)
+**最后更新**: 2025-10-20
+**维护者**: YSS Development Team

+ 1484 - 19
docs/prd/项目-售后归档.md

@@ -1,4 +1,4 @@
-# 项目管理 - 售后归档阶段 PRD
+# 项目管理 - 售后归档阶段 PRD (Product表版本)
 
 ## 1. 功能概述
 
@@ -6,11 +6,12 @@
 售后归档阶段是项目管理流程的收尾环节,包含尾款结算、全景图合成、客户评价、投诉处理、项目复盘五大核心模块。该阶段负责完成项目交付、收集反馈、总结经验,为后续项目优化提供数据支撑。
 
 ### 1.2 核心目标
-- 实现自动化尾款结算流程
-- 生成全景图分享链接
-- 收集客户多维度评价
-- 处理客户投诉反馈
-- 生成项目复盘报告
+- **多产品设计售后管理**:基于Product表实现单产品到多产品项目的差异化管理
+- **实现自动化尾款结算流程**
+- **按产品生成全景图分享链接**
+- **收集客户多维度评价(按产品维度)**
+- **处理客户投诉反馈(产品定位)**
+- **生成多产品项目复盘报告**
 
 ### 1.3 涉及角色
 - **客服人员**:跟进尾款支付、发送评价链接、处理投诉
@@ -18,24 +19,1488 @@
 - **组长**:审核复盘报告、处理投诉、优化流程
 - **财务人员**:确认款项到账、核对支付凭证
 
-### 1.4 五大核心模块
+### 1.4 多产品设计售后特性
 
+#### 1.4.1 产品差异化售后策略
+- **单产品设计项目**:标准售后流程,统一结算和评价
+- **双产品设计项目**:按产品独立生成全景图,支持分别评价
+- **多产品设计项目**:
+  - 分产品尾款结算(支持按产品或整体结算)
+  - 产品全景图合集(支持单产品查看和全屋漫游)
+  - 分产品客户评价(支持整体和细分产品评价)
+  - 产品定位的投诉处理
+  - 多维度项目复盘(产品间对比分析)
+
+#### 1.4.2 产品间协同售后
 ```mermaid
 graph TD
-    A[后期完成] --> B[尾款结算]
-    B --> C[全景图合成]
-    C --> D[客户评价]
-    D --> E[投诉处理]
-    E --> F[项目复盘]
-
-    style B fill:#e8f5e9
-    style C fill:#fff3e0
-    style D fill:#e3f2fd
-    style E fill:#fce4ec
-    style F fill:#f3e5f5
+    A[所有产品设计完成] --> B{是否多产品设计项目?}
+    B -->|否| C[单产品设计售后流程]
+    B -->|是| D[多产品协同售后]
+
+    D --> E[产品设计质量检查]
+    D --> F[跨产品一致性验证]
+    D --> G[整体结算策略选择]
+    D --> H[全景图合成方案确定]
+
+    E --> I[产品间对比分析]
+    F --> I
+    G --> J[分产品/整体结算]
+    H --> K[产品全景图合集]
+    I --> L[多产品复盘报告]
+    J --> M[客户多维度评价]
+    K --> N[产品漫游体验]
+    L --> O[项目归档完成]
+    M --> O
+    N --> O
+
+    style C fill:#e8f5e9
+    style D fill:#fff3e0
+    style O fill:#e3f2fd
+```
+
+## 2. 多产品尾款结算模块
+
+### 2.1 基于Product表的多产品结算策略
+
+#### 2.1.1 结算模式选择
+```typescript
+interface MultiProductSettlementStrategy {
+  mode: 'unified' | 'separated' | 'hybrid';     // 统一结算/分产品结算/混合模式
+  products: string[];                            // 参与结算的产品ID列表
+  settlementBreakdown: ProductSettlementBreakdown[]; // 产品结算明细
+  discounts: SettlementDiscount[];               // 多产品优惠
+  paymentSchedule: PaymentSchedule[];            // 付款计划
+}
+
+interface ProductSettlementBreakdown {
+  productId: string;
+  productName: string;
+  productType: string;                           // "bedroom", "living_room" 等
+  totalAmount: number;
+  paidAmount: number;
+  remainingAmount: number;
+  completionPercentage: number;                  // 该产品完成度
+  isFullyDelivered: boolean;                    // 是否完全交付
+  specialNotes?: string;                        // 特殊说明
+}
+
+class MultiProductSettlementManager {
+  // 智能推荐结算模式
+  recommendSettlementMode(products: Product[]): SettlementModeRecommendation {
+    const totalProducts = products.length;
+    const completedProducts = products.filter(p => p.status === 'completed').length;
+    const highPriorityProducts = products.filter(p => p.space?.priority >= 8).length;
+
+    // 策略1:所有产品都完成且优先级相似,推荐统一结算
+    if (completedProducts === totalProducts && this.hasSimilarPriority(products)) {
+      return {
+        recommendedMode: 'unified',
+        confidence: 0.9,
+        reason: '所有产品已完成且优先级相近,统一结算更便捷'
+      };
+    }
+
+    // 策略2:高优先级产品已完成,推荐混合模式
+    if (highPriorityProducts > 0 && highPriorityProducts === completedProducts) {
+      return {
+        recommendedMode: 'hybrid',
+        confidence: 0.8,
+        reason: '高优先级产品已完成,可以优先结算'
+      };
+    }
+
+    // 策略3:产品完成情况差异大,推荐分产品结算
+    if (this.hasVariableCompletion(products)) {
+      return {
+        recommendedMode: 'separated',
+        confidence: 0.85,
+        reason: '各产品完成情况差异较大,分产品结算更清晰'
+      };
+    }
+
+    // 默认推荐统一结算
+    return {
+      recommendedMode: 'unified',
+      confidence: 0.6,
+      reason: '标准项目,统一结算'
+    };
+  }
+
+  // 计算多产品优惠
+  calculateMultiProductDiscount(
+    products: Product[],
+    baseTotal: number
+  ): SettlementDiscount[] {
+    const discounts: SettlementDiscount[] = [];
+
+    // 1. 产品数量折扣
+    if (products.length >= 5) {
+      discounts.push({
+        type: 'product_count',
+        description: '5产品及以上项目享受10%折扣',
+        value: baseTotal * 0.1,
+        applicable: true
+      });
+    } else if (products.length >= 3) {
+      discounts.push({
+        type: 'product_count',
+        description: '3-4产品项目享受5%折扣',
+        value: baseTotal * 0.05,
+        applicable: true
+      });
+    }
+
+    // 2. 优先级折扣
+    const highPriorityCount = products.filter(p => p.space?.priority >= 8).length;
+    if (highPriorityCount === products.length) {
+      discounts.push({
+        type: 'high_priority',
+        description: '全高优先级产品项目额外5%折扣',
+        value: baseTotal * 0.05,
+        applicable: true
+      });
+    }
+
+    // 3. 同时完成折扣
+    const completedWithinTimeframe = this.getProductsCompletedWithinTimeframe(products, 7); // 7天内
+    if (completedWithinTimeframe.length === products.length) {
+      discounts.push({
+        type: 'simultaneous_completion',
+        description: '所有产品同时完成享受3%折扣',
+        value: baseTotal * 0.03,
+        applicable: true
+      });
+    }
+
+    return discounts;
+  }
+
+  // 生成结算报告
+  generateSettlementReport(
+    strategy: MultiProductSettlementStrategy,
+    products: Product[]
+  ): SettlementReport {
+    const report: SettlementReport = {
+      projectId: this.getProjectId(),
+      settlementDate: new Date(),
+      strategy: strategy.mode,
+      totalProducts: products.length,
+      completedProducts: products.filter(p => p.status === 'completed').length,
+
+      // 财务明细
+      financials: {
+        totalBaseAmount: this.calculateBaseAmount(products),
+        discounts: strategy.discounts,
+        finalAmount: this.calculateFinalAmount(products, strategy.discounts),
+        paidAmount: this.calculatePaidAmount(products),
+        remainingAmount: 0
+      },
+
+      // 产品详情
+      productDetails: strategy.settlementBreakdown.map(breakdown => ({
+        productId: breakdown.productId,
+        productName: breakdown.productName,
+        productType: breakdown.productType,
+        totalAmount: breakdown.totalAmount,
+        discountApplied: this.getProductDiscount(breakdown.productId, strategy.discounts),
+        finalAmount: breakdown.remainingAmount,
+        completionStatus: breakdown.isFullyDelivered ? 'completed' : 'partial',
+        deliveryQuality: this.assessDeliveryQuality(breakdown.productId)
+      })),
+
+      // 风险评估
+      riskAssessment: this.assessSettlementRisks(products, strategy),
+
+      // 建议
+      recommendations: this.generateSettlementRecommendations(products, strategy)
+    };
+
+    return report;
+  }
+}
+
+interface SettlementModeRecommendation {
+  recommendedMode: 'unified' | 'separated' | 'hybrid';
+  confidence: number;    // 推荐置信度 0-1
+  reason: string;        // 推荐理由
+}
+
+interface SettlementDiscount {
+  type: 'space_count' | 'high_priority' | 'simultaneous_completion' | 'early_payment';
+  description: string;
+  value: number;
+  applicable: boolean;
+}
+
+interface SettlementReport {
+  projectId: string;
+  settlementDate: Date;
+  strategy: string;
+  totalSpaces: number;
+  completedSpaces: number;
+  financials: {
+    totalBaseAmount: number;
+    discounts: SettlementDiscount[];
+    finalAmount: number;
+    paidAmount: number;
+    remainingAmount: number;
+  };
+  spaceDetails: any[];
+  riskAssessment: any;
+  recommendations: string[];
+}
+```
+
+## 3. 多产品全景图合成模块
+
+### 3.1 基于Product表的产品全景图管理
+
+#### 3.1.1 全景图合成策略
+```typescript
+class MultiProductPanoramaManager {
+  // 生成产品全景图合集
+  async generateProductPanoramaCollection(
+    products: Product[],
+    synthesisOptions: PanoramaSynthesisOptions
+  ): Promise<PanoramaCollection> {
+
+    const collection: PanoramaCollection = {
+      id: `collection_${Date.now()}`,
+      projectId: this.getProjectId(),
+      totalProducts: products.length,
+      synthesisStrategy: synthesisOptions.strategy,
+
+      // 单产品全景图
+      productPanoramas: [],
+
+      // 跨产品连接
+      productConnections: [],
+
+      // 全屋漫游
+      fullHouseTour: null,
+
+      // 分享链接
+      shareLinks: {
+        collection: '',
+        individualProducts: {} as Record<string, string>
+      },
+
+      createdAt: new Date(),
+      status: 'processing'
+    };
+
+    // 1. 生成各产品独立全景图
+    for (const product of products) {
+      const productPanorama = await this.generateProductPanorama(product, synthesisOptions);
+      collection.productPanoramas.push(productPanorama);
+    }
+
+    // 2. 分析产品间连接关系
+    collection.productConnections = await this.analyzeProductConnections(products);
+
+    // 3. 生成全屋漫游(如果是多产品项目)
+    if (products.length > 1) {
+      collection.fullHouseTour = await this.generateFullHouseTour(
+        collection.productPanoramas,
+        collection.productConnections
+      );
+    }
+
+    // 4. 生成分享链接
+    collection.shareLinks = await this.generatePanoramaShareLinks(collection);
+
+    // 5. 保存并返回结果
+    await this.savePanoramaCollection(collection);
+
+    return collection;
+  }
+
+  // 生成单产品全景图
+  private async generateProductPanorama(
+    product: Product,
+    options: PanoramaSynthesisOptions
+  ): Promise<ProductPanorama> {
+
+    // 获取产品的最终交付图片
+    const finalImages = await this.getProductFinalImages(product.id);
+
+    // KR Panel 集成
+    const krPanelConfig = {
+      spaceType: product.productType,
+      spaceName: product.productName,
+      images: finalImages,
+      synthesisQuality: options.quality,
+      outputFormat: options.format,
+      includeHotspots: options.includeHotspots,
+      backgroundMusic: options.backgroundMusic
+    };
+
+    // 调用 KR Panel 合成
+    const panoramaData = await this.krPanelService.synthesizePanorama(krPanelConfig);
+
+    return {
+      id: `panorama_${product.id}_${Date.now()}`,
+      productId: product.id,
+      productName: product.productName,
+      productType: product.productType,
+
+      // 全景图资源
+      panoramaUrl: panoramaData.panoramaUrl,
+      thumbnailUrl: panoramaData.thumbnailUrl,
+      previewImages: panoramaData.previewImages,
+
+      // 热点信息
+      hotspots: panoramaData.hotspots || [],
+
+      // 技术参数
+      resolution: panoramaData.resolution,
+      fileSize: panoramaData.fileSize,
+      renderTime: panoramaData.renderTime,
+
+      // 元数据
+      metadata: {
+        createdAt: new Date(),
+        synthesisEngine: 'KR Panel',
+        quality: options.quality,
+        imageCount: finalImages.length
+      }
+    };
+  }
+
+  // 分析产品连接关系
+  private async analyzeProductConnections(products: Product[]): Promise<ProductConnection[]> {
+    const connections: ProductConnection[] = [];
+
+    // 基于产品类型和位置推断连接关系
+    for (let i = 0; i < products.length; i++) {
+      for (let j = i + 1; j < products.length; j++) {
+        const product1 = products[i];
+        const product2 = products[j];
+
+        const connection = await this.determineProductConnection(product1, product2);
+        if (connection) {
+          connections.push(connection);
+        }
+      }
+    }
+
+    return connections;
+  }
+
+  private async determineProductConnection(
+    product1: Product,
+    product2: Product
+  ): Promise<ProductConnection | null> {
+    // 定义常见的产品连接关系
+    const connectionRules = [
+      {
+        from: 'living_room',
+        to: 'dining_room',
+        type: 'direct',
+        transitionStyle: 'open_passage',
+        likelihood: 0.9
+      },
+      {
+        from: 'living_room',
+        to: 'corridor',
+        type: 'direct',
+        transitionStyle: 'doorway',
+        likelihood: 0.8
+      },
+      {
+        from: 'bedroom',
+        to: 'corridor',
+        type: 'direct',
+        transitionStyle: 'doorway',
+        likelihood: 0.9
+      },
+      {
+        from: 'kitchen',
+        to: 'dining_room',
+        type: 'direct',
+        transitionStyle: 'open_passage',
+        likelihood: 0.7
+      }
+    ];
+
+    // 查找匹配的连接规则
+    const rule = connectionRules.find(r =>
+      (r.from === product1.productType && r.to === product2.productType) ||
+      (r.from === product2.productType && r.to === product1.productType)
+    );
+
+    if (rule && rule.likelihood > 0.6) {
+      return {
+        fromProductId: product1.id,
+        toProductId: product2.id,
+        connectionType: rule.type,
+        transitionStyle: rule.transitionStyle,
+        confidence: rule.likelihood,
+        navigationLabel: `${product1.productName} → ${product2.productName}`,
+        estimatedDistance: this.estimateProductDistance(product1, product2)
+      };
+    }
+
+    return null;
+  }
+
+  // 生成全屋漫游
+  private async generateFullHouseTour(
+    panoramas: ProductPanorama[],
+    connections: ProductConnection[]
+  ): Promise<FullHouseTour> {
+
+    // 构建漫游路径
+    const tourPath = this.optimizeTourPath(panoramas, connections);
+
+    // 生成导航数据
+    const navigationData = {
+      panoramas: panoramas.map(p => ({
+        id: p.id,
+        name: p.productName,
+        type: p.productType,
+        url: p.panoramaUrl,
+        hotspots: p.hotspots
+      })),
+      connections: connections.map(c => ({
+        from: c.fromProductId,
+        to: c.toProductId,
+        type: c.connectionType,
+        style: c.transitionStyle,
+        label: c.navigationLabel
+      })),
+      path: tourPath
+    };
+
+    // 生成漫游配置
+    const tourConfig = {
+      autoPlay: true,
+      transitionDuration: 2000,
+      pauseDuration: 5000,
+      showNavigation: true,
+      backgroundMusic: 'soft_ambient',
+      quality: 'high'
+    };
+
+    return {
+      id: `tour_${Date.now()}`,
+      navigationData,
+      tourConfig,
+      totalDuration: this.calculateTourDuration(tourPath, tourConfig),
+      estimatedSize: this.estimateTourSize(panoramas),
+      generatedAt: new Date()
+    };
+  }
+}
+
+interface PanoramaSynthesisOptions {
+  strategy: 'individual' | 'connected' | 'full_house';
+  quality: 'standard' | 'high' | 'ultra';
+  format: 'jpg' | 'png' | 'webp';
+  includeHotspots: boolean;
+  backgroundMusic?: string;
+  maxFileSize?: number;
+}
+
+interface PanoramaCollection {
+  id: string;
+  projectId: string;
+  totalProducts: number;
+  synthesisStrategy: string;
+  productPanoramas: ProductPanorama[];
+  productConnections: ProductConnection[];
+  fullHouseTour?: FullHouseTour;
+  shareLinks: {
+    collection: string;
+    individualProducts: Record<string, string>;
+  };
+  createdAt: Date;
+  status: 'processing' | 'completed' | 'failed';
+}
+
+interface ProductPanorama {
+  id: string;
+  productId: string;
+  productName: string;
+  productType: string;
+  panoramaUrl: string;
+  thumbnailUrl: string;
+  previewImages: string[];
+  hotspots: PanoramaHotspot[];
+  resolution: { width: number; height: number };
+  fileSize: number;
+  renderTime: number;
+  metadata: any;
+}
+
+interface ProductConnection {
+  fromProductId: string;
+  toProductId: string;
+  connectionType: 'direct' | 'indirect' | 'external';
+  transitionStyle: 'doorway' | 'open_passage' | 'stair' | 'corridor';
+  confidence: number;
+  navigationLabel: string;
+  estimatedDistance: number;
+}
+
+interface FullHouseTour {
+  id: string;
+  navigationData: any;
+  tourConfig: any;
+  totalDuration: number;
+  estimatedSize: number;
+  generatedAt: Date;
+}
+```
+
+## 4. 多产品客户评价模块
+
+### 4.1 基于Product表的分产品评价系统
+
+#### 4.1.1 多维度评价结构
+```typescript
+interface MultiProductCustomerReview {
+  id: string;
+  projectId: string;
+  submittedAt: Date;
+
+  // 整体评价
+  overallReview: OverallReview;
+
+  // 产品评价
+  productReviews: ProductReview[];
+
+  // 跨产品评价
+  crossProductReview: CrossProductReview;
+
+  // 推荐意愿
+  recommendations: RecommendationData;
+}
+
+interface OverallReview {
+  // 整体满意度评分 (1-5星)
+  overallSatisfaction: number;
+
+  // 多维度评分
+  dimensionRatings: {
+    designQuality: number;         // 设计质量
+    productPlanning: number;       // 产品规划
+    colorCoordination: number;     // 色彩协调
+    functionality: number;         // 功能性
+    timeliness: number;            // 及时性
+    communication: number;         // 沟通效率
+    professionalism: number;       // 专业程度
+    valueForMoney: number;         // 性价比
+  };
+
+  // 文字评价
+  comments: {
+    strengths: string;            // 优点
+    improvements: string;         // 改进建议
+    overallImpression: string;    // 整体印象
+    additionalComments: string;   // 其他意见
+  };
+
+  // 最满意和最不满意的产品
+  mostSatisfiedProduct?: string;
+  leastSatisfiedProduct?: string;
+}
+
+interface ProductReview {
+  productId: string;
+  productName: string;
+  productType: string;             // "bedroom", "living_room" 等
+
+  // 产品满意度评分
+  satisfactionScore: number;
+
+  // 产品特定评分
+  productSpecificRatings: {
+    layoutDesign: number;         // 布局设计
+    functionality: number;         // 功能实现
+    aestheticAppeal: number;       // 美观度
+    practicality: number;         // 实用性
+    storageSolutions: number;      // 收纳方案
+    lighting: number;             // 灯光效果
+  };
+
+  // 产品使用反馈
+  usageFeedback: {
+    actualUsage: string;           // 实际使用情况
+    favoriteFeatures: string[];    // 最喜欢的特点
+    issuesEncountered: string[];   // 遇到的问题
+    modifications: string[];       // 后续改动
+    unexpectedBenefits: string[];  // 意外收获
+  };
+
+  // 产品文字评价
+  comments: {
+    whatWorkedWell: string;       // 做得好的地方
+    whatCouldBeBetter: string;     // 可以改进的地方
+    personalNotes: string;         // 个人备注
+  };
+
+  // 照片上传(实际使用后的照片)
+  afterPhotos?: string[];
+}
+
+interface CrossProductReview {
+  // 产品间一致性
+  consistencyRatings: {
+    styleConsistency: number;      // 风格一致性
+    colorFlow: number;             // 色彩流线
+    materialHarmony: number;       // 材质和谐
+    scaleProportion: number;       // 比例协调
+  };
+
+  // 动线体验
+  circulationExperience: {
+    flowLogic: number;             // 流线逻辑性
+    transitionSmoothness: number;  // 过渡流畅度
+    accessibility: number;         // 便利性
+  };
+
+  // 跨产品评价
+  crossProductComments: {
+    productRelationships: string;  // 产品关系
+    overallCohesion: string;       // 整体协调性
+    suggestedImprovements: string; // 改进建议
+  };
+}
+
+interface RecommendationData {
+  wouldRecommend: boolean;         // 是否推荐
+  likelihoodScore: number;         // 推荐意愿 0-10
+
+  // 推荐理由
+  recommendationReasons: string[];
+
+  // 不推荐原因(如果不推荐)
+  nonRecommendationReasons?: string[];
+
+  // 推荐给的人群
+  recommendedFor: string[];
+
+  // 联系信息(允许联系)
+  contactPermission: boolean;
+  contactInfo?: {
+    wechat?: string;
+    phone?: string;
+    email?: string;
+  };
+}
+
+class MultiProductReviewManager {
+  // 生成分产品评价链接
+  async generateMultiProductReviewLinks(
+    projectId: string,
+    products: Product[]
+  ): Promise<MultiProductReviewLinks> {
+
+    const links: MultiProductReviewLinks = {
+      projectId,
+      collectionLink: '',
+      productLinks: {} as Record<string, string>,
+      expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后过期
+      createdAt: new Date()
+    };
+
+    // 1. 生成合集评价链接
+    links.collectionLink = await this.generateCollectionReviewLink(projectId, products);
+
+    // 2. 生成各产品独立评价链接
+    for (const product of products) {
+      const productLink = await this.generateProductReviewLink(projectId, product);
+      links.productLinks[product.id] = productLink;
+    }
+
+    // 3. 保存链接记录
+    await this.saveReviewLinks(links);
+
+    return links;
+  }
+
+  // 处理多产品评价提交
+  async processMultiProductReview(
+    reviewData: MultiProductCustomerReview
+  ): Promise<ReviewProcessingResult> {
+
+    const result: ReviewProcessingResult = {
+      success: false,
+      reviewId: '',
+      processingSteps: []
+    };
+
+    try {
+      // 1. 验证评价数据
+      await this.validateReviewData(reviewData);
+      result.processingSteps.push({ step: 'validation', status: 'completed' });
+
+      // 2. 保存评价数据
+      const savedReview = await this.saveMultiProductReview(reviewData);
+      result.reviewId = savedReview.id;
+      result.processingSteps.push({ step: 'saving', status: 'completed' });
+
+      // 3. 计算评价统计
+      const statistics = await this.calculateReviewStatistics(reviewData);
+      result.processingSteps.push({ step: 'statistics', status: 'completed' });
+
+      // 4. 更新项目评分
+      await this.updateProjectRating(reviewData.projectId, statistics);
+      result.processingSteps.push({ step: 'rating_update', status: 'completed' });
+
+      // 5. 发送通知
+      await this.sendReviewNotifications(reviewData);
+      result.processingSteps.push({ step: 'notifications', status: 'completed' });
+
+      result.success = true;
+
+    } catch (error) {
+      console.error('处理多产品评价失败:', error);
+      result.error = error.message;
+    }
+
+    return result;
+  }
+
+  // 分析评价数据
+  async analyzeMultiProductReviews(
+    projectId: string
+  ): Promise<MultiProductReviewAnalysis> {
+
+    // 获取项目的所有评价
+    const reviews = await this.getProjectReviews(projectId);
+
+    const analysis: MultiProductReviewAnalysis = {
+      projectId,
+      totalReviews: reviews.length,
+
+      // 整体分析
+      overallAnalysis: this.analyzeOverallReviews(reviews),
+
+      // 产品分析
+      productAnalysis: this.analyzeProductReviews(reviews),
+
+      // 跨产品分析
+      crossProductAnalysis: this.analyzeCrossProductReviews(reviews),
+
+      // 趋势分析
+      trendAnalysis: this.analyzeReviewTrends(reviews),
+
+      // 改进建议
+      improvementSuggestions: this.generateImprovementSuggestions(reviews),
+
+      // 对比分析
+      benchmarkComparison: await this.benchmarkAgainstIndustry(reviews)
+    };
+
+    return analysis;
+  }
+
+  private analyzeProductReviews(reviews: MultiProductCustomerReview[]): Record<string, ProductAnalysis> {
+    const productAnalysis: Record<string, ProductAnalysis> = {};
+
+    // 按产品分组统计
+    const reviewsByProduct: Record<string, ProductReview[]> = {};
+
+    for (const review of reviews) {
+      for (const productReview of review.productReviews) {
+        if (!reviewsByProduct[productReview.productId]) {
+          reviewsByProduct[productReview.productId] = [];
+        }
+        reviewsByProduct[productReview.productId].push(productReview);
+      }
+    }
+
+    // 分析每个产品
+    for (const [productId, productReviews] of Object.entries(reviewsByProduct)) {
+      const satisfactionScores = productReviews.map(r => r.satisfactionScore);
+      const averageSatisfaction = satisfactionScores.reduce((a, b) => a + b, 0) / satisfactionScores.length;
+
+      productAnalysis[productId] = {
+        productId,
+        productName: productReviews[0].productName,
+        productType: productReviews[0].productType,
+        totalReviews: productReviews.length,
+        averageSatisfaction,
+        satisfactionDistribution: this.calculateSatisfactionDistribution(satisfactionScores),
+
+        // 详细评分分析
+        dimensionAverages: this.calculateDimensionAverages(productReviews),
+
+        // 常见反馈
+        commonStrengths: this.extractCommonStrengths(productReviews),
+        commonIssues: this.extractCommonIssues(productReviews),
+
+        // 改进建议
+        improvementSuggestions: this.generateProductImprovementSuggestions(productReviews)
+      };
+    }
+
+    return productAnalysis;
+  }
+}
+
+interface MultiProductReviewLinks {
+  projectId: string;
+  collectionLink: string;
+  productLinks: Record<string, string>;
+  expiresAt: Date;
+  createdAt: Date;
+}
+
+interface ReviewProcessingResult {
+  success: boolean;
+  reviewId: string;
+  processingSteps: Array<{
+    step: string;
+    status: 'completed' | 'failed' | 'skipped';
+    error?: string;
+  }>;
+  error?: string;
+}
+
+interface MultiProductReviewAnalysis {
+  projectId: string;
+  totalReviews: number;
+  overallAnalysis: any;
+  productAnalysis: Record<string, ProductAnalysis>;
+  crossProductAnalysis: any;
+  trendAnalysis: any;
+  improvementSuggestions: string[];
+  benchmarkComparison: any;
+}
+
+interface ProductAnalysis {
+  productId: string;
+  productName: string;
+  productType: string;
+  totalReviews: number;
+  averageSatisfaction: number;
+  satisfactionDistribution: Record<string, number>;
+  dimensionAverages: Record<string, number>;
+  commonStrengths: string[];
+  commonIssues: string[];
+  improvementSuggestions: string[];
+}
+```
+
+## 5. 多产品投诉处理模块
+
+### 5.1 基于Product表的产品定位投诉系统
+
+#### 5.1.1 产品投诉分类
+```typescript
+class MultiProductComplaintManager {
+  // 创建产品相关投诉
+  async createProductComplaint(
+    complaintData: ProductComplaintData
+  ): Promise<ProductComplaint> {
+
+    const complaint: ProductComplaint = {
+      id: `complaint_${Date.now()}`,
+      projectId: complaintData.projectId,
+
+      // 投诉分类
+      category: complaintData.category,
+      subcategory: complaintData.subcategory,
+
+      // 产品信息
+      productId: complaintData.productId,
+      productName: complaintData.productName,
+      productType: complaintData.productType,
+      affectedProducts: complaintData.affectedProducts || [],
+
+      // 投诉内容
+      title: complaintData.title,
+      description: complaintData.description,
+      severity: complaintData.severity,
+
+      // 客户信息
+      customerInfo: complaintData.customerInfo,
+
+      // 处理信息
+      status: 'pending',
+      priority: this.calculateComplaintPriority(complaintData),
+      assignedTo: null,
+      assignedAt: null,
+
+      // 时间信息
+      createdAt: new Date(),
+      expectedResolutionTime: this.calculateExpectedResolutionTime(complaintData),
+
+      // 附件
+      attachments: complaintData.attachments || []
+    };
+
+    // 保存投诉
+    const savedComplaint = await this.saveProductComplaint(complaint);
+
+    // 自动分析并分配
+    await this.autoAnalyzeAndAssign(complaint);
+
+    // 发送通知
+    await this.sendComplaintNotifications(complaint);
+
+    return savedComplaint;
+  }
+
+  // 产品投诉智能分类
+  private classifyProductComplaint(description: string, productType: string): ComplaintClassification {
+    const classification: ComplaintClassification = {
+      category: 'other',
+      subcategory: 'general',
+      confidence: 0,
+      keywords: []
+    };
+
+    // 产品特定关键词库
+    const productSpecificKeywords = {
+      'living_room': {
+        'sofa': { category: 'furniture', subcategory: 'seating' },
+        'tv': { category: 'electronics', subcategory: 'entertainment' },
+        'lighting': { category: 'lighting', subcategory: 'ambient' },
+        'storage': { category: 'storage', subcategory: 'display' }
+      },
+      'bedroom': {
+        'bed': { category: 'furniture', subcategory: 'sleeping' },
+        'wardrobe': { category: 'storage', subcategory: 'clothing' },
+        'lighting': { category: 'lighting', subcategory: 'task' },
+        'noise': { category: 'environmental', subcategory: 'acoustic' }
+      },
+      'kitchen': {
+        'cabinet': { category: 'furniture', subcategory: 'storage' },
+        'countertop': { category: 'materials', subcategory: 'surface' },
+        'appliances': { category: 'equipment', subcategory: 'kitchen' },
+        'plumbing': { category: 'systems', subcategory: 'water' }
+      }
+    };
+
+    // 通用关键词
+    const generalKeywords = {
+      'color': { category: 'aesthetics', subcategory: 'color' },
+      'size': { category: 'layout', subcategory: 'dimensions' },
+      'quality': { category: 'quality', subcategory: 'materials' },
+      'function': { category: 'functionality', subcategory: 'usage' },
+      'delivery': { category: 'service', subcategory: 'timeline' }
+    };
+
+    // 分析描述中的关键词
+    const allKeywords = {
+      ...generalKeywords,
+      ...(productSpecificKeywords[productType] || {})
+    };
+
+    const foundKeywords: Array<{ keyword: string; classification: any; confidence: number }> = [];
+
+    for (const [keyword, classification] of Object.entries(allKeywords)) {
+      if (description.toLowerCase().includes(keyword.toLowerCase())) {
+        foundKeywords.push({
+          keyword,
+          classification,
+          confidence: 0.8
+        });
+      }
+    }
+
+    if (foundKeywords.length > 0) {
+      // 选择置信度最高的分类
+      const bestMatch = foundKeywords.reduce((best, current) =>
+        current.confidence > best.confidence ? current : best
+      );
+
+      classification.category = bestMatch.classification.category;
+      classification.subcategory = bestMatch.classification.subcategory;
+      classification.confidence = bestMatch.confidence;
+      classification.keywords = foundKeywords.map(f => f.keyword);
+    }
+
+    return classification;
+  }
+
+  // 跨产品投诉处理
+  async handleCrossProductComplaint(
+    complaintData: CrossProductComplaintData
+  ): Promise<CrossProductComplaint> {
+
+    const complaint: CrossProductComplaint = {
+      id: `cross_product_complaint_${Date.now()}`,
+      projectId: complaintData.projectId,
+
+      // 跨产品特有字段
+      primaryProductId: complaintData.primaryProductId,
+      affectedProducts: complaintData.affectedProducts,
+      relationshipType: complaintData.relationshipType, // 'style_inconsistency', 'functional_conflict', 'transition_issue'
+
+      // 投诉内容
+      title: complaintData.title,
+      description: complaintData.description,
+      category: 'cross_product',
+      severity: complaintData.severity,
+
+      // 处理信息
+      status: 'pending',
+      requiresMultiProductCoordination: true,
+      assignedTeam: this.assignCrossProductTeam(complaintData),
+
+      // 时间信息
+      createdAt: new Date(),
+      expectedResolutionTime: this.calculateCrossProductResolutionTime(complaintData)
+    };
+
+    // 分析产品间关系
+    complaint.productRelationshipAnalysis = await this.analyzeProductRelationship(
+      complaint.primaryProductId,
+      complaint.affectedProducts
+    );
+
+    // 保存并处理
+    const savedComplaint = await this.saveCrossProductComplaint(complaint);
+    await this.initiateCrossProductResolution(complaint);
+
+    return savedComplaint;
+  }
+
+  // 生成投诉处理报告
+  async generateProductComplaintReport(
+    projectId: string,
+    timeRange?: { start: Date; end: Date }
+  ): Promise<ProductComplaintReport> {
+
+    const complaints = await this.getProjectProductComplaints(projectId, timeRange);
+
+    const report: ProductComplaintReport = {
+      projectId,
+      reportPeriod: timeRange || { start: new Date(0), end: new Date() },
+
+      // 统计概览
+      overview: {
+        totalComplaints: complaints.length,
+        resolvedComplaints: complaints.filter(c => c.status === 'resolved').length,
+        pendingComplaints: complaints.filter(c => c.status === 'pending').length,
+        averageResolutionTime: this.calculateAverageResolutionTime(complaints),
+        complaintRate: this.calculateComplaintRate(complaints)
+      },
+
+      // 产品分布
+      productDistribution: this.analyzeComplaintProductDistribution(complaints),
+
+      // 分类统计
+      categoryBreakdown: this.analyzeComplaintCategories(complaints),
+
+      // 严重程度分析
+      severityAnalysis: this.analyzeComplaintSeverity(complaints),
+
+      // 处理效率
+      resolutionEfficiency: this.analyzeResolutionEfficiency(complaints),
+
+      // 改进建议
+      recommendations: this.generateComplaintResolutionRecommendations(complaints),
+
+      // 趋势分析
+      trends: this.analyzeComplaintTrends(complaints)
+    };
+
+    return report;
+  }
+}
+
+interface ProductComplaintData {
+  projectId: string;
+  productId: string;
+  productName: string;
+  productType: string;
+  affectedProducts?: string[];
+  category: string;
+  subcategory: string;
+  title: string;
+  description: string;
+  severity: 'low' | 'medium' | 'high' | 'critical';
+  customerInfo: any;
+  attachments?: any[];
+}
+
+interface CrossProductComplaintData {
+  projectId: string;
+  primaryProductId: string;
+  affectedProducts: string[];
+  relationshipType: 'style_inconsistency' | 'functional_conflict' | 'transition_issue';
+  title: string;
+  description: string;
+  severity: 'low' | 'medium' | 'high' | 'critical';
+}
+
+interface ProductComplaint {
+  id: string;
+  projectId: string;
+  category: string;
+  subcategory: string;
+  productId: string;
+  productName: string;
+  productType: string;
+  affectedProducts: string[];
+  title: string;
+  description: string;
+  severity: string;
+  customerInfo: any;
+  status: string;
+  priority: number;
+  assignedTo: string;
+  assignedAt: Date;
+  createdAt: Date;
+  expectedResolutionTime: Date;
+  attachments: any[];
+}
+
+interface CrossProductComplaint {
+  id: string;
+  projectId: string;
+  primaryProductId: string;
+  affectedProducts: string[];
+  relationshipType: string;
+  title: string;
+  description: string;
+  category: string;
+  severity: string;
+  status: string;
+  requiresMultiProductCoordination: boolean;
+  assignedTeam: string[];
+  productRelationshipAnalysis: any;
+  createdAt: Date;
+  expectedResolutionTime: Date;
+}
+
+interface ProductComplaintReport {
+  projectId: string;
+  reportPeriod: { start: Date; end: Date };
+  overview: any;
+  productDistribution: any;
+  categoryBreakdown: any;
+  severityAnalysis: any;
+  resolutionEfficiency: any;
+  recommendations: string[];
+  trends: any;
+}
+```
+
+## 6. 多产品项目复盘模块
+
+### 6.1 基于Product表的产品对比分析
+
+#### 6.1.1 产品绩效对比
+```typescript
+class MultiProductReviewManager {
+  // 生成多产品项目复盘报告
+  async generateMultiProductReviewReport(
+    projectId: string,
+    options?: ReviewReportOptions
+  ): Promise<MultiProductReviewReport> {
+
+    const report: MultiProductReviewReport = {
+      id: `review_report_${Date.now()}`,
+      projectId,
+      reportType: 'multi_product',
+      generatedAt: new Date(),
+
+      // 项目概览
+      projectOverview: await this.generateProjectOverview(projectId),
+
+      // 产品对比分析
+      productComparison: await this.generateProductComparison(projectId),
+
+      // 跨产品分析
+      crossProductAnalysis: await this.generateCrossProductAnalysis(projectId),
+
+      // 效率分析
+      efficiencyAnalysis: await this.generateEfficiencyAnalysis(projectId),
+
+      // 客户满意度分析
+      satisfactionAnalysis: await this.generateSatisfactionAnalysis(projectId),
+
+      // 改进建议
+      improvementRecommendations: await this.generateImprovementRecommendations(projectId),
+
+      // 经验总结
+      lessonsLearned: await this.extractLessonsLearned(projectId)
+    };
+
+    return report;
+  }
+
+  // 生成产品对比分析
+  private async generateProductComparison(projectId: string): Promise<ProductComparisonAnalysis> {
+    const products = await this.getProjectProducts(projectId);
+
+    const comparison: ProductComparisonAnalysis = {
+      products: products.map(product => ({
+        productId: product.id,
+        productName: product.productName,
+        productType: product.productType,
+        metrics: {} as ProductMetrics
+      })),
+
+      // 对比维度
+      comparisonMetrics: [
+        'deliveryTime',
+        'qualityScore',
+        'customerSatisfaction',
+        'budgetPerformance',
+        'revisionCount',
+        'teamEfficiency'
+      ],
+
+      // 产品排名
+      productRankings: {} as Record<string, Record<string, number>>,
+
+      // 最佳实践
+      bestPractices: {},
+
+      // 改进产品
+      improvementAreas: {}
+    };
+
+    // 计算各产品指标
+    for (const product of products) {
+      comparison.products.find(p => p.productId === product.id)!.metrics =
+        await this.calculateProductMetrics(product.id);
+    }
+
+    // 生成产品排名
+    for (const metric of comparison.comparisonMetrics) {
+      const ranked = comparison.products
+        .sort((a, b) => (b.metrics as any)[metric] - (a.metrics as any)[metric])
+        .map((product, index) => ({
+          productId: product.productId,
+          rank: index + 1,
+          value: (product.metrics as any)[metric]
+        }));
+
+      comparison.productRankings[metric] = ranked;
+    }
+
+    // 识别最佳实践
+    comparison.bestPractices = this.identifyBestPractices(comparison.products);
+
+    // 识别改进区域
+    comparison.improvementAreas = this.identifyImprovementAreas(comparison.products);
+
+    return comparison;
+  }
+
+  // 计算产品指标
+  private async calculateProductMetrics(productId: string): Promise<ProductMetrics> {
+    const metrics: ProductMetrics = {
+      // 时间指标
+      deliveryTime: await this.calculateProductDeliveryTime(productId),
+      onTimeDelivery: await this.calculateOnTimeDeliveryRate(productId),
+
+      // 质量指标
+      qualityScore: await this.calculateProductQualityScore(productId),
+      revisionCount: await this.countProductRevisions(productId),
+      reworkRate: await this.calculateProductReworkRate(productId),
+
+      // 客户满意度
+      customerSatisfaction: await this.calculateProductCustomerSatisfaction(productId),
+      customerComplaints: await this.countProductComplaints(productId),
+
+      // 财务指标
+      budgetPerformance: await this.calculateProductBudgetPerformance(productId),
+      profitability: await this.calculateProductProfitability(productId),
+
+      // 团队效率
+      teamEfficiency: await this.calculateProductTeamEfficiency(productId),
+      resourceUtilization: await this.calculateProductResourceUtilization(productId)
+    };
+
+    return metrics;
+  }
+
+  // 识别最佳实践
+  private identifyBestPractices(products: any[]): Record<string, BestPractice[]> {
+    const bestPractices: Record<string, BestPractice[]> = {};
+
+    // 找出各维度表现最好的产品
+    const topPerformers = {
+      deliveryTime: this.getTopPerformer(products, 'deliveryTime', 'asc'),      // 时间越短越好
+      qualityScore: this.getTopPerformer(products, 'qualityScore', 'desc'),    // 质量越高越好
+      customerSatisfaction: this.getTopPerformer(products, 'customerSatisfaction', 'desc'),
+      budgetPerformance: this.getTopPerformer(products, 'budgetPerformance', 'desc'),
+      teamEfficiency: this.getTopPerformer(products, 'teamEfficiency', 'desc')
+    };
+
+    // 提取最佳实践
+    for (const [metric, performer] of Object.entries(topPerformers)) {
+      const practices = await this.extractBestPractices(performer.productId, metric);
+      bestPractices[metric] = practices;
+    }
+
+    return bestPractices;
+  }
+
+  // 生成跨产品分析
+  private async generateCrossProductAnalysis(projectId: string): Promise<CrossProductAnalysis> {
+    const analysis: CrossProductAnalysis = {
+      // 风格一致性分析
+      styleConsistency: await this.analyzeStyleConsistency(projectId),
+
+      // 功能协调性分析
+      functionalCoordination: await this.analyzeFunctionalCoordination(projectId),
+
+      // 产品流线分析
+      circulationFlow: await this.analyzeCirculationFlow(projectId),
+
+      // 资源配置分析
+      resourceAllocation: await this.analyzeResourceAllocation(projectId),
+
+      // 时间协调分析
+      timeCoordination: await this.analyzeTimeCoordination(projectId)
+    };
+
+    return analysis;
+  }
+
+  // 风格一致性分析
+  private async analyzeStyleConsistency(projectId: string): Promise<StyleConsistencyAnalysis> {
+    const products = await this.getProjectProducts(projectId);
+
+    // 提取各产品的设计元素
+    const designElements = await Promise.all(
+      products.map(product => this.extractProductDesignElements(product.id))
+    );
+
+    // 分析一致性
+    const consistencyAnalysis: StyleConsistencyAnalysis = {
+      overallConsistencyScore: this.calculateOverallConsistency(designElements),
+
+      // 具体维度分析
+      colorConsistency: this.analyzeColorConsistency(designElements),
+      materialConsistency: this.analyzeMaterialConsistency(designElements),
+      styleConsistency: this.analyzeStyleConsistency(designElements),
+      scaleConsistency: this.analyzeScaleConsistency(designElements),
+
+      // 不一致点识别
+      inconsistencies: this.identifyStyleInconsistencies(designElements),
+
+      // 改进建议
+      recommendations: this.generateStyleConsistencyRecommendations(designElements)
+    };
+
+    return consistencyAnalysis;
+  }
+
+  // 生成效率分析
+  private async generateEfficiencyAnalysis(projectId: string): Promise<EfficiencyAnalysis> {
+    const analysis: EfficiencyAnalysis = {
+      // 时间效率
+      timeEfficiency: await this.analyzeTimeEfficiency(projectId),
+
+      // 资源效率
+      resourceEfficiency: await this.analyzeResourceEfficiency(projectId),
+
+      // 流程效率
+      processEfficiency: await this.analyzeProcessEfficiency(projectId),
+
+      // 协作效率
+      collaborationEfficiency: await this.analyzeCollaborationEfficiency(projectId),
+
+      // 效率瓶颈
+      bottlenecks: await this.identifyEfficiencyBottlenecks(projectId),
+
+      // 优化建议
+      optimizationSuggestions: await this.generateEfficiencyOptimizationSuggestions(projectId)
+    };
+
+    return analysis;
+  }
+}
+
+interface MultiProductReviewReport {
+  id: string;
+  projectId: string;
+  reportType: string;
+  generatedAt: Date;
+  projectOverview: any;
+  productComparison: ProductComparisonAnalysis;
+  crossProductAnalysis: CrossProductAnalysis;
+  efficiencyAnalysis: EfficiencyAnalysis;
+  satisfactionAnalysis: any;
+  improvementRecommendations: any[];
+  lessonsLearned: string[];
+}
+
+interface ProductComparisonAnalysis {
+  products: Array<{
+    productId: string;
+    productName: string;
+    productType: string;
+    metrics: ProductMetrics;
+  }>;
+  comparisonMetrics: string[];
+  productRankings: Record<string, Array<{
+    productId: string;
+    rank: number;
+    value: number;
+  }>>;
+  bestPractices: Record<string, BestPractice[]>;
+  improvementAreas: Record<string, ImprovementArea[]>;
+}
+
+interface ProductMetrics {
+  deliveryTime: number;
+  onTimeDelivery: number;
+  qualityScore: number;
+  revisionCount: number;
+  reworkRate: number;
+  customerSatisfaction: number;
+  customerComplaints: number;
+  budgetPerformance: number;
+  profitability: number;
+  teamEfficiency: number;
+  resourceUtilization: number;
+}
+
+interface BestPractice {
+  title: string;
+  description: string;
+  applicableTo: SpaceType[];
+  impactLevel: 'high' | 'medium' | 'low';
+  implementationComplexity: 'simple' | 'moderate' | 'complex';
+}
+
+interface CrossSpaceAnalysis {
+  styleConsistency: StyleConsistencyAnalysis;
+  functionalCoordination: any;
+  circulationFlow: any;
+  resourceAllocation: any;
+  timeCoordination: any;
+}
+
+interface StyleConsistencyAnalysis {
+  overallConsistencyScore: number;
+  colorConsistency: any;
+  materialConsistency: any;
+  styleConsistency: any;
+  scaleConsistency: any;
+  inconsistencies: any[];
+  recommendations: string[];
+}
+
+interface EfficiencyAnalysis {
+  timeEfficiency: any;
+  resourceEfficiency: any;
+  processEfficiency: any;
+  collaborationEfficiency: any;
+  bottlenecks: any[];
+  optimizationSuggestions: string[];
+}
 ```
 
-## 2. 尾款结算模块
+---
+
+**文档版本**:v3.0 (Product表统一空间管理)
+**更新日期**:2025-10-20
+**维护者**:YSS Development Team
 
 ### 2.1 功能特点
 - 技术验收触发自动化结算

+ 1572 - 0
docs/prd/项目-空间任务逻辑.md

@@ -0,0 +1,1572 @@
+# 项目多产品任务逻辑设计 (Product表版本)
+
+## 1. 多产品场景概述
+
+### 1.1 业务背景
+映三色设计师项目管理系统面临的核心挑战是如何优雅地处理多产品场景下的项目管理。虽然大部分项目为单产品,但多产品项目在各个环节都有不同的数据组织和展示需求。
+
+### 1.2 多产品分布特征
+- **单产品设计项目**:占比约70%,主要针对单个产品设计(如客厅设计、卧室设计)
+- **双产品设计项目**:占比约20%,通常是客餐厅一体化、主卧+衣帽间等组合设计
+- **多产品设计项目**:占比约10%,全屋设计、别墅等多空间综合项目设计
+
+### 1.3 现有系统分析
+当前系统已具备产品管理的统一架构:
+- 交付执行阶段已有完整的产品管理系统(通过Product表)
+- 需求确认阶段的Product.requirements字段
+- Product表统一管理空间信息、报价、需求和评价
+
+## 2. 基于Product表的数据模型设计
+
+### 2.1 Product表统一管理
+
+#### 2.1.1 Product 表作为产品空间管理核心
+```typescript
+interface Product {
+  // 产品基本信息
+  objectId: string;
+  project: Pointer<Project>;
+  profile: Pointer<Profile>;                // 负责设计师
+  productName: string;                       // "李总主卧设计"
+  productType: string;                       // "bedroom", "living_room" 等
+  status: 'not_started' | 'in_progress' | 'awaiting_review' | 'completed';
+
+  // 空间信息字段
+  space: {
+    spaceName: string;                       // "主卧"
+    area: number;                            // 18.5
+    dimensions: {
+      length: number;
+      width: number;
+      height: number;
+    };
+    features: string[];                      // ["朝南", "飘窗", "独立卫浴"]
+    constraints: string[];                   // ["承重墙不可动"]
+    priority: number;                        // 优先级 1-10
+    complexity: string;                      // "medium"
+  };
+
+  // 产品需求字段
+  requirements: {
+    colorRequirement: Object;
+    materialRequirement: Object;
+    lightingRequirement: Object;
+    specificRequirements: string[];
+    constraints: Object;
+  };
+
+  // 产品报价字段
+  quotation: {
+    price: number;
+    currency: string;                        // "CNY"
+    breakdown: {
+      design: number;
+      modeling: number;
+      rendering: number;
+      softDecor: number;
+    };
+    status: string;                          // "pending" | "approved"
+    validUntil: Date;
+  };
+
+  // 产品评价字段
+  reviews: Array<Object>;
+}
+
+enum SpaceType {
+  LIVING_ROOM = 'living_room',      // 客厅
+  BEDROOM = 'bedroom',              // 卧室
+  KITCHEN = 'kitchen',              // 厨房
+  BATHROOM = 'bathroom',            // 卫生间
+  DINING_ROOM = 'dining_room',      // 餐厅
+  STUDY = 'study',                  // 书房
+  BALCONY = 'balcony',              // 阳台
+  CORRIDOR = 'corridor',            // 走廊
+  STORAGE = 'storage',              // 储物间
+  ENTRANCE = 'entrance',            // 玄关
+  OTHER = 'other'                   // 其他
+}
+```
+
+#### 2.1.2 空间进度管理
+```typescript
+interface SpaceProgress {
+  spaceId: string;                          // 关联空间ID
+  stage: ProjectStage;                      // 当前阶段
+  progress: number;                         // 进度百分比 0-100
+  status: ProgressStatus;
+  timeline: StageTimeline[];                // 各阶段时间线
+  blockers?: string[];                      // 阻碍因素
+  estimatedCompletion?: Date;               // 预计完成时间
+}
+
+interface StageTimeline {
+  stage: ProjectStage;
+  startTime?: Date;
+  endTime?: Date;
+  duration?: number;                        // 持续时间(小时)
+  status: 'not_started' | 'in_progress' | 'completed' | 'blocked';
+  assignee?: string;                        // 负责人ID
+}
+
+enum ProgressStatus {
+  NOT_STARTED = 'not_started',
+  IN_PROGRESS = 'in_progress',
+  AWAITING_REVIEW = 'awaiting_review',
+  COMPLETED = 'completed',
+  BLOCKED = 'blocked',
+  DELAYED = 'delayed'
+}
+```
+
+#### 2.1.3 空间人员分配
+```typescript
+interface SpaceAssignment {
+  spaceId: string;                          // 空间ID
+  stage: ProjectStage;                      // 阶段
+  assigneeId: string;                       // 负责人ID
+  assigneeName: string;                     // 负责人姓名
+  role: AssignmentRole;                     // 分配角色
+  assignedAt: Date;                         // 分配时间
+  assignedBy: string;                       // 分配人ID
+  status: 'active' | 'completed' | 'reassigned';
+  workload: number;                         // 工作量占比 0-1
+  notes?: string;                           // 分配备注
+}
+
+enum AssignmentRole {
+  PRIMARY_DESIGNER = 'primary_designer',    // 主设计师
+  MODELING_DESIGNER = 'modeling_designer',  // 建模师
+  RENDERING_DESIGNER = 'rendering_designer',// 渲染师
+  SOFT_DECOR_DESIGNER = 'soft_decor_designer', // 软装师
+  QUALITY_REVIEWER = 'quality_reviewer'     // 质量审核员
+}
+```
+
+### 2.2 各阶段数据结构适配
+
+#### 2.2.1 订单分配阶段多空间适配
+```typescript
+// 扩展 Project.quotation 数据结构
+interface MultiSpaceQuotation {
+  totalAmount: number;                      // 总金额
+  currency: string;                         // 货币单位
+
+  // 按空间分项报价
+  spaceQuotations: SpaceQuotation[];
+
+  // 按费用类型汇总
+  breakdown: {
+    design: number;                         // 设计费
+    modeling: number;                       // 建模费
+    rendering: number;                      // 渲染费
+    softDecor: number;                      // 软装费
+    postProcess: number;                    // 后期费
+  };
+
+  // 折扣信息
+  discount?: {
+    type: 'percentage' | 'fixed';
+    value: number;
+    reason: string;
+  };
+}
+
+interface SpaceQuotation {
+  spaceId: string;                          // 空间ID
+  spaceName: string;                        // 空间名称
+  amount: number;                           // 该空间金额
+  items: QuotationItem[];                   // 报价项明细
+  priority: number;                         // 优先级
+  notes?: string;                           // 备注
+}
+
+interface QuotationItem {
+  id: string;
+  category: 'design' | 'modeling' | 'rendering' | 'soft_decor' | 'post_process';
+  description: string;                      // 项目描述
+  quantity: number;                         // 数量
+  unitPrice: number;                        // 单价
+  totalPrice: number;                       // 小计
+}
+```
+
+#### 2.2.2 需求确认阶段多空间适配
+```typescript
+// 扩展 ProjectRequirement 数据结构
+interface MultiSpaceRequirement {
+  spaces: SpaceRequirement[];               // 空间需求列表
+  globalRequirements: GlobalRequirements;   // 全局需求
+  crossSpaceRequirements: CrossSpaceRequirement[]; // 跨空间需求
+}
+
+interface SpaceRequirement {
+  spaceId: string;                          // 空间ID
+  spaceName: string;                        // 空间名称
+
+  // 四大需求数据
+  colorRequirement: ColorAtmosphereRequirement;
+  spaceStructureRequirement: SpaceStructureRequirement;
+  materialRequirement: MaterialRequirement;
+  lightingRequirement: LightingRequirement;
+
+  // 空间特定需求
+  specificRequirements: {
+    functional?: string[];                  // 功能需求:收纳、展示等
+    style?: string[];                       // 风格偏好
+    constraints?: string[];                 // 限制条件:承重、管道等
+    specialFeatures?: string[];             // 特殊功能:智能家居、无障碍设计等
+  };
+
+  priority: number;                         // 优先级
+  complexity: 'simple' | 'medium' | 'complex'; // 复杂度
+}
+
+interface GlobalRequirements {
+  overallStyle: string;                     // 整体风格
+  budget: {
+    total: number;
+    currency: string;
+    breakdown?: Record<string, number>;
+  };
+  timeline: {
+    preferredStartDate?: Date;
+    deadline: Date;
+    milestones?: Array<{
+      date: Date;
+      description: string;
+    }>;
+  };
+  familyComposition: string;               // 家庭构成
+  lifestyle: string[];                     // 生活习惯
+}
+
+interface CrossSpaceRequirement {
+  type: 'style_consistency' | 'color_flow' | 'material_matching' | 'traffic_flow';
+  description: string;                      // 跨空间需求描述
+  involvedSpaces: string[];                // 涉及的空间ID列表
+  priority: number;                         // 优先级
+}
+```
+
+#### 2.2.3 交付执行阶段多空间适配
+```typescript
+// 扩展现有的 deliveryProcesses 数据结构
+interface MultiSpaceDeliveryProcess {
+  processId: string;                        // 流程ID:modeling、softDecor、rendering、postProcess
+  processName: string;                      // 流程名称
+
+  // 空间管理(增强版)
+  spaces: DeliverySpace[];
+
+  // 按空间组织的内容
+  content: Record<string, SpaceContent>;
+
+  // 跨空间协调
+  crossSpaceCoordination: {
+    dependencies: SpaceDependency[];        // 空间依赖关系
+    batchOperations: BatchOperation[];      // 批量操作
+    qualityStandards: QualityStandard[];    // 质量标准
+  };
+
+  // 整体进度管理
+  overallProgress: {
+    total: number;                          // 总体进度
+    bySpace: Record<string, number>;        // 各空间进度
+    byStage: Record<string, number>;        // 各阶段进度
+    estimatedCompletion: Date;
+  };
+}
+
+interface SpaceDependency {
+  fromSpace: string;                        // 源空间
+  toSpace: string;                          // 目标空间
+  type: 'style_reference' | 'color_flow' | 'material_matching' | 'size_reference';
+  description: string;                      // 依赖描述
+  status: 'pending' | 'satisfied' | 'blocked';
+}
+
+interface BatchOperation {
+  id: string;
+  type: 'style_sync' | 'color_adjustment' | 'material_update';
+  targetSpaces: string[];                   // 目标空间列表
+  operation: any;                           // 具体操作内容
+  status: 'pending' | 'in_progress' | 'completed';
+  createdBy: string;
+  createdAt: Date;
+}
+
+interface QualityStandard {
+  spaceType: SpaceType;                     // 空间类型
+  criteria: QualityCriterion[];             // 质量标准
+  applyToAll: boolean;                      // 是否应用到所有该类型空间
+}
+
+interface QualityCriterion {
+  aspect: string;                           // 质量维度:色彩、材质、比例等
+  standard: string;                         // 标准描述
+  tolerance: string;                        // 容差范围
+  checkMethod: string;                      // 检查方法
+}
+```
+
+#### 2.2.4 售后归档阶段多空间适配
+```typescript
+// 扩展售后数据结构
+interface MultiSpaceAfterCare {
+  spaceReviews: SpaceReview[];              // 各空间评价
+  crossSpaceAnalysis: CrossSpaceAnalysis;   // 跨空间分析
+  spaceComparison: SpaceComparison[];       // 空间对比
+}
+
+interface SpaceReview {
+  spaceId: string;                          // 空间ID
+  spaceName: string;                        // 空间名称
+
+  // 客户评价
+  customerRating: {
+    overall: number;                        // 整体评分 1-5
+    aspects: {
+      design: number;                       // 设计评分
+      functionality: number;                // 功能性评分
+      aesthetics: number;                   // 美观度评分
+      practicality: number;                 // 实用性评分
+    };
+    feedback: string;                       // 具体反馈
+  };
+
+  // 使用情况
+  usageMetrics: {
+    satisfaction: number;                   // 满意度 0-100
+    usageFrequency: string;                 // 使用频率
+    modifications: string[];                // 后续改动
+    issues: string[];                       // 问题记录
+  };
+
+  // 经济价值
+  economicValue: {
+    costPerSpace: number;                   // 单空间成本
+    perceivedValue: number;                 // 感知价值
+    roi: number;                            // 投资回报率
+  };
+}
+
+interface CrossSpaceAnalysis {
+  styleConsistency: {
+    score: number;                          // 风格一致性评分 0-100
+    issues: string[];                       // 不一致问题
+    improvements: string[];                 // 改进建议
+  };
+
+  functionalFlow: {
+    score: number;                          // 功能流线评分
+    bottlenecks: string[];                  // 瓶颈问题
+    optimizations: string[];                // 优化建议
+  };
+
+  spaceUtilization: {
+    efficiency: number;                     // 空间利用率
+    recommendations: string[];              // 优化建议
+  };
+}
+
+interface SpaceComparison {
+  spaceId: string;                          // 对比空间ID
+  comparisonType: 'before_after' | 'design_vs_reality' | 'similar_projects';
+  metrics: ComparisonMetric[];
+  insights: string[];                       // 洞察发现
+  lessons: string[];                        // 经验教训
+}
+
+interface ComparisonMetric {
+  name: string;                             // 指标名称
+  beforeValue?: number;                     // 改造前数值
+  afterValue?: number;                      // 改造后数值
+  plannedValue?: number;                    // 计划数值
+  actualValue?: number;                     // 实际数值
+  unit: string;                             // 单位
+  improvement?: number;                     // 改善程度
+}
+```
+
+## 3. 多空间业务流程设计
+
+### 3.1 空间识别与创建流程
+
+#### 3.1.1 空间识别时机
+```mermaid
+graph TD
+    A[客服接收需求] --> B{是否多空间项目?}
+    B -->|否| C[创建单空间项目]
+    B -->|是| D[分析空间需求]
+    D --> E[识别潜在空间]
+    E --> F[创建空间列表]
+    F --> G[设置空间优先级]
+    G --> H[分配空间ID]
+    H --> I[进入订单分配阶段]
+
+    style C fill:#e8f5e9
+    style I fill:#e3f2fd
+```
+
+#### 3.1.2 空间识别规则
+```typescript
+class SpaceIdentifier {
+  // 基于关键词识别空间
+  identifySpacesFromDescription(description: string): string[] {
+    const spaceKeywords = {
+      [SpaceType.LIVING_ROOM]: ['客厅', '起居室', '会客厅', '茶室'],
+      [SpaceType.BEDROOM]: ['卧室', '主卧', '次卧', '儿童房', '老人房', '客房'],
+      [SpaceType.KITCHEN]: ['厨房', '开放式厨房', '中西厨'],
+      [SpaceType.BATHROOM]: ['卫生间', '浴室', '洗手间', '盥洗室'],
+      [SpaceType.DINING_ROOM]: ['餐厅', '餐厅区', '用餐区'],
+      [SpaceType.STUDY]: ['书房', '工作室', '办公室'],
+      [SpaceType.BALCONY]: ['阳台', '露台', '花园'],
+      [SpaceType.CORRIDOR]: ['走廊', '过道', '玄关'],
+      [SpaceType.STORAGE]: ['储物间', '衣帽间', '杂物间']
+    };
+
+    const identifiedSpaces: string[] = [];
+
+    for (const [spaceType, keywords] of Object.entries(spaceKeywords)) {
+      if (keywords.some(keyword => description.includes(keyword))) {
+        identifiedSpaces.push(spaceType);
+      }
+    }
+
+    return identifiedSpaces.length > 0 ? identifiedSpaces : [SpaceType.LIVING_ROOM];
+  }
+
+  // 基于面积和预算推断空间数量
+  estimateSpaceCount(totalArea: number, budget: number): number {
+    // 基于面积的空间数量估算
+    const areaBasedCount = Math.max(1, Math.floor(totalArea / 20)); // 每20平米一个主要空间
+
+    // 基于预算的空间数量估算
+    const budgetBasedCount = Math.max(1, Math.floor(budget / 30000)); // 每3万一个空间
+
+    // 综合判断
+    return Math.min(areaBasedCount, budgetBasedCount);
+  }
+}
+```
+
+### 3.2 多空间报价策略
+
+#### 3.2.1 报价计算逻辑
+```typescript
+class MultiSpacePricingCalculator {
+  calculateSpacePricing(
+    spaces: ProjectSpace[],
+    globalRequirements: GlobalRequirements,
+    pricingRules: PricingRule[]
+  ): MultiSpaceQuotation {
+
+    const spaceQuotations: SpaceQuotation[] = [];
+    let totalAmount = 0;
+
+    for (const space of spaces) {
+      const spaceQuotation = this.calculateSingleSpacePricing(space, globalRequirements, pricingRules);
+      spaceQuotations.push(spaceQuotation);
+      totalAmount += spaceQuotation.amount;
+    }
+
+    // 应用多空间折扣
+    const discount = this.calculateMultiSpaceDiscount(spaces.length, totalAmount);
+    const finalAmount = totalAmount - discount.value;
+
+    return {
+      totalAmount: finalAmount,
+      currency: 'CNY',
+      spaceQuotations,
+      breakdown: this.calculateBreakdown(spaceQuotations),
+      discount: discount.value > 0 ? discount : undefined
+    };
+  }
+
+  private calculateSingleSpacePricing(
+    space: ProjectSpace,
+    globalRequirements: GlobalRequirements,
+    pricingRules: PricingRule[]
+  ): SpaceQuotation {
+    const basePrice = this.getBasePriceForSpaceType(space.type, space.area || 0);
+    const complexityMultiplier = this.getComplexityMultiplier(space.metadata.features || []);
+    const priorityAdjustment = this.getPriorityAdjustment(space.priority);
+
+    const items: QuotationItem[] = [
+      {
+        id: `${space.id}_design`,
+        category: 'design',
+        description: '设计费',
+        quantity: 1,
+        unitPrice: basePrice * 0.3 * complexityMultiplier,
+        totalPrice: basePrice * 0.3 * complexityMultiplier
+      },
+      {
+        id: `${space.id}_modeling`,
+        category: 'modeling',
+        description: '建模费',
+        quantity: 1,
+        unitPrice: basePrice * 0.25 * complexityMultiplier,
+        totalPrice: basePrice * 0.25 * complexityMultiplier
+      },
+      {
+        id: `${space.id}_rendering`,
+        category: 'rendering',
+        description: '渲染费',
+        quantity: 1,
+        unitPrice: basePrice * 0.25 * complexityMultiplier,
+        totalPrice: basePrice * 0.25 * complexityMultiplier
+      },
+      {
+        id: `${space.id}_soft_decor`,
+        category: 'soft_decor',
+        description: '软装费',
+        quantity: 1,
+        unitPrice: basePrice * 0.15 * complexityMultiplier,
+        totalPrice: basePrice * 0.15 * complexityMultiplier
+      },
+      {
+        id: `${space.id}_post_process`,
+        category: 'post_process',
+        description: '后期费',
+        quantity: 1,
+        unitPrice: basePrice * 0.05 * complexityMultiplier,
+        totalPrice: basePrice * 0.05 * complexityMultiplier
+      }
+    ];
+
+    const totalAmount = items.reduce((sum, item) => sum + item.totalPrice, 0);
+
+    return {
+      spaceId: space.id,
+      spaceName: space.name,
+      amount: totalAmount * priorityAdjustment,
+      items,
+      priority: space.priority,
+      notes: `复杂度系数: ${complexityMultiplier}, 优先级调整: ${priorityAdjustment}`
+    };
+  }
+
+  private calculateMultiSpaceDiscount(spaceCount: number, totalAmount: number): { type: string; value: number; reason: string } {
+    if (spaceCount >= 5) {
+      return {
+        type: 'percentage',
+        value: totalAmount * 0.1, // 10% 折扣
+        reason: '5空间以上项目享受10%折扣'
+      };
+    } else if (spaceCount >= 3) {
+      return {
+        type: 'percentage',
+        value: totalAmount * 0.05, // 5% 折扣
+        reason: '3-4空间项目享受5%折扣'
+      };
+    } else if (totalAmount > 200000) {
+      return {
+        type: 'fixed',
+        value: 5000,
+        reason: '高额度项目固定优惠5000元'
+      };
+    }
+
+    return { type: 'percentage', value: 0, reason: '无折扣' };
+  }
+}
+```
+
+### 3.3 多空间需求采集流程
+
+#### 3.3.1 需求采集策略
+```typescript
+class MultiSpaceRequirementCollector {
+  async collectRequirements(
+    spaces: ProjectSpace[],
+    globalRequirements: GlobalRequirements
+  ): Promise<MultiSpaceRequirement> {
+
+    const spaceRequirements: SpaceRequirement[] = [];
+
+    // 1. 并行采集各空间需求
+    const requirementPromises = spaces.map(space =>
+      this.collectSpaceRequirements(space, globalRequirements)
+    );
+
+    const collectedRequirements = await Promise.all(requirementPromises);
+    spaceRequirements.push(...collectedRequirements);
+
+    // 2. 分析跨空间需求
+    const crossSpaceRequirements = await this.analyzeCrossSpaceRequirements(spaceRequirements);
+
+    // 3. 验证需求一致性
+    await this.validateRequirementConsistency(spaceRequirements, crossSpaceRequirements);
+
+    return {
+      spaces: spaceRequirements,
+      globalRequirements,
+      crossSpaceRequirements
+    };
+  }
+
+  private async collectSpaceRequirements(
+    space: ProjectSpace,
+    globalRequirements: GlobalRequirements
+  ): Promise<SpaceRequirement> {
+
+    // 基于空间类型预填充需求模板
+    const template = this.getSpaceRequirementTemplate(space.type);
+
+    // 采集四大核心需求
+    const colorRequirement = await this.collectColorRequirement(space, template.colorTemplate);
+    const spaceStructureRequirement = await this.collectSpaceStructureRequirement(space, template.structureTemplate);
+    const materialRequirement = await this.collectMaterialRequirement(space, template.materialTemplate);
+    const lightingRequirement = await this.collectLightingRequirement(space, template.lightingTemplate);
+
+    // 采集空间特定需求
+    const specificRequirements = await this.collectSpecificRequirements(space, template.specificTemplate);
+
+    return {
+      spaceId: space.id,
+      spaceName: space.name,
+      colorRequirement,
+      spaceStructureRequirement,
+      materialRequirement,
+      lightingRequirement,
+      specificRequirements,
+      priority: space.priority,
+      complexity: this.assessSpaceComplexity(space, specificRequirements)
+    };
+  }
+
+  private async analyzeCrossSpaceRequirements(
+    spaceRequirements: SpaceRequirement[]
+  ): Promise<CrossSpaceRequirement[]> {
+
+    const crossSpaceRequirements: CrossSpaceRequirement[] = [];
+
+    // 分析风格一致性需求
+    const styleRequirement = this.analyzeStyleConsistency(spaceRequirements);
+    if (styleRequirement) crossSpaceRequirements.push(styleRequirement);
+
+    // 分析色彩流线需求
+    const colorFlowRequirement = this.analyzeColorFlow(spaceRequirements);
+    if (colorFlowRequirement) crossSpaceRequirements.push(colorFlowRequirement);
+
+    // 分析材质匹配需求
+    const materialMatchingRequirement = this.analyzeMaterialMatching(spaceRequirements);
+    if (materialMatchingRequirement) crossSpaceRequirements.push(materialMatchingRequirement);
+
+    // 分析动线连接需求
+    const trafficFlowRequirement = this.analyzeTrafficFlow(spaceRequirements);
+    if (trafficFlowRequirement) crossSpaceRequirements.push(trafficFlowRequirement);
+
+    return crossSpaceRequirements;
+  }
+}
+```
+
+### 3.4 多空间交付协调机制
+
+#### 3.4.1 空间依赖管理
+```typescript
+class SpaceDependencyManager {
+  analyzeSpaceDependencies(spaces: ProjectSpace[]): SpaceDependency[] {
+    const dependencies: SpaceDependency[] = [];
+
+    // 分析风格参考依赖
+    const styleDependencies = this.analyzeStyleDependencies(spaces);
+    dependencies.push(...styleDependencies);
+
+    // 分析色彩流线依赖
+    const colorDependencies = this.analyzeColorDependencies(spaces);
+    dependencies.push(...colorDependencies);
+
+    // 分析尺寸参考依赖
+    const sizeDependencies = this.analyzeSizeDependencies(spaces);
+    dependencies.push(...sizeDependencies);
+
+    return dependencies;
+  }
+
+  private analyzeStyleDependencies(spaces: ProjectSpace[]): SpaceDependency[] {
+    const dependencies: SpaceDependency[] = [];
+    const livingRoom = spaces.find(s => s.type === SpaceType.LIVING_ROOM);
+
+    if (livingRoom) {
+      // 客厅通常是风格参考基准
+      const otherSpaces = spaces.filter(s => s.id !== livingRoom.id);
+
+      for (const space of otherSpaces) {
+        dependencies.push({
+          fromSpace: livingRoom.id,
+          toSpace: space.id,
+          type: 'style_reference',
+          description: `${space.name}需要与客厅风格保持一致`,
+          status: 'pending'
+        });
+      }
+    }
+
+    return dependencies;
+  }
+
+  async resolveDependency(dependency: SpaceDependency): Promise<boolean> {
+    switch (dependency.type) {
+      case 'style_reference':
+        return await this.resolveStyleDependency(dependency);
+      case 'color_flow':
+        return await this.resolveColorDependency(dependency);
+      case 'material_matching':
+        return await this.resolveMaterialDependency(dependency);
+      case 'size_reference':
+        return await this.resolveSizeDependency(dependency);
+      default:
+        return false;
+    }
+  }
+
+  private async resolveStyleDependency(dependency: SpaceDependency): Promise<boolean> {
+    // 实现风格依赖解决逻辑
+    // 1. 获取源空间的设计方案
+    // 2. 提取关键风格元素
+    // 3. 应用到目标空间
+    // 4. 验证一致性
+
+    console.log(`解决风格依赖: ${dependency.fromSpace} -> ${dependency.toSpace}`);
+    dependency.status = 'satisfied';
+    return true;
+  }
+}
+```
+
+#### 3.4.2 批量操作支持
+```typescript
+class SpaceBatchOperationManager {
+  async executeBatchOperation(operation: BatchOperation): Promise<boolean> {
+    try {
+      operation.status = 'in_progress';
+
+      switch (operation.type) {
+        case 'style_sync':
+          return await this.executeStyleSync(operation);
+        case 'color_adjustment':
+          return await this.executeColorAdjustment(operation);
+        case 'material_update':
+          return await this.executeMaterialUpdate(operation);
+        default:
+          throw new Error(`未知的批量操作类型: ${operation.type}`);
+      }
+    } catch (error) {
+      console.error(`批量操作失败:`, error);
+      return false;
+    }
+  }
+
+  private async executeStyleSync(operation: BatchOperation): Promise<boolean> {
+    const { targetSpaces, operation: syncData } = operation;
+
+    // 获取风格同步数据
+    const sourceStyle = syncData.sourceStyle;
+    const styleElements = syncData.elements;
+
+    // 批量应用到目标空间
+    for (const spaceId of targetSpaces) {
+      await this.applyStyleToSpace(spaceId, sourceStyle, styleElements);
+    }
+
+    operation.status = 'completed';
+    return true;
+  }
+
+  private async executeColorAdjustment(operation: BatchOperation): Promise<boolean> {
+    const { targetSpaces, operation: colorData } = operation;
+
+    // 获取色彩调整数据
+    const colorPalette = colorData.colorPalette;
+    const adjustmentType = colorData.adjustmentType;
+
+    // 批量调整目标空间色彩
+    for (const spaceId of targetSpaces) {
+      await this.adjustSpaceColors(spaceId, colorPalette, adjustmentType);
+    }
+
+    operation.status = 'completed';
+    return true;
+  }
+}
+```
+
+## 4. 界面交互设计
+
+### 4.1 空间概览界面
+
+#### 4.1.1 空间卡片布局
+```html
+<!-- 空间概览界面 -->
+<div class="space-overview-container">
+  <!-- 全局信息栏 -->
+  <div class="global-info-bar">
+    <div class="project-info">
+      <h3>{{ project.title }}</h3>
+      <span class="space-count">{{ spaces.length }}个空间</span>
+      <span class="total-budget">总预算: ¥{{ totalBudget.toLocaleString() }}</span>
+    </div>
+
+    <div class="overall-progress">
+      <div class="progress-circle">
+        <svg width="120" height="120">
+          <circle cx="60" cy="60" r="50" fill="none" stroke="#e0e0e0" stroke-width="8"/>
+          <circle cx="60" cy="60" r="50" fill="none" stroke="#4CAF50" stroke-width="8"
+                  [attr.stroke-dasharray]="circumference"
+                  [attr.stroke-dashoffset]="progressOffset"/>
+        </svg>
+        <div class="progress-text">
+          <span class="percentage">{{ overallProgress }}%</span>
+          <span class="label">总体进度</span>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 空间卡片网格 -->
+  <div class="spaces-grid">
+    @for (space of spaces; track space.id) {
+      <div class="space-card"
+           [class.priority-high]="space.priority >= 8"
+           [class.priority-medium]="space.priority >= 5 && space.priority < 8"
+           [class.status-completed]="space.status === 'completed'"
+           [class.status-in-progress]="space.status === 'in_progress'">
+
+        <!-- 空间头部 -->
+        <div class="space-header">
+          <div class="space-icon">
+            <i class="icon-{{ getSpaceIcon(space.type) }}"></i>
+          </div>
+          <div class="space-info">
+            <h4>{{ space.name }}</h4>
+            <span class="space-type">{{ getSpaceTypeName(space.type) }}</span>
+            @if (space.area) {
+              <span class="space-area">{{ space.area }}m²</span>
+            }
+          </div>
+          <div class="space-actions">
+            <button class="btn-icon" (click)="editSpace(space.id)" title="编辑">
+              <i class="icon-edit"></i>
+            </button>
+            <button class="btn-icon" (click)="viewSpaceDetails(space.id)" title="查看详情">
+              <i class="icon-view"></i>
+            </button>
+          </div>
+        </div>
+
+        <!-- 空间进度 -->
+        <div class="space-progress">
+          <div class="progress-bar">
+            <div class="progress-fill"
+                 [style.width.%]="getSpaceProgress(space.id)"
+                 [class.color-warning]="getSpaceProgress(space.id) < 50"
+                 [class.color-success]="getSpaceProgress(space.id) >= 80">
+            </div>
+          </div>
+          <span class="progress-text">{{ getSpaceProgress(space.id) }}%</span>
+        </div>
+
+        <!-- 当前阶段 -->
+        <div class="current-stage">
+          <span class="stage-label">当前阶段:</span>
+          <span class="stage-value">{{ getCurrentStage(space.id) }}</span>
+        </div>
+
+        <!-- 负责人 -->
+        <div class="assignee-info">
+          @if (getSpaceAssignee(space.id)) {
+            <div class="assignee-avatar">
+              <img [src]="getSpaceAssignee(space.id).avatar" [alt]="getSpaceAssignee(space.id).name">
+            </div>
+            <span class="assignee-name">{{ getSpaceAssignee(space.id).name }}</span>
+          } @else {
+            <span class="no-assignee">未分配</span>
+          }
+        </div>
+
+        <!-- 空间状态标签 -->
+        <div class="space-tags">
+          @if (space.priority >= 8) {
+            <span class="tag tag-high">高优先级</span>
+          }
+          @if (getSpaceComplexity(space.id) === 'complex') {
+            <span class="tag tag-complex">复杂</span>
+          }
+          @if (hasCrossSpaceDependencies(space.id)) {
+            <span class="tag tag-dependency">依赖</span>
+          }
+        </div>
+
+        <!-- 快速操作 -->
+        <div class="quick-actions">
+          <button class="btn-small"
+                  [disabled]="!canAdvanceStage(space.id)"
+                  (click)="advanceSpaceStage(space.id)">
+            推进阶段
+          </button>
+          <button class="btn-small btn-secondary"
+                  (click)="viewSpaceFiles(space.id)">
+            查看文件
+          </button>
+        </div>
+      </div>
+    }
+
+    <!-- 添加新空间卡片 -->
+    <div class="space-card add-space-card" (click)="showAddSpaceDialog = true">
+      <div class="add-space-content">
+        <i class="icon-plus"></i>
+        <span>添加空间</span>
+      </div>
+    </div>
+  </div>
+</div>
+```
+
+#### 4.1.2 空间详情弹窗
+```html
+<!-- 空间详情弹窗 -->
+<div class="space-detail-modal" *ngIf="selectedSpaceId">
+  <div class="modal-overlay" (click)="closeSpaceDetails()"></div>
+  <div class="modal-content large">
+    <div class="modal-header">
+      <h3>{{ getSpaceName(selectedSpaceId) }} - 详细信息</h3>
+      <div class="header-actions">
+        <button class="btn-secondary" (click)="editSpace(selectedSpaceId)">
+          <i class="icon-edit"></i> 编辑空间
+        </button>
+        <button class="btn-secondary" (click)="exportSpaceReport(selectedSpaceId)">
+          <i class="icon-export"></i> 导出报告
+        </button>
+        <button class="btn-icon" (click)="closeSpaceDetails()">
+          <i class="icon-close"></i>
+        </button>
+      </div>
+    </div>
+
+    <div class="modal-body">
+      <!-- 标签页导航 -->
+      <div class="tab-navigation">
+        <button class="tab-btn"
+                [class.active]="activeTab === 'overview'"
+                (click)="activeTab = 'overview'">
+          概览
+        </button>
+        <button class="tab-btn"
+                [class.active]="activeTab === 'requirements'"
+                (click)="activeTab = 'requirements'">
+          需求
+        </button>
+        <button class="tab-btn"
+                [class.active]="activeTab === 'delivery'"
+                (click)="activeTab = 'delivery'">
+          交付
+        </button>
+        <button class="tab-btn"
+                [class.active]="activeTab === 'timeline'"
+                (click)="activeTab = 'timeline'">
+          时间线
+        </button>
+        <button class="tab-btn"
+                [class.active]="activeTab === 'dependencies'"
+                (click)="activeTab = 'dependencies'">
+          依赖关系
+        </button>
+      </div>
+
+      <!-- 标签页内容 -->
+      <div class="tab-content">
+        <!-- 概览标签页 -->
+        <div *ngIf="activeTab === 'overview'" class="overview-tab">
+          <div class="space-metadata">
+            <h4>空间信息</h4>
+            <div class="metadata-grid">
+              <div class="metadata-item">
+                <label>空间类型:</label>
+                <span>{{ getSpaceTypeName(getSpace(selectedSpaceId).type) }}</span>
+              </div>
+              <div class="metadata-item">
+                <label>面积:</label>
+                <span>{{ getSpace(selectedSpaceId).area }}m²</span>
+              </div>
+              <div class="metadata-item">
+                <label>优先级:</label>
+                <span class="priority-badge priority-{{ getSpace(selectedSpaceId).priority }}">
+                  {{ getSpace(selectedSpaceId).priority }}
+                </span>
+              </div>
+              <div class="metadata-item">
+                <label>复杂度:</label>
+                <span>{{ getSpaceComplexity(selectedSpaceId) }}</span>
+              </div>
+            </div>
+          </div>
+
+          <div class="space-progress-detail">
+            <h4>进度详情</h4>
+            <div class="progress-stages">
+              @for (stage of getAllStages(); track stage) {
+                <div class="stage-progress-item"
+                     [class.completed]="isStageCompleted(selectedSpaceId, stage)"
+                     [class.current]="isCurrentStage(selectedSpaceId, stage)">
+                  <div class="stage-icon">
+                    <i class="icon-{{ getStageIcon(stage) }}"></i>
+                  </div>
+                  <div class="stage-info">
+                    <span class="stage-name">{{ stage }}</span>
+                    <span class="stage-time">{{ getStageTime(selectedSpaceId, stage) }}</span>
+                  </div>
+                  <div class="stage-progress">
+                    <div class="progress-bar small">
+                      <div class="progress-fill"
+                           [style.width.%]="getStageProgress(selectedSpaceId, stage)">
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              }
+            </div>
+          </div>
+        </div>
+
+        <!-- 需求标签页 -->
+        <div *ngIf="activeTab === 'requirements'" class="requirements-tab">
+          <app-space-requirements-view
+            [spaceId]="selectedSpaceId"
+            [readonly]="isReadOnly()">
+          </app-space-requirements-view>
+        </div>
+
+        <!-- 交付标签页 -->
+        <div *ngIf="activeTab === 'delivery'" class="delivery-tab">
+          <app-space-delivery-view
+            [spaceId]="selectedSpaceId"
+            [readonly]="isReadOnly()">
+          </app-space-delivery-view>
+        </div>
+
+        <!-- 时间线标签页 -->
+        <div *ngIf="activeTab === 'timeline'" class="timeline-tab">
+          <app-space-timeline-view
+            [spaceId]="selectedSpaceId">
+          </app-space-timeline-view>
+        </div>
+
+        <!-- 依赖关系标签页 -->
+        <div *ngIf="activeTab === 'dependencies'" class="dependencies-tab">
+          <app-space-dependencies-view
+            [spaceId]="selectedSpaceId">
+          </app-space-dependencies-view>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+```
+
+### 4.2 多空间文件管理界面
+
+#### 4.2.1 空间文件浏览器
+```html
+<!-- 多空间文件浏览器 -->
+<div class="multi-space-file-browser">
+  <!-- 空间选择器 -->
+  <div class="space-selector">
+    <div class="space-tabs">
+      @for (space of spaces; track space.id) {
+        <button class="space-tab"
+                [class.active]="selectedSpaceId === space.id"
+                [class.has-files]="getSpaceFileCount(space.id) > 0"
+                (click)="selectSpace(space.id)">
+          <div class="tab-content">
+            <i class="icon-{{ getSpaceIcon(space.type) }}"></i>
+            <span class="space-name">{{ space.name }}</span>
+            <span class="file-count" *ngIf="getSpaceFileCount(space.id) > 0">
+              {{ getSpaceFileCount(space.id) }}
+            </span>
+          </div>
+        </button>
+      }
+    </div>
+
+    <!-- 全选/批量操作 -->
+    <div class="batch-actions">
+      <label class="checkbox-label">
+        <input type="checkbox"
+               [(ngModel)]="selectAllSpaces"
+               (change)="toggleSelectAllSpaces()">
+        <span>全选空间</span>
+      </label>
+
+      @if (selectedSpaces.length > 0) {
+        <div class="selected-actions">
+          <span class="selected-count">已选择 {{ selectedSpaces.length }} 个空间</span>
+          <button class="btn-small" (click)="batchUploadFiles()">
+            批量上传
+          </button>
+          <button class="btn-small btn-secondary" (click)="batchDownloadFiles()">
+            批量下载
+          </button>
+        </div>
+      }
+    </div>
+  </div>
+
+  <!-- 文件列表 -->
+  <div class="file-content-area">
+    @if (selectedSpaceId) {
+      <div class="space-file-view">
+        <!-- 当前空间信息 -->
+        <div class="current-space-header">
+          <div class="space-info">
+            <i class="icon-{{ getSpaceIcon(getSpace(selectedSpaceId).type) }}"></i>
+            <h4>{{ getSpace(selectedSpaceId).name }}</h4>
+            <span class="file-total">{{ getSpaceFileCount(selectedSpaceId) }} 个文件</span>
+          </div>
+
+          <div class="view-options">
+            <div class="view-toggle">
+              <button class="btn-icon"
+                      [class.active]="viewMode === 'grid'"
+                      (click)="viewMode = 'grid'"
+                      title="网格视图">
+                <i class="icon-grid"></i>
+              </button>
+              <button class="btn-icon"
+                      [class.active]="viewMode === 'list'"
+                      (click)="viewMode = 'list'"
+                      title="列表视图">
+                <i class="icon-list"></i>
+              </button>
+            </div>
+
+            <button class="btn-primary" (click)="triggerFileUpload(selectedSpaceId)">
+              <i class="icon-upload"></i> 上传文件
+            </button>
+          </div>
+        </div>
+
+        <!-- 文件上传区域 -->
+        <div class="upload-zone"
+             [class.drag-over]="isDragOver"
+             (dragover)="isDragOver = true"
+             (dragleave)="isDragOver = false"
+             (drop)="handleFileDrop($event, selectedSpaceId)">
+          <div class="upload-prompt">
+            <i class="icon-upload"></i>
+            <p>拖拽文件到此处上传</p>
+            <p class="hint">或点击上传按钮选择文件</p>
+          </div>
+        </div>
+
+        <!-- 文件网格视图 -->
+        @if (viewMode === 'grid') {
+          <div class="files-grid">
+            @for (file of getSpaceFiles(selectedSpaceId); track file.id) {
+              <div class="file-card"
+                   [class.selected]="selectedFiles.has(file.id)"
+                   (click)="toggleFileSelection(file.id)">
+                <div class="file-preview">
+                  @if (isImageFile(file)) {
+                    <img [src]="file.url" [alt]="file.name">
+                  } @else {
+                    <div class="file-icon-placeholder">
+                      <i class="icon-{{ getFileIcon(file.type) }}"></i>
+                    </div>
+                  }
+                </div>
+
+                <div class="file-info">
+                  <span class="file-name" [title]="file.name">{{ file.name }}</span>
+                  <span class="file-size">{{ formatFileSize(file.size) }}</span>
+                  <span class="file-date">{{ formatDate(file.uploadTime) }}</span>
+                </div>
+
+                <div class="file-actions">
+                  <button class="btn-icon" (click)="previewFile(file)" title="预览">
+                    <i class="icon-eye"></i>
+                  </button>
+                  <button class="btn-icon" (click)="downloadFile(file)" title="下载">
+                    <i class="icon-download"></i>
+                  </button>
+                  <button class="btn-icon" (click)="deleteFile(file)" title="删除">
+                    <i class="icon-delete"></i>
+                  </button>
+                </div>
+              </div>
+            }
+          </div>
+        }
+
+        <!-- 文件列表视图 -->
+        @if (viewMode === 'list') {
+          <div class="files-list">
+            <div class="list-header">
+              <div class="header-cell">
+                <input type="checkbox"
+                       [(ngModel)]="selectAllFiles"
+                       (change)="toggleSelectAllFiles()">
+              </div>
+              <div class="header-cell">文件名</div>
+              <div class="header-cell">大小</div>
+              <div class="header-cell">类型</div>
+              <div class="header-cell">上传时间</div>
+              <div class="header-cell">操作</div>
+            </div>
+
+            @for (file of getSpaceFiles(selectedSpaceId); track file.id) {
+              <div class="list-row"
+                   [class.selected]="selectedFiles.has(file.id)">
+                <div class="list-cell">
+                  <input type="checkbox"
+                         [(ngModel)]="selectedFiles.has(file.id)"
+                         (change)="toggleFileSelection(file.id)">
+                </div>
+                <div class="list-cell file-name-cell">
+                  <i class="icon-{{ getFileIcon(file.type) }}"></i>
+                  <span>{{ file.name }}</span>
+                </div>
+                <div class="list-cell">{{ formatFileSize(file.size) }}</div>
+                <div class="list-cell">{{ getFileTypeLabel(file.type) }}</div>
+                <div class="list-cell">{{ formatDate(file.uploadTime) }}</div>
+                <div class="list-cell actions-cell">
+                  <button class="btn-icon small" (click)="previewFile(file)" title="预览">
+                    <i class="icon-eye"></i>
+                  </button>
+                  <button class="btn-icon small" (click)="downloadFile(file)" title="下载">
+                    <i class="icon-download"></i>
+                  </button>
+                  <button class="btn-icon small" (click)="deleteFile(file)" title="删除">
+                    <i class="icon-delete"></i>
+                  </button>
+                </div>
+              </div>
+            }
+          </div>
+        }
+      </div>
+    } @else {
+      <div class="no-space-selected">
+        <i class="icon-folder"></i>
+        <p>请选择一个空间查看文件</p>
+      </div>
+    }
+  </div>
+</div>
+```
+
+## 5. 数据库迁移方案
+
+### 5.1 数据结构变更
+
+#### 5.1.1 Project 表迁移
+```sql
+-- 为 Project 表添加多空间支持字段
+ALTER TABLE Project ADD COLUMN spaceType VARCHAR(20) DEFAULT 'single';
+ALTER TABLE Project ADD COLUMN spaces JSON;
+ALTER TABLE Project ADD COLUMN spaceProgress JSON;
+ALTER TABLE Project ADD COLUMN spaceAssignment JSON;
+
+-- 创建空间索引
+CREATE INDEX idx_project_spaceType ON Project(spaceType);
+CREATE INDEX idx_project_spaces ON Project USING GIN(spaces);
+```
+
+#### 5.1.2 新增空间相关表
+```sql
+-- 创建项目空间表
+CREATE TABLE ProjectSpace (
+  id VARCHAR(50) PRIMARY KEY,
+  projectId VARCHAR(50) NOT NULL,
+  name VARCHAR(100) NOT NULL,
+  type VARCHAR(50) NOT NULL,
+  area DECIMAL(8,2),
+  priority INTEGER DEFAULT 5,
+  status VARCHAR(20) DEFAULT 'pending',
+  metadata JSON,
+  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+  FOREIGN KEY (projectId) REFERENCES Project(objectId)
+);
+
+-- 创建空间进度表
+CREATE TABLE SpaceProgress (
+  id VARCHAR(50) PRIMARY KEY,
+  spaceId VARCHAR(50) NOT NULL,
+  stage VARCHAR(50) NOT NULL,
+  progress INTEGER DEFAULT 0,
+  status VARCHAR(20) DEFAULT 'not_started',
+  timeline JSON,
+  blockers JSON,
+  estimatedCompletion DATETIME,
+  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+  FOREIGN KEY (spaceId) REFERENCES ProjectSpace(id)
+);
+
+-- 创建空间分配表
+CREATE TABLE SpaceAssignment (
+  id VARCHAR(50) PRIMARY KEY,
+  spaceId VARCHAR(50) NOT NULL,
+  stage VARCHAR(50) NOT NULL,
+  assigneeId VARCHAR(50) NOT NULL,
+  assigneeName VARCHAR(100) NOT NULL,
+  role VARCHAR(50) NOT NULL,
+  assignedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
+  assignedBy VARCHAR(50),
+  status VARCHAR(20) DEFAULT 'active',
+  workload DECIMAL(3,2) DEFAULT 0.0,
+  notes TEXT,
+  FOREIGN KEY (spaceId) REFERENCES ProjectSpace(id)
+);
+```
+
+### 5.2 数据迁移脚本
+
+#### 5.2.1 现有项目数据迁移
+```typescript
+class DataMigrationService {
+  async migrateExistingProjects(): Promise<void> {
+    console.log('开始迁移现有项目数据...');
+
+    // 1. 获取所有现有项目
+    const projects = await this.getAllProjects();
+
+    for (const project of projects) {
+      await this.migrateProject(project);
+    }
+
+    console.log('项目数据迁移完成');
+  }
+
+  private async migrateProject(project: any): Promise<void> {
+    // 2. 分析项目是否为多空间
+    const isMultiSpace = await this.analyzeProjectSpaceType(project);
+
+    if (isMultiSpace) {
+      // 3. 创建空间记录
+      const spaces = await this.createSpacesForProject(project);
+
+      // 4. 更新项目记录
+      await this.updateProjectWithSpaces(project.objectId, spaces);
+
+      // 5. 迁移交付数据到空间维度
+      await this.migrateDeliveryData(project, spaces);
+
+      // 6. 迁移需求数据到空间维度
+      await this.migrateRequirementData(project, spaces);
+    } else {
+      // 单空间项目,创建默认空间
+      const defaultSpace = await this.createDefaultSpace(project);
+      await this.updateProjectWithSpaces(project.objectId, [defaultSpace]);
+    }
+  }
+
+  private async analyzeProjectSpaceType(project: any): Promise<boolean> {
+    // 基于项目标题、描述、文件等信息判断是否为多空间
+    const indicators = [
+      project.title?.includes('全屋') || project.title?.includes('整套'),
+      project.data?.description?.includes('多空间'),
+      (project.data?.quotation?.items?.length || 0) > 3,
+      await this.hasMultipleRoomTypes(project)
+    ];
+
+    return indicators.some(indicator => indicator === true);
+  }
+
+  private async createSpacesForProject(project: any): Promise<ProjectSpace[]> {
+    const spaces: ProjectSpace[] = [];
+
+    // 基于报价项创建空间
+    if (project.data?.quotation?.items) {
+      for (const item of project.data.quotation.items) {
+        const spaceType = this.inferSpaceTypeFromDescription(item.description);
+        if (spaceType) {
+          const space: ProjectSpace = {
+            id: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+            projectId: project.objectId,
+            name: item.room || this.getDefaultSpaceName(spaceType),
+            type: spaceType,
+            priority: this.calculateSpacePriority(item.amount),
+            status: 'pending',
+            metadata: {
+              budgetAllocation: item.amount
+            },
+            createdAt: new Date(),
+            updatedAt: new Date()
+          };
+
+          spaces.push(space);
+        }
+      }
+    }
+
+    // 如果没有从报价识别出空间,创建默认空间
+    if (spaces.length === 0) {
+      const defaultSpace = await this.createDefaultSpace(project);
+      spaces.push(defaultSpace);
+    }
+
+    return spaces;
+  }
+
+  private async createDefaultSpace(project: any): Promise<ProjectSpace> {
+    return {
+      id: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+      projectId: project.objectId,
+      name: '主空间',
+      type: SpaceType.LIVING_ROOM,
+      priority: 5,
+      status: 'pending',
+      metadata: {},
+      createdAt: new Date(),
+      updatedAt: new Date()
+    };
+  }
+
+  private async migrateDeliveryData(project: any, spaces: ProjectSpace[]): Promise<void> {
+    // 迁移交付执行数据到对应空间
+    if (project.data?.deliveryProcesses) {
+      for (const process of project.data.deliveryProcesses) {
+        for (const space of spaces) {
+          // 将交付数据关联到对应空间
+          await this.createSpaceDeliveryData(space.id, process);
+        }
+      }
+    }
+  }
+
+  private async migrateRequirementData(project: any, spaces: ProjectSpace[]): Promise<void> {
+    // 迁移需求数据到对应空间
+    if (project.data?.requirements) {
+      for (const space of spaces) {
+        await this.createSpaceRequirementData(space.id, project.data.requirements);
+      }
+    }
+  }
+}
+```
+
+### 5.3 向后兼容性保证
+
+#### 5.3.1 数据访问层适配
+```typescript
+class ProjectDataAdapter {
+  // 适配旧的单空间项目数据格式
+  adaptLegacyProject(legacyProject: any): Project {
+    const adaptedProject: Project = {
+      ...legacyProject,
+      spaceType: 'single',
+      spaces: this.createDefaultSpaceFromLegacy(legacyProject),
+      spaceProgress: this.createDefaultProgressFromLegacy(legacyProject),
+      spaceAssignment: this.createDefaultAssignmentFromLegacy(legacyProject)
+    };
+
+    return adaptedProject;
+  }
+
+  private createDefaultSpaceFromLegacy(legacyProject: any): ProjectSpace[] {
+    return [{
+      id: `default_space_${legacyProject.objectId}`,
+      projectId: legacyProject.objectId,
+      name: '主空间',
+      type: SpaceType.LIVING_ROOM,
+      priority: 5,
+      status: this.legacyStatusToSpaceStatus(legacyProject.status),
+      metadata: {
+        legacyData: legacyProject.data
+      },
+      createdAt: legacyProject.createdAt,
+      updatedAt: legacyProject.updatedAt
+    }];
+  }
+
+  // 适配新的多空间数据格式到旧格式(用于兼容性接口)
+  adaptToLegacyFormat(multiSpaceProject: Project): any {
+    if (multiSpaceProject.spaceType === 'single') {
+      return {
+        ...multiSpaceProject,
+        // 将单空间数据扁平化到原有格式
+        data: {
+          ...multiSpaceProject.data,
+          deliveryProcesses: this.extractDeliveryProcessesFromSpaces(multiSpaceProject),
+          requirements: this.extractRequirementsFromSpaces(multiSpaceProject)
+        }
+      };
+    }
+
+    // 多空间项目返回增强格式的数据
+    return multiSpaceProject;
+  }
+}
+```
+
+## 6. 实施建议
+
+### 6.1 分阶段实施计划
+
+#### 6.1.1 第一阶段:基础架构(2周)
+- [ ] 数据库结构设计和迁移
+- [ ] 核心数据模型实现
+- [ ] 空间管理基础服务
+- [ ] 向后兼容性保证
+
+#### 6.1.2 第二阶段:界面适配(3周)
+- [ ] 空间概览界面开发
+- [ ] 空间详情弹窗实现
+- [ ] 多空间文件管理界面
+- [ ] 响应式布局适配
+
+#### 6.1.3 第三阶段:业务逻辑(3周)
+- [ ] 多空间报价计算
+- [ ] 空间依赖管理
+- [ ] 批量操作支持
+- [ ] 跨空间协调机制
+
+#### 6.1.4 第四阶段:测试优化(2周)
+- [ ] 单元测试和集成测试
+- [ ] 性能优化
+- [ ] 用户体验优化
+- [ ] 文档完善
+
+### 6.2 风险控制
+
+#### 6.2.1 数据一致性风险
+- **风险**:数据迁移过程中可能出现数据丢失或不一致
+- **应对**:完整的数据备份、分步迁移、数据校验机制
+
+#### 6.2.2 性能风险
+- **风险**:多空间查询可能影响系统性能
+- **应对**:合理的数据库索引、查询优化、缓存策略
+
+#### 6.2.3 用户体验风险
+- **风险**:界面复杂度增加可能影响用户体验
+- **应对**:渐进式界面更新、用户培训、智能默认配置
+
+### 6.3 成功指标
+
+#### 6.3.1 功能指标
+- [ ] 支持单空间到10+空间的项目管理
+- [ ] 空间间依赖关系自动识别率 > 80%
+- [ ] 批量操作成功率 > 95%
+- [ ] 数据迁移零丢失
+
+#### 6.3.2 性能指标
+- [ ] 空间切换响应时间 < 200ms
+- [ ] 多空间项目加载时间 < 1s
+- [ ] 并发用户支持数 > 100
+
+#### 6.3.3 用户体验指标
+- [ ] 用户满意度 > 4.5/5
+- [ ] 操作流程简化度提升 > 30%
+- [ ] 错误率降低 > 50%
+
+---
+
+**文档版本**:v3.0 (Product表统一空间管理)
+**更新日期**:2025-10-20
+**维护者**:YSS Development Team

+ 596 - 8
docs/prd/项目-订单分配.md

@@ -6,19 +6,607 @@
 订单分配阶段是项目管理流程的第一个环节,主要负责将客户咨询转化为正式项目订单,并完成设计师团队的初步分配。该阶段是连接客服端和设计师端的关键桥梁。
 
 ### 1.2 核心目标
-- 完成客户信息的结构化录入和同步
-- 确定项目报价和付款条件
-- 匹配并分配合适的设计师资源
-- 建立项目基础档案,为后续环节提供数据支撑
+- **多空间产品识别与管理**:智能识别单空间/多空间项目,基于Product表管理
+- **完成客户信息的结构化录入和同步**
+- **确定项目报价和付款条件(支持多产品设计报价策略)**
+- **匹配并分配合适的设计师资源(考虑多产品协调需求)**
+- **建立项目基础档案,为后续环节提供数据支撑**
 
 ### 1.3 涉及角色
 - **客服人员**:负责创建订单、录入客户信息、初步需求沟通
-- **设计师**:接收订单分配、查看项目基础信息
-- **组长**:查看团队订单分配情况、协调设计师资源
+- **设计师**:接收产品分配、查看项目基础信息
+- **组长**:查看团队产品分配情况、协调设计师资源
 
-## 2. 核心功能模块
+## 2. 基于Product表的空间产品管理
 
-### 2.1 客户信息管理
+### 2.1 空间产品识别与分类
+
+#### 2.1.1 智能空间产品识别系统
+```typescript
+class ProductIdentificationService {
+  // 基于客户需求描述自动识别空间设计产品
+  async identifyProductsFromDescription(description: string): Promise<ProductIdentificationResult> {
+    const result: ProductIdentificationResult = {
+      identifiedProducts: [],
+      confidence: 0,
+      reasoning: '',
+      suggestedQuestions: []
+    };
+
+    // 产品类型关键词映射
+    const productKeywords = {
+      [ProductType.LIVING_ROOM]: ['客厅', '起居室', '会客厅', '茶室', '待客区'],
+      [ProductType.BEDROOM]: ['卧室', '主卧', '次卧', '儿童房', '老人房', '客房', '主人房'],
+      [ProductType.KITCHEN]: ['厨房', '开放式厨房', '中西厨', '餐厨一体'],
+      [ProductType.BATHROOM]: ['卫生间', '浴室', '洗手间', '盥洗室', '主卫', '次卫'],
+      [ProductType.DINING_ROOM]: ['餐厅', '餐厅区', '用餐区', '就餐空间'],
+      [ProductType.STUDY]: ['书房', '工作室', '办公室', '学习区', '阅读区'],
+      [ProductType.BALCONY]: ['阳台', '露台', '花园阳台', '休闲阳台'],
+      [ProductType.CORRIDOR]: ['走廊', '过道', '玄关', '门厅', '入户'],
+      [ProductType.STORAGE]: ['储物间', '衣帽间', '杂物间', '收纳空间']
+    };
+
+    // 分析描述中的空间关键词
+    const foundProducts: Array<{ type: ProductType; keywords: string[]; confidence: number }> = [];
+
+    for (const [productType, keywords] of Object.entries(productKeywords)) {
+      const matchedKeywords = keywords.filter(keyword =>
+        description.toLowerCase().includes(keyword.toLowerCase())
+      );
+
+      if (matchedKeywords.length > 0) {
+        foundProducts.push({
+          type: productType as ProductType,
+          keywords: matchedKeywords,
+          confidence: matchedKeywords.length / keywords.length
+        });
+      }
+    }
+
+    // 按置信度排序
+    foundProducts.sort((a, b) => b.confidence - a.confidence);
+
+    // 构建识别结果
+    result.identifiedProducts = foundProducts.map(fp => ({
+      type: fp.type,
+      productName: this.getDefaultProductName(fp.type),
+      priority: this.calculateProductPriority(fp.type, fp.confidence),
+      confidence: fp.confidence,
+      identifiedKeywords: fp.keywords
+    }));
+
+    // 计算整体置信度
+    result.confidence = foundProducts.length > 0
+      ? foundProducts.reduce((sum, fp) => sum + fp.confidence, 0) / foundProducts.length
+      : 0;
+
+    // 生成推理说明
+    result.reasoning = this.generateIdentificationReasoning(foundProducts);
+
+    // 生成建议问题
+    result.suggestedQuestions = this.generateClarifyingQuestions(foundProducts);
+
+    return result;
+  }
+
+  // 基于面积和预算推断产品设计数量
+  estimateProductCount(totalArea: number, budget: number): ProductEstimationResult {
+    const result: ProductEstimationResult = {
+      estimatedProductCount: 1,
+      confidence: 0.5,
+      reasoning: '',
+      possibleProductTypes: []
+    };
+
+    // 基于面积的产品数量估算
+    const areaBasedCount = Math.max(1, Math.floor(totalArea / 20)); // 每20平米一个主要空间
+
+    // 基于预算的产品数量估算
+    const budgetBasedCount = Math.max(1, Math.floor(budget / 30000)); // 每3万一个空间
+
+    // 综合判断
+    const finalCount = Math.min(areaBasedCount, budgetBasedCount);
+    result.estimatedProductCount = finalCount;
+
+    // 推断可能的空间类型
+    result.possibleProductTypes = this.inferPossibleProductTypes(totalArea, budget);
+
+    // 生成推理
+    result.reasoning = `基于面积${totalArea}平米和预算${budget}元,估算需要${finalCount}个主要空间设计产品`;
+
+    return result;
+  }
+
+  // 生成产品设计配置建议
+  generateProductConfiguration(
+    identifiedProducts: IdentifiedProduct[],
+    totalArea: number,
+    budget: number
+  ): ProductConfiguration {
+    const configuration: ProductConfiguration = {
+      products: [],
+      totalEstimatedBudget: 0,
+      budgetAllocation: {},
+      recommendations: []
+    };
+
+    // 为识别出的产品创建配置
+    for (const product of identifiedProducts) {
+      const productConfig = this.createProductConfiguration(product, totalArea, budget);
+      configuration.products.push(productConfig);
+      configuration.budgetAllocation[product.type] = productConfig.estimatedBudget;
+    }
+
+    // 如果没有识别出产品,创建默认配置
+    if (configuration.products.length === 0) {
+      const defaultProduct = this.createDefaultProductConfiguration(totalArea, budget);
+      configuration.products.push(defaultProduct);
+      configuration.budgetAllocation[defaultProduct.type] = defaultProduct.estimatedBudget;
+    }
+
+    // 计算总预算
+    configuration.totalEstimatedBudget = Object.values(configuration.budgetAllocation)
+      .reduce((sum, budget) => sum + budget, 0);
+
+    // 生成建议
+    configuration.recommendations = this.generateConfigurationRecommendations(configuration);
+
+    return configuration;
+  }
+}
+
+interface ProductIdentificationResult {
+  identifiedProducts: IdentifiedProduct[];
+  confidence: number;
+  reasoning: string;
+  suggestedQuestions: string[];
+}
+
+interface IdentifiedProduct {
+  type: ProductType;
+  productName: string;
+  priority: number;
+  confidence: number;
+  identifiedKeywords: string[];
+}
+
+interface ProductEstimationResult {
+  estimatedProductCount: number;
+  confidence: number;
+  reasoning: string;
+  possibleProductTypes: ProductType[];
+}
+
+interface ProductConfiguration {
+  products: ProductConfig[];
+  totalEstimatedBudget: number;
+  budgetAllocation: Record<ProductType, number>;
+  recommendations: string[];
+}
+
+interface ProductConfig {
+  type: ProductType;
+  productName: string;
+  estimatedArea: number;
+  estimatedBudget: number;
+  priority: number;
+  complexity: 'simple' | 'medium' | 'complex';
+}
+```
+
+#### 2.1.2 空间产品设计管理界面
+```html
+<!-- 空间产品设计管理面板 -->
+<div class="space-management-panel">
+  <div class="panel-header">
+    <h3>空间配置</h3>
+    <div class="space-type-indicator">
+      <span class="indicator-label">项目类型:</span>
+      <span class="indicator-value"
+            [class.single-space]="isSingleSpaceProject"
+            [class.multi-space]="!isSingleSpaceProject">
+        {{ isSingleSpaceProject ? '单空间项目' : '多空间项目' }}
+      </span>
+    </div>
+  </div>
+
+  <!-- 空间识别结果 -->
+  <div class="space-identification-result" *ngIf="spaceIdentificationResult">
+    <div class="identification-summary">
+      <h4>识别到 {{ spaceIdentificationResult.identifiedSpaces.length }} 个空间</h4>
+      <div class="confidence-indicator">
+        <span class="confidence-label">置信度:</span>
+        <div class="confidence-bar">
+          <div class="confidence-fill"
+               [style.width.%]="spaceIdentificationResult.confidence * 100"
+               [class.high]="spaceIdentificationResult.confidence >= 0.8"
+               [class.medium]="spaceIdentificationResult.confidence >= 0.5 && spaceIdentificationResult.confidence < 0.8"
+               [class.low]="spaceIdentificationResult.confidence < 0.5">
+          </div>
+        </div>
+        <span class="confidence-value">{{ Math.round(spaceIdentificationResult.confidence * 100) }}%</span>
+      </div>
+    </div>
+
+    <div class="identification-reasoning">
+      <p>{{ spaceIdentificationResult.reasoning }}</p>
+    </div>
+
+    <!-- 建议问题 -->
+    <div class="suggested-questions" *ngIf="spaceIdentificationResult.suggestedQuestions.length > 0">
+      <h5>建议确认的问题:</h5>
+      <ul>
+        @for (question of spaceIdentificationResult.suggestedQuestions; track question) {
+          <li>{{ question }}</li>
+        }
+      </ul>
+    </div>
+  </div>
+
+  <!-- 空间列表 -->
+  <div class="spaces-list">
+    <div class="list-header">
+      <h4>空间列表</h4>
+      <button class="btn-add-space" (click)="showAddSpaceDialog()">
+        <i class="icon-plus"></i> 添加空间
+      </button>
+    </div>
+
+    <div class="spaces-grid">
+      @for (space of projectSpaces; track space.id) {
+        <div class="space-card"
+             [class.high-priority]="space.priority >= 8"
+             [class.medium-priority]="space.priority >= 5 && space.priority < 8"
+             [class.low-priority]="space.priority < 5">
+          <div class="space-header">
+            <div class="space-icon">
+              <i class="icon-{{ getSpaceIcon(space.type) }}"></i>
+            </div>
+            <div class="space-info">
+              <input type="text"
+                     [(ngModel)]="space.name"
+                     class="space-name-input"
+                     placeholder="空间名称">
+              <select [(ngModel)]="space.type" class="space-type-select">
+                <option value="{{ SpaceType.LIVING_ROOM }}">客厅</option>
+                <option value="{{ SpaceType.BEDROOM }}">卧室</option>
+                <option value="{{ SpaceType.KITCHEN }}">厨房</option>
+                <option value="{{ SpaceType.BATHROOM }}">卫生间</option>
+                <option value="{{ SpaceType.DINING_ROOM }}">餐厅</option>
+                <option value="{{ SpaceType.STUDY }}">书房</option>
+                <option value="{{ SpaceType.BALCONY }}">阳台</option>
+                <option value="{{ SpaceType.CORRIDOR }}">走廊</option>
+                <option value="{{ SpaceType.STORAGE }}">储物间</option>
+                <option value="{{ SpaceType.OTHER }}">其他</option>
+              </select>
+            </div>
+            <div class="space-actions">
+              <button class="btn-icon" (click)="editSpace(space.id)" title="编辑">
+                <i class="icon-edit"></i>
+              </button>
+              <button class="btn-icon danger" (click)="removeSpace(space.id)" title="删除">
+                <i class="icon-delete"></i>
+              </button>
+            </div>
+          </div>
+
+          <div class="space-details">
+            <div class="detail-row">
+              <label>面积:</label>
+              <input type="number"
+                     [(ngModel)]="space.area"
+                     min="1"
+                     class="detail-input">
+              <span class="unit">m²</span>
+            </div>
+
+            <div class="detail-row">
+              <label>优先级:</label>
+              <select [(ngModel)]="space.priority" class="priority-select">
+                <option [ngValue]="10">最高</option>
+                <option [ngValue]="8">高</option>
+                <option [ngValue]="5">中</option>
+                <option [ngValue]="3">低</option>
+                <option [ngValue]="1">最低</option>
+              </select>
+            </div>
+
+            <div class="detail-row">
+              <label>复杂度:</label>
+              <select [(ngModel)]="space.complexity" class="complexity-select">
+                <option value="simple">简单</option>
+                <option value="medium">中等</option>
+                <option value="complex">复杂</option>
+              </select>
+            </div>
+          </div>
+
+          <div class="space-budget">
+            <label>预估预算:</label>
+            <div class="budget-input-group">
+              <input type="number"
+                     [(ngModel)]="space.estimatedBudget"
+                     min="0"
+                     class="budget-input">
+              <span class="currency">元</span>
+            </div>
+          </div>
+        </div>
+      }
+    </div>
+  </div>
+
+  <!-- 空间统计信息 -->
+  <div class="space-statistics">
+    <div class="stat-item">
+      <span class="stat-label">总空间数:</span>
+      <span class="stat-value">{{ projectSpaces.length }}</span>
+    </div>
+    <div class="stat-item">
+      <span class="stat-label">总面积:</span>
+      <span class="stat-value">{{ totalSpaceArea }}m²</span>
+    </div>
+    <div class="stat-item">
+      <span class="stat-label">总预算:</span>
+      <span class="stat-value">¥{{ totalSpaceBudget.toLocaleString() }}</span>
+    </div>
+  </div>
+</div>
+```
+
+### 2.2 多空间报价管理
+
+#### 2.2.1 空间报价计算
+```typescript
+class MultiSpaceQuotationService {
+  // 计算多空间项目报价
+  calculateMultiSpaceQuotation(
+    spaces: ProjectSpace[],
+    globalOptions: QuotationOptions
+  ): MultiSpaceQuotation {
+
+    const quotation: MultiSpaceQuotation = {
+      projectId: this.getProjectId(),
+      quotationDate: new Date(),
+      currency: 'CNY',
+
+      // 空间报价明细
+      spaceQuotations: [],
+
+      // 全局折扣和优惠
+      globalDiscounts: [],
+
+      // 汇总信息
+      summary: {
+        totalBaseAmount: 0,
+        totalDiscountAmount: 0,
+        finalAmount: 0,
+        averagePricePerSqm: 0
+      }
+    };
+
+    // 计算各空间报价
+    for (const space of spaces) {
+      const spaceQuotation = this.calculateSpaceQuotation(space, globalOptions);
+      quotation.spaceQuotations.push(spaceQuotation);
+    }
+
+    // 计算基础总额
+    quotation.summary.totalBaseAmount = quotation.spaceQuotations
+      .reduce((sum, sq) => sum + sq.totalAmount, 0);
+
+    // 应用多空间折扣
+    quotation.globalDiscounts = this.calculateMultiSpaceDiscounts(
+      spaces,
+      quotation.summary.totalBaseAmount
+    );
+
+    // 计算折扣总额
+    quotation.summary.totalDiscountAmount = quotation.globalDiscounts
+      .reduce((sum, discount) => sum + discount.value, 0);
+
+    // 计算最终金额
+    quotation.summary.finalAmount = quotation.summary.totalBaseAmount - quotation.summary.totalDiscountAmount;
+
+    // 计算平均单价
+    const totalArea = spaces.reduce((sum, space) => sum + (space.area || 0), 0);
+    quotation.summary.averagePricePerSqm = totalArea > 0 ? quotation.summary.finalAmount / totalArea : 0;
+
+    return quotation;
+  }
+
+  // 计算单个空间报价
+  private calculateSpaceQuotation(
+    space: ProjectSpace,
+    options: QuotationOptions
+  ): SpaceQuotation {
+
+    // 基础价格计算
+    const basePrice = this.calculateBasePrice(space, options);
+
+    // 复杂度调整
+    const complexityMultiplier = this.getComplexityMultiplier(space.complexity);
+
+    // 优先级调整
+    const priorityMultiplier = this.getPriorityMultiplier(space.priority);
+
+    // 面积系数
+    const areaCoefficient = this.getAreaCoefficient(space.area || 0);
+
+    // 计算最终价格
+    const finalPrice = basePrice * complexityMultiplier * priorityMultiplier * areaCoefficient;
+
+    const spaceQuotation: SpaceQuotation = {
+      spaceId: space.id,
+      spaceName: space.name,
+      spaceType: space.type,
+      area: space.area || 0,
+
+      // 价格明细
+      priceBreakdown: {
+        basePrice: basePrice,
+        complexityAdjustment: basePrice * (complexityMultiplier - 1),
+        priorityAdjustment: basePrice * (priorityMultiplier - 1),
+        areaAdjustment: basePrice * (areaCoefficient - 1),
+      },
+
+      // 总价
+      totalAmount: finalPrice,
+
+      // 单价
+      unitPrice: space.area ? finalPrice / space.area : 0,
+
+      // 时间预估
+      estimatedDays: this.calculateEstimatedDays(space, finalPrice),
+
+      // 设计师配置
+      designerRequirements: this.getDesignerRequirements(space)
+    };
+
+    return spaceQuotation;
+  }
+
+  // 计算多空间折扣
+  private calculateMultiSpaceDiscounts(
+    spaces: ProjectSpace[],
+    baseAmount: number
+  ): QuotationDiscount[] {
+    const discounts: QuotationDiscount[] = [];
+
+    // 1. 空间数量折扣
+    if (spaces.length >= 5) {
+      discounts.push({
+        type: 'space_count',
+        name: '5空间及以上项目折扣',
+        description: '5个及以上空间项目享受10%折扣',
+        value: baseAmount * 0.1,
+        isApplicable: true
+      });
+    } else if (spaces.length >= 3) {
+      discounts.push({
+        type: 'space_count',
+        name: '3-4空间项目折扣',
+        description: '3-4个空间项目享受5%折扣',
+        value: baseAmount * 0.05,
+        isApplicable: true
+      });
+    }
+
+    // 2. 总额折扣
+    if (baseAmount >= 200000) {
+      discounts.push({
+        type: 'total_amount',
+        name: '高额度项目折扣',
+        description: '项目总额超过20万享受额外5%折扣',
+        value: baseAmount * 0.05,
+        isApplicable: true
+      });
+    }
+
+    // 3. 复杂度折扣(针对全高优先级空间)
+    const allHighPriority = spaces.every(space => space.priority >= 8);
+    if (allHighPriority) {
+      discounts.push({
+        type: 'priority_bonus',
+        name: '高优先级项目折扣',
+        description: '全高优先级空间项目享受3%折扣',
+        value: baseAmount * 0.03,
+        isApplicable: true
+      });
+    }
+
+    return discounts;
+  }
+
+  // 生成报价单
+  async generateQuotationDocument(quotation: MultiSpaceQuotation): Promise<QuotationDocument> {
+    const document: QuotationDocument = {
+      id: `quotation_${Date.now()}`,
+      projectId: quotation.projectId,
+      documentNumber: this.generateQuotationNumber(),
+      issueDate: new Date(),
+      validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天有效期
+      currency: quotation.currency,
+      quotation: quotation,
+
+      // 客户信息
+      customerInfo: await this.getCustomerInfo(),
+
+      // 项目信息
+      projectInfo: await this.getProjectInfo(),
+
+      // 支付条款
+      paymentTerms: this.generatePaymentTerms(quotation),
+
+      // 服务内容
+      serviceScope: this.generateServiceScope(quotation),
+
+      // 注意事项
+      notes: this.generateQuotationNotes(quotation)
+    };
+
+    return document;
+  }
+}
+
+interface MultiSpaceQuotation {
+  projectId: string;
+  quotationDate: Date;
+  currency: string;
+  spaceQuotations: SpaceQuotation[];
+  globalDiscounts: QuotationDiscount[];
+  summary: {
+    totalBaseAmount: number;
+    totalDiscountAmount: number;
+    finalAmount: number;
+    averagePricePerSqm: number;
+  };
+}
+
+interface SpaceQuotation {
+  spaceId: string;
+  spaceName: string;
+  spaceType: SpaceType;
+  area: number;
+  priceBreakdown: {
+    basePrice: number;
+    complexityAdjustment: number;
+    priorityAdjustment: number;
+    areaAdjustment: number;
+  };
+  totalAmount: number;
+  unitPrice: number;
+  estimatedDays: number;
+  designerRequirements: string[];
+}
+
+interface QuotationDiscount {
+  type: 'space_count' | 'total_amount' | 'priority_bonus' | 'special_offer';
+  name: string;
+  description: string;
+  value: number;
+  isApplicable: boolean;
+}
+
+interface QuotationDocument {
+  id: string;
+  projectId: string;
+  documentNumber: string;
+  issueDate: Date;
+  validUntil: Date;
+  currency: string;
+  quotation: MultiSpaceQuotation;
+  customerInfo: any;
+  projectInfo: any;
+  paymentTerms: any;
+  serviceScope: any;
+  notes: string[];
+}
+```
+
+## 3. 核心功能模块
+
+### 3.1 客户信息管理
 
 #### 2.1.1 信息展示卡片
 **位置**:订单分配阶段左侧面板

+ 546 - 1920
docs/prd/项目-需求确认.md

@@ -1,4 +1,4 @@
-# 项目管理 - 需求确认阶段 PRD
+# 项目管理 - 需求确认阶段 PRD (Product表版本)
 
 ## 1. 功能概述
 
@@ -6,2075 +6,701 @@
 需求确认阶段包含"需求沟通"和"方案确认"两个子环节,是连接订单分配与交付执行的关键桥梁。该阶段通过AI辅助分析工具深入理解客户需求,并将抽象需求转化为可执行的设计方案。
 
 ### 1.2 核心目标
+- **多产品设计需求差异化处理**:按产品设计维度独立管理需求,支持跨产品一致性检查
 - **需求沟通环节**:深度挖掘客户的色彩、空间、材质、照明等多维度需求
 - **方案确认环节**:基于需求分析生成初步设计方案,并获得客户确认
-- 建立需求与设计方案之间的映射关系
-- 为后续建模、软装、渲染阶段提供标准化输入
+- **建立需求与设计方案之间的映射关系**
+- **为后续建模、软装、渲染阶段提供标准化输入**
 
 ### 1.3 涉及角色
 - **客服人员**:协助收集客户需求材料、沟通确认需求细节
 - **设计师**:主导需求分析、方案设计、与客户沟通确认
 - **组长**:审核方案可行性、协调资源、把控质量
 
-### 1.4 阶段划分
+### 1.4 多产品设计需求特性
 
-```mermaid
-graph LR
-    A[订单分配] --> B[需求沟通]
-    B --> C[方案确认]
-    C --> D[建模]
-
-    style B fill:#e3f2fd
-    style C fill:#e8f5e9
-```
-
-## 2. 需求沟通环节
-
-### 2.1 需求沟通卡片组件
-
-#### 2.1.1 组件集成
-**组件标签**:
-```html
-<app-requirements-confirm-card
-  #requirementsCard
-  [project]="project"
-  [readonly]="!canEditStage('需求沟通')"
-  (requirementDataUpdated)="onRequirementDataUpdated($event)"
-  (mappingDataUpdated)="onMappingDataUpdated($event)"
-  (uploadModalRequested)="onUploadModalRequested($event)"
-  (stageCompleted)="onRequirementsStageCompleted($event)">
-</app-requirements-confirm-card>
-```
-
-#### 2.1.2 核心功能模块
-
-**四大需求采集流程**:
-1. **色彩氛围需求** → AI色彩分析
-2. **空间结构需求** → AI空间布局分析
-3. **材质权重需求** → AI材质识别分析
-4. **照明需求** → AI光照场景分析
-
-### 2.2 色彩氛围需求采集
-
-#### 2.2.1 数据结构
-```typescript
-interface ColorAtmosphereRequirement {
-  // 用户描述
-  description: string;          // 客户对色彩氛围的文字描述
-  referenceImages: Array<{      // 参考图片
-    id: string;
-    url: string;
-    name: string;
-    uploadTime: Date;
-  }>;
-
-  // AI分析结果
-  colorAnalysisResult?: {
-    originalImage: string;      // 原始参考图URL
-    colors: Array<{
-      hex: string;              // 十六进制颜色值
-      rgb: string;              // RGB值
-      percentage: number;       // 占比 0-100
-      name: string;             // 颜色名称
-    }>;
-    dominantColor: {            // 主色
-      hex: string;
-      rgb: string;
-      name: string;
-    };
-    colorHarmony: {             // 色彩调和
-      type: string;             // 调和类型:monochromatic/analogous/complementary
-      temperature: 'warm' | 'neutral' | 'cool'; // 色温
-      contrast: number;         // 对比度 0-100
-    };
-    mood: string;               // 氛围:温馨/冷静/活力/优雅
-  };
-
-  // 映射到设计指标
-  colorIndicators?: {
-    mainColor: { r: number; g: number; b: number };
-    colorRange: string;         // 色彩范围描述
-    colorTemperature: number;   // 色温值(K)
-  };
-}
-```
-
-#### 2.2.2 参考图上传与分析流程
-
-**上传触发**:
-```typescript
-// 用户点击"上传参考图"按钮
-onUploadReferenceImages(event: Event): void {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-
-  const files = Array.from(input.files);
-
-  // 1. 验证文件类型
-  const validFiles = files.filter(file =>
-    /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)
-  );
-
-  if (validFiles.length === 0) {
-    alert('请上传有效的图片文件(JPG/PNG/GIF/BMP/WEBP)');
-    return;
-  }
-
-  // 2. 显示上传进度
-  this.isUploadingFiles = true;
-  this.uploadProgress = 0;
-
-  // 3. 上传文件到服务器
-  this.uploadFiles(validFiles).subscribe({
-    next: (uploadedFiles) => {
-      // 4. 添加到参考图列表
-      this.referenceImages.push(...uploadedFiles);
-
-      // 5. 触发AI色彩分析
-      this.triggerColorAnalysis(uploadedFiles[0].url);
-    },
-    error: (error) => {
-      console.error('上传失败:', error);
-      alert('图片上传失败,请重试');
-      this.isUploadingFiles = false;
-    }
-  });
-}
-```
-
-**AI色彩分析**:
-```typescript
-// ColorAnalysisService 调用
-triggerColorAnalysis(imageUrl: string): void {
-  this.isAnalyzingColors = true;
-
-  this.colorAnalysisService.analyzeImage(imageUrl).subscribe({
-    next: (result: ColorAnalysisResult) => {
-      // 保存分析结果
-      this.colorAnalysisResult = result;
-
-      // 计算主色
-      const colors = result.colors || [];
-      if (colors.length > 0) {
-        const dominant = colors.reduce((max, cur) =>
-          cur.percentage > max.percentage ? cur : max,
-          colors[0]
-        );
-        this.dominantColorHex = dominant.hex;
-      }
-
-      // 映射到需求指标
-      this.mapColorResultToIndicators(result);
+#### 1.4.1 产品设计需求管理模式
+- **单产品设计项目**:统一需求管理,基于Product表的标准需求数据结构
+- **双产品设计项目**:支持独立产品设计需求,基础的跨产品一致性检查
+- **多产品设计项目**:
+  - 按产品设计独立管理需求(Product.requirements字段)
+  - 跨产品风格一致性要求
+  - 产品间功能协调性分析
+  - 整体预算和时间约束管理
 
-      this.isAnalyzingColors = false;
+#### 1.4.2 多产品设计需求管理流程
+```mermaid
+graph TD
+    A[订单分配完成] --> B{是否多产品设计项目?}
+    B -->|否| C[单产品设计需求管理流程]
+    B -->|是| D[多产品设计需求管理流程]
 
-      // 通知父组件更新
-      this.requirementDataUpdated.emit({
-        colorAnalysisResult: result,
-        colorIndicators: this.colorIndicators
-      });
-    },
-    error: (error) => {
-      console.error('色彩分析失败:', error);
-      alert('AI色彩分析失败,请重试');
-      this.isAnalyzingColors = false;
-    }
-  });
-}
-```
+    D --> E[产品设计需求识别]
+    D --> F[全局需求定义]
+    D --> G[跨产品关系分析]
 
-**色彩结果映射**:
-```typescript
-mapColorResultToIndicators(result: ColorAnalysisResult): void {
-  if (!result.dominantColor) return;
+    E --> H[各产品设计独立需求采集]
+    F --> I[整体风格和预算约束]
+    G --> J[产品间依赖和一致性]
 
-  // 将十六进制颜色转换为RGB
-  const rgb = this.hexToRgb(result.dominantColor.hex);
+    H --> K[需求一致性检查]
+    I --> K
+    J --> K
 
-  this.colorIndicators = {
-    mainColor: { r: rgb.r, g: rgb.g, b: rgb.b },
-    colorRange: this.describeColorRange(result.colors),
-    colorTemperature: this.calculateColorTemperature(rgb)
-  };
-}
+    K --> L{一致性检查通过?}
+    L -->|是| M[生成多产品设计方案]
+    L -->|否| N[需求调整和协调]
+    N --> E
 
-// 计算色温(简化算法)
-calculateColorTemperature(rgb: {r: number; g: number; b: number}): number {
-  // 基于RGB值估算色温
-  // 暖色调:2700K-3500K,中性:4000K-5000K,冷色调:5500K-6500K
-  const warmth = (rgb.r - rgb.b) / 255;
-  if (warmth > 0.3) return 2700 + warmth * 800;  // 暖色调
-  if (warmth < -0.3) return 5500 - warmth * 1000; // 冷色调
-  return 4500; // 中性
-}
-```
+    M --> O[客户确认方案]
+    O --> P[推进到交付执行]
 
-#### 2.2.3 色彩分析可视化
-
-**右侧面板展示**(project-detail.html lines 1826-1900):
-```html
-<div class="analysis-visualization-panel">
-  <h4>色彩分析结果</h4>
-
-  @if (colorAnalysisResult) {
-    <!-- 主色展示 -->
-    <div class="dominant-color-display">
-      <div class="color-swatch"
-           [style.background-color]="dominantColorHex">
-      </div>
-      <div class="color-info">
-        <span class="color-name">{{ colorAnalysisResult.dominantColor.name }}</span>
-        <span class="color-hex">{{ colorAnalysisResult.dominantColor.hex }}</span>
-      </div>
-    </div>
-
-    <!-- 色彩占比饼图 -->
-    <div class="color-distribution">
-      @for (color of colorAnalysisResult.colors; track color.hex) {
-        <div class="color-bar">
-          <div class="color-swatch-small"
-               [style.background-color]="color.hex">
-          </div>
-          <div class="color-bar-fill"
-               [style.width.%]="color.percentage"
-               [style.background-color]="color.hex">
-          </div>
-          <span class="percentage">{{ color.percentage }}%</span>
-        </div>
-      }
-    </div>
-
-    <!-- 色彩调和信息 -->
-    <div class="color-harmony-info">
-      <div class="info-item">
-        <span class="label">调和类型:</span>
-        <span class="value">{{ getColorHarmonyName(colorAnalysisResult.colorHarmony?.type) }}</span>
-      </div>
-      <div class="info-item">
-        <span class="label">色温:</span>
-        <span class="value">{{ getTemperatureName(colorAnalysisResult.colorHarmony?.temperature) }}</span>
-      </div>
-      <div class="info-item">
-        <span class="label">对比度:</span>
-        <span class="value">{{ colorAnalysisResult.colorHarmony?.contrast }}%</span>
-      </div>
-    </div>
-
-    <!-- 色彩氛围标签 -->
-    <div class="mood-tags">
-      <span class="mood-tag">{{ colorAnalysisResult.mood }}</span>
-    </div>
-
-    <!-- 原图预览按钮 -->
-    <button class="btn-secondary" (click)="previewColorRefImage()">
-      查看原图
-    </button>
-  } @else {
-    <div class="empty-state">
-      <p>上传参考图后将显示AI色彩分析结果</p>
-    </div>
-  }
-</div>
+    style C fill:#e8f5e9
+    style D fill:#fff3e0
+    style P fill:#e3f2fd
 ```
 
-**色彩轮盘可视化组件**:
-```html
-<app-color-wheel-visualizer
-  [colors]="colorAnalysisResult?.colors"
-  [dominantColor]="dominantColorHex"
-  [showHarmony]="true">
-</app-color-wheel-visualizer>
-```
+## 2. 基于Product表的产品设计需求管理系统
 
-### 2.3 空间结构需求采集
+### 2.1 产品设计需求数据结构
 
-#### 2.3.1 数据结构
+#### 2.1.1 Product表的需求字段结构
 ```typescript
-interface SpaceStructureRequirement {
-  // CAD文件上传
-  cadFiles: Array<{
-    id: string;
-    name: string;
-    url: string;
-    uploadTime: Date;
-    fileSize: number;
-  }>;
-
-  // 手动输入
-  dimensions?: {
-    length: number;           // 长度(米)
-    width: number;            // 宽度(米)
-    height: number;           // 层高(米)
-    area: number;             // 面积(平方米)
-  };
+interface Product {
+  // 产品基本信息
+  objectId: string;
+  project: Pointer<Project>;
+  profile: Pointer<Profile>;
+  productName: string;           // "李总主卧设计"
+  productType: string;           // "bedroom"
 
-  // AI分析结果
-  spaceAnalysis?: {
+  // 空间信息字段 (Product.space)
+  space: {
+    spaceName: string;            // "主卧"
+    area: number;                 // 18.5
     dimensions: {
       length: number;
       width: number;
       height: number;
-      area: number;
-      volume: number;
     };
-    functionalZones: Array<{
-      zone: string;           // 功能区名称
-      area: number;
-      percentage: number;
-      requirements: string[];
-      furniture: string[];
-    }>;
-    circulation: {
-      mainPaths: string[];    // 主要动线
-      pathWidth: number;      // 动线宽度
-      efficiency: number;     // 动线效率 0-100
-    };
-    layoutType: string;       // 布局类型:open/enclosed/semi-open
+    features: string[];           // ["朝南", "飘窗", "独立卫浴"]
+    constraints: string[];        // ["承重墙不可动"]
+    priority: string;             // "high"
+    complexity: string;           // "medium"
   };
 
-  // 映射到设计指标
-  spaceIndicators?: {
-    lineRatio: number;        // 线条占比 0-1
-    blankRatio: number;       // 留白占比 0-1
-    flowWidth: number;        // 流线宽度
-    aspectRatio: number;      // 空间比例
-    ceilingHeight: number;    // 层高
-  };
-}
-```
-
-#### 2.3.2 CAD文件上传与解析
-
-**文件上传**:
-```typescript
-onCADFilesSelected(event: Event): void {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-
-  const files = Array.from(input.files);
-
-  // 1. 验证文件类型(支持DWG/DXF/PDF等)
-  const validFiles = files.filter(file =>
-    /\.(dwg|dxf|pdf)$/i.test(file.name)
-  );
-
-  if (validFiles.length === 0) {
-    alert('请上传有效的CAD文件(DWG/DXF/PDF)');
-    return;
-  }
-
-  // 2. 上传并解析CAD文件
-  this.uploadAndParseCAD(validFiles).subscribe({
-    next: (parsedData) => {
-      this.cadFiles.push(...parsedData.files);
-      this.spaceAnalysis = parsedData.analysis;
-
-      // 映射到设计指标
-      this.mapSpaceAnalysisToIndicators(parsedData.analysis);
-
-      // 通知父组件
-      this.requirementDataUpdated.emit({
-        spaceAnalysis: this.spaceAnalysis,
-        spaceIndicators: this.spaceIndicators
-      });
-    },
-    error: (error) => {
-      console.error('CAD解析失败:', error);
-      alert('CAD文件解析失败,请检查文件格式');
-    }
-  });
-}
-```
-
-**空间指标映射**:
-```typescript
-mapSpaceAnalysisToIndicators(analysis: SpaceAnalysis): void {
-  if (!analysis) return;
-
-  const { dimensions, functionalZones, circulation } = analysis;
+  // 产品需求字段 (Product.requirements)
+  requirements: {
+    // 色彩需求
+    colorRequirement: {
+      primaryHue: number;
+      saturation: number;
+      temperature: string;        // "暖色调"
+      colorDistribution: Array<{
+        hex: string;
+        percentage: number;
+        name: string;
+      }>;
+    };
 
-  this.spaceIndicators = {
-    // 线条占比:基于功能区划分密度
-    lineRatio: functionalZones.length / 10, // 简化计算
+    // 材质需求
+    materialRequirement: {
+      preferred: string[];         // ["实木", "环保材料"]
+      avoid: string[];            // ["塑料", "合成材料"]
+      budget: {
+        min: number;
+        max: number;
+      };
+    };
 
-    // 留白占比:基于功能区总占比
-    blankRatio: 1 - functionalZones.reduce((sum, zone) =>
-      sum + zone.percentage / 100, 0
-    ),
+    // 照明需求
+    lightingRequirement: {
+      naturalLight: string;        // "充足"
+      lightColor: string;          // "暖白"
+      specialRequirements: string[]; // ["床头阅读灯", "氛围灯"]
+    };
 
-    // 流线宽度
-    flowWidth: circulation.pathWidth,
+    // 具体需求
+    specificRequirements: string[]; // ["需要大储物空间", "独立卫浴"]
 
-    // 空间比例(长宽比)
-    aspectRatio: dimensions.length / dimensions.width,
+    // 参考图片
+    referenceImages: string[];
 
-    // 层高
-    ceilingHeight: dimensions.height
+    // 约束条件
+    constraints: {
+      structural: string[];
+      budget: number;
+      timeline: number;
+    };
   };
-}
-```
-
-#### 2.3.3 空间结构可视化
-
-**空间分区图表**:
-```html
-<div class="space-zones-chart">
-  <h4>功能区分布</h4>
-  @if (spaceAnalysis?.functionalZones) {
-    <div class="zones-grid">
-      @for (zone of spaceAnalysis.functionalZones; track zone.zone) {
-        <div class="zone-card">
-          <div class="zone-header">
-            <span class="zone-name">{{ zone.zone }}</span>
-            <span class="zone-percentage">{{ zone.percentage }}%</span>
-          </div>
-          <div class="zone-area">面积:{{ zone.area }}m²</div>
-          <div class="zone-requirements">
-            <span class="label">需求:</span>
-            <div class="tags">
-              @for (req of zone.requirements; track req) {
-                <span class="tag">{{ req }}</span>
-              }
-            </div>
-          </div>
-          <div class="zone-furniture">
-            <span class="label">家具:</span>
-            <div class="tags">
-              @for (furn of zone.furniture; track furn) {
-                <span class="tag furniture-tag">{{ furn }}</span>
-              }
-            </div>
-          </div>
-        </div>
-      }
-    </div>
-  }
-</div>
-```
-
-**动线效率雷达图**:
-```html
-<div class="circulation-chart">
-  <h4>动线分析</h4>
-  @if (spaceAnalysis?.circulation) {
-    <div class="circulation-info">
-      <div class="info-row">
-        <span class="label">主要动线:</span>
-        <span class="value">{{ spaceAnalysis.circulation.mainPaths.join(' → ') }}</span>
-      </div>
-      <div class="info-row">
-        <span class="label">动线宽度:</span>
-        <span class="value">{{ spaceAnalysis.circulation.pathWidth }}m</span>
-      </div>
-      <div class="info-row">
-        <span class="label">效率评分:</span>
-        <div class="efficiency-bar">
-          <div class="bar-fill"
-               [style.width.%]="spaceAnalysis.circulation.efficiency"
-               [class.excellent]="spaceAnalysis.circulation.efficiency >= 85"
-               [class.good]="spaceAnalysis.circulation.efficiency >= 70 && spaceAnalysis.circulation.efficiency < 85"
-               [class.average]="spaceAnalysis.circulation.efficiency < 70">
-          </div>
-          <span class="score">{{ spaceAnalysis.circulation.efficiency }}分</span>
-        </div>
-      </div>
-    </div>
-  }
-</div>
-```
 
-### 2.4 材质权重需求采集
-
-#### 2.4.1 数据结构
-```typescript
-interface MaterialRequirement {
-  // 参考图片
-  materialImages: Array<{
-    id: string;
-    url: string;
-    name: string;
-  }>;
-
-  // AI材质识别结果
-  materialAnalysis?: Array<{
-    id: string;
-    name: string;             // 材质名称
-    category: string;         // 类别:wood/metal/fabric/leather/plastic/glass/ceramic/stone
-    confidence: number;       // 识别置信度 0-1
-    properties: {
-      texture: string;        // 纹理:smooth/rough/woven/carved
-      color: string;
-      finish: string;         // 表面处理:matte/glossy/satin
-      hardness: number;       // 硬度 0-10
+  // 产品报价字段 (Product.quotation)
+  quotation: {
+    price: number;
+    currency: string;             // "CNY"
+    breakdown: {
+      design: number;
+      modeling: number;
+      rendering: number;
+      softDecor: number;
     };
-    usage: {
-      suitableAreas: string[]; // 适用区域
-      priority: 'primary' | 'secondary' | 'accent';
-    };
-  }>;
-
-  // 映射到设计指标
-  materialIndicators?: {
-    fabricRatio: number;      // 布艺占比 0-100
-    woodRatio: number;        // 木质占比 0-100
-    metalRatio: number;       // 金属占比 0-100
-    smoothness: number;       // 平滑度 0-10
-    glossiness: number;       // 光泽度 0-10
+    status: string;               // "pending" | "approved"
+    validUntil: Date;
   };
+
+  // 产品状态
+  status: 'not_started' | 'in_progress' | 'awaiting_review' | 'completed';
 }
 ```
 
-#### 2.4.2 材质识别流程
+### 2.2 需求管理系统功能
 
-**图片上传触发识别**:
+#### 2.2.1 产品需求采集服务
 ```typescript
-onMaterialImagesSelected(event: Event): void {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
+class ProductRequirementService {
+  // 创建产品设计需求
+  async createProductRequirement(
+    projectId: string,
+    designerId: string,
+    requirementData: ProductRequirementData
+  ): Promise<Product> {
 
-  const files = Array.from(input.files);
+    const product = new Parse.Object("Product");
+    product.set("project", { __type: "Pointer", className: "Project", objectId: projectId });
+    product.set("profile", { __type: "Pointer", className: "Profile", objectId: designerId });
+    product.set("productName", requirementData.productName);
+    product.set("productType", requirementData.productType);
 
-  this.isAnalyzingMaterials = true;
+    // 设置空间信息
+    product.set("space", requirementData.space);
 
-  // 1. 上传图片
-  this.uploadFiles(files).subscribe({
-    next: (uploadedFiles) => {
-      this.materialImages.push(...uploadedFiles);
-
-      // 2. 触发AI材质识别
-      this.analyzeMaterials(uploadedFiles.map(f => f.url));
-    }
-  });
-}
+    // 设置需求信息
+    product.set("requirements", requirementData.requirements);
 
-analyzeMaterials(imageUrls: string[]): void {
-  // 调用材质识别服务(可以是本地模型或云端API)
-  this.materialAnalysisService.analyzeImages(imageUrls).subscribe({
-    next: (results) => {
-      this.materialAnalysisData = results;
+    // 设置初始报价
+    product.set("quotation", this.generateInitialQuotation(requirementData));
 
-      // 计算材质权重
-      this.calculateMaterialWeights(results);
+    await product.save();
+    return product;
+  }
 
-      this.isAnalyzingMaterials = false;
+  // 需求一致性检查
+  async checkRequirementConsistency(
+    products: Product[]
+  ): Promise<ConsistencyCheckResult> {
+    const result: ConsistencyCheckResult = {
+      isConsistent: true,
+      conflicts: [],
+      recommendations: []
+    };
 
-      // 通知父组件
-      this.requirementDataUpdated.emit({
-        materialAnalysisData: results,
-        materialIndicators: this.materialIndicators
-      });
-    },
-    error: (error) => {
-      console.error('材质识别失败:', error);
-      this.isAnalyzingMaterials = false;
+    // 检查跨产品风格一致性
+    const styleConflicts = this.checkStyleConsistency(products);
+    if (styleConflicts.length > 0) {
+      result.isConsistent = false;
+      result.conflicts.push(...styleConflicts);
     }
-  });
-}
-```
-
-**材质权重计算**:
-```typescript
-calculateMaterialWeights(materials: MaterialAnalysis[]): void {
-  if (!materials || materials.length === 0) return;
-
-  // 按类别分组统计
-  const categoryCount: Record<string, number> = {};
-  const categoryConfidence: Record<string, number> = {};
-
-  materials.forEach(mat => {
-    categoryCount[mat.category] = (categoryCount[mat.category] || 0) + 1;
-    categoryConfidence[mat.category] =
-      (categoryConfidence[mat.category] || 0) + mat.confidence;
-  });
-
-  const total = materials.length;
-
-  // 计算加权占比
-  this.materialIndicators = {
-    fabricRatio: Math.round(
-      (categoryCount['fabric'] || 0) / total *
-      (categoryConfidence['fabric'] || 0) / (categoryCount['fabric'] || 1) *
-      100
-    ),
-    woodRatio: Math.round(
-      (categoryCount['wood'] || 0) / total *
-      (categoryConfidence['wood'] || 0) / (categoryCount['wood'] || 1) *
-      100
-    ),
-    metalRatio: Math.round(
-      (categoryCount['metal'] || 0) / total *
-      (categoryConfidence['metal'] || 0) / (categoryCount['metal'] || 1) *
-      100
-    ),
-    // 根据材质属性计算平滑度和光泽度
-    smoothness: this.calculateAverageSmoothness(materials),
-    glossiness: this.calculateAverageGlossiness(materials)
-  };
-}
-
-calculateAverageSmoothness(materials: MaterialAnalysis[]): number {
-  const textureScores: Record<string, number> = {
-    'smooth': 10,
-    'satin': 7,
-    'rough': 3,
-    'woven': 5,
-    'carved': 2
-  };
-
-  const scores = materials
-    .map(m => textureScores[m.properties.texture] || 5)
-    .filter(s => s > 0);
-
-  return scores.length > 0
-    ? Math.round(scores.reduce((sum, s) => sum + s, 0) / scores.length)
-    : 5;
-}
-```
-
-#### 2.4.3 材质分析可视化
-
-**材质卡片网格**:
-```html
-<div class="material-analysis-grid">
-  <h4>识别的材质</h4>
-  @if (materialAnalysisData && materialAnalysisData.length > 0) {
-    <div class="material-cards">
-      @for (material of materialAnalysisData; track material.id) {
-        <div class="material-card">
-          <div class="material-header">
-            <span class="material-name">{{ material.name }}</span>
-            <span class="confidence-badge"
-                  [class.high]="material.confidence >= 0.8"
-                  [class.medium]="material.confidence >= 0.6 && material.confidence < 0.8"
-                  [class.low]="material.confidence < 0.6">
-              {{ (material.confidence * 100).toFixed(0) }}%
-            </span>
-          </div>
-
-          <div class="material-category">
-            {{ getMaterialName(material.category) }}
-          </div>
 
-          <div class="material-properties">
-            <div class="property">
-              <span class="prop-label">纹理:</span>
-              <span class="prop-value">{{ material.properties.texture }}</span>
-            </div>
-            <div class="property">
-              <span class="prop-label">表面:</span>
-              <span class="prop-value">{{ material.properties.finish }}</span>
-            </div>
-            <div class="property">
-              <span class="prop-label">硬度:</span>
-              <div class="hardness-bar">
-                <div class="bar-fill"
-                     [style.width.%]="material.properties.hardness * 10">
-                </div>
-              </div>
-            </div>
-          </div>
+    // 检查预算约束
+    const budgetConflicts = this.checkBudgetConstraints(products);
+    if (budgetConflicts.length > 0) {
+      result.isConsistent = false;
+      result.conflicts.push(...budgetConflicts);
+    }
 
-          <div class="material-usage">
-            <span class="usage-label">适用区域:</span>
-            <div class="area-tags">
-              @for (area of material.usage.suitableAreas; track area) {
-                <span class="area-tag">{{ area }}</span>
-              }
-            </div>
-          </div>
-        </div>
-      }
-    </div>
+    // 检查时间约束
+    const timelineConflicts = this.checkTimelineConstraints(products);
+    if (timelineConflicts.length > 0) {
+      result.isConsistent = false;
+      result.conflicts.push(...timelineConflicts);
+    }
 
-    <!-- 材质占比饼图 -->
-    <div class="material-distribution-chart">
-      <h5>材质分布</h5>
-      <div class="pie-chart">
-        <!-- 使用图表库绘制饼图 -->
-        <canvas #materialPieChart></canvas>
-      </div>
-      <div class="chart-legend">
-        <div class="legend-item">
-          <span class="color-dot" style="background-color: #8B4513;"></span>
-          <span>木质 {{ materialIndicators?.woodRatio }}%</span>
-        </div>
-        <div class="legend-item">
-          <span class="color-dot" style="background-color: #C0C0C0;"></span>
-          <span>金属 {{ materialIndicators?.metalRatio }}%</span>
-        </div>
-        <div class="legend-item">
-          <span class="color-dot" style="background-color: #DEB887;"></span>
-          <span>布艺 {{ materialIndicators?.fabricRatio }}%</span>
-        </div>
-      </div>
-    </div>
-  } @else {
-    <div class="empty-state">
-      上传材质参考图后将显示AI识别结果
-    </div>
+    return result;
   }
-</div>
-```
-
-**纹理对比可视化组件**:
-```html
-<app-texture-comparison-visualizer
-  [materials]="materialAnalysisData"
-  [showProperties]="true">
-</app-texture-comparison-visualizer>
-```
 
-### 2.5 照明需求采集
+  // AI需求分析
+  async analyzeRequirement(
+    customerDescription: string,
+    referenceImages: string[]
+  ): Promise<RequirementAnalysisResult> {
+    // 调用AI服务分析客户描述
+    const aiAnalysis = await this.aiService.analyzeDesignRequirement({
+      description: customerDescription,
+      images: referenceImages,
+      context: "home_design"
+    });
 
-#### 2.5.1 数据结构
-```typescript
-interface LightingRequirement {
-  // 照明场景图片
-  lightingImages: Array<{
-    id: string;
-    url: string;
-    name: string;
-  }>;
-
-  // AI光照分析结果
-  lightingAnalysis?: {
-    naturalLight: {
-      direction: string[];    // 采光方向:north/south/east/west
-      intensity: string;      // 光照强度:strong/moderate/weak
-      duration: string;       // 日照时长
-      quality: number;        // 光照质量 0-100
+    return {
+      colorPreference: aiAnalysis.colorPreference,
+      materialPreference: aiAnalysis.materialPreference,
+      stylePreference: aiAnalysis.stylePreference,
+      spaceRequirements: aiAnalysis.spaceRequirements,
+      confidence: aiAnalysis.confidence,
+      suggestedQuestions: aiAnalysis.suggestedQuestions
     };
-    artificialLight: {
-      mainLighting: {
-        type: string;         // 主照明类型:ceiling/chandelier/downlight
-        distribution: string; // 分布方式:uniform/concentrated/layered
-        brightness: number;   // 亮度 0-100
-      };
-      accentLighting: {
-        type: string;         // 重点照明类型:spotlight/wallwash/uplighting
-        locations: string[];
-        intensity: number;
-      };
-      ambientLighting: {
-        type: string;         // 环境照明类型:cove/indirect/decorative
-        mood: string;         // 氛围:warm/cool/neutral
-        colorTemperature: number; // 色温(K)
-      };
-    };
-    lightingMood: string;     // 整体照明氛围:dramatic/romantic/energetic/calm
-  };
-
-  // 映射到设计指标
-  lightingIndicators?: {
-    naturalLightRatio: number;  // 自然光占比 0-1
-    artificialLightRatio: number; // 人工光占比 0-1
-    mainLightIntensity: number;   // 主光强度 0-100
-    accentLightIntensity: number; // 辅助光强度 0-100
-    ambientColorTemp: number;     // 环境色温(K)
-  };
+  }
 }
 ```
 
-#### 2.5.2 光照分析流程
-
-**图片上传触发分析**:
+#### 2.2.2 需求管理界面组件
 ```typescript
-onLightingImagesSelected(event: Event): void {
-  const input = event.target as HTMLInputElement;
-  if (!input.files || input.files.length === 0) return;
-
-  const files = Array.from(input.files);
-
-  this.isAnalyzingLighting = true;
-
-  // 1. 上传图片
-  this.uploadFiles(files).subscribe({
-    next: (uploadedFiles) => {
-      this.lightingImages.push(...uploadedFiles);
-
-      // 2. 触发AI光照分析
-      this.analyzeLighting(uploadedFiles.map(f => f.url));
-    }
-  });
-}
+// React 组件示例
+const ProductRequirementPanel = ({ projectId, onRequirementUpdate }) => {
+  const [products, setProducts] = useState<Product[]>([]);
+  const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
+  const [consistencyResult, setConsistencyResult] = useState<ConsistencyCheckResult | null>(null);
 
-analyzeLighting(imageUrls: string[]): void {
-  this.lightingAnalysisService.analyzeImages(imageUrls).subscribe({
-    next: (result) => {
-      this.lightingAnalysis = result;
+  // 加载项目的产品列表
+  useEffect(() => {
+    loadProjectProducts();
+  }, [projectId]);
 
-      // 映射到设计指标
-      this.mapLightingToIndicators(result);
-
-      this.isAnalyzingLighting = false;
+  const loadProjectProducts = async () => {
+    const query = new Parse.Query("Product");
+    query.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
+    query.include("profile");
+    const results = await query.find();
+    setProducts(results);
+  };
 
-      // 通知父组件
-      this.requirementDataUpdated.emit({
-        lightingAnalysis: result,
-        lightingIndicators: this.lightingIndicators
-      });
-    },
-    error: (error) => {
-      console.error('光照分析失败:', error);
-      this.isAnalyzingLighting = false;
+  // 需求一致性检查
+  const checkConsistency = async () => {
+    if (products.length > 1) {
+      const result = await requirementService.checkRequirementConsistency(products);
+      setConsistencyResult(result);
     }
-  });
-}
-```
-
-**光照指标映射**:
-```typescript
-mapLightingToIndicators(analysis: LightingAnalysis): void {
-  if (!analysis) return;
-
-  // 根据自然光质量和人工光配置计算占比
-  const naturalQuality = analysis.naturalLight.quality || 50;
-  const artificialBrightness = analysis.artificialLight.mainLighting.brightness || 50;
-
-  const totalLight = naturalQuality + artificialBrightness;
-
-  this.lightingIndicators = {
-    naturalLightRatio: naturalQuality / totalLight,
-    artificialLightRatio: artificialBrightness / totalLight,
-    mainLightIntensity: analysis.artificialLight.mainLighting.brightness,
-    accentLightIntensity: analysis.artificialLight.accentLighting.intensity,
-    ambientColorTemp: analysis.artificialLight.ambientLighting.colorTemperature
   };
-}
-```
 
-#### 2.5.3 光照分析可视化
-
-**光照信息面板**:
-```html
-<div class="lighting-analysis-panel">
-  <h4>光照分析</h4>
-
-  @if (lightingAnalysis) {
-    <!-- 自然光信息 -->
-    <div class="natural-light-section">
-      <h5>自然光</h5>
-      <div class="light-info-grid">
-        <div class="info-card">
-          <span class="label">采光方向</span>
-          <div class="direction-icons">
-            @for (dir of lightingAnalysis.naturalLight.direction; track dir) {
-              <span class="direction-icon">{{ dir }}</span>
-            }
-          </div>
-        </div>
-        <div class="info-card">
-          <span class="label">光照强度</span>
-          <span class="value intensity-{{ lightingAnalysis.naturalLight.intensity }}">
-            {{ lightingAnalysis.naturalLight.intensity }}
-          </span>
-        </div>
-        <div class="info-card">
-          <span class="label">日照时长</span>
-          <span class="value">{{ lightingAnalysis.naturalLight.duration }}</span>
-        </div>
-        <div class="info-card">
-          <span class="label">光照质量</span>
-          <div class="quality-bar">
-            <div class="bar-fill"
-                 [style.width.%]="lightingAnalysis.naturalLight.quality">
-            </div>
-            <span class="score">{{ lightingAnalysis.naturalLight.quality }}分</span>
-          </div>
-        </div>
+  return (
+    <div className="product-requirement-panel">
+      {/* 产品列表 */}
+      <div className="product-list">
+        <h3>空间设计产品</h3>
+        {products.map(product => (
+          <ProductRequirementCard
+            key={product.id}
+            product={product}
+            onSelect={setSelectedProduct}
+            onUpdate={loadProjectProducts}
+          />
+        ))}
       </div>
-    </div>
-
-    <!-- 人工光信息 -->
-    <div class="artificial-light-section">
-      <h5>人工光</h5>
 
-      <!-- 主照明 -->
-      <div class="light-type-card">
-        <h6>主照明</h6>
-        <div class="type-info">
-          <span class="label">类型:</span>
-          <span class="value">{{ lightingAnalysis.artificialLight.mainLighting.type }}</span>
-        </div>
-        <div class="type-info">
-          <span class="label">分布:</span>
-          <span class="value">{{ lightingAnalysis.artificialLight.mainLighting.distribution }}</span>
-        </div>
-        <div class="type-info">
-          <span class="label">亮度:</span>
-          <div class="brightness-bar">
-            <div class="bar-fill"
-                 [style.width.%]="lightingAnalysis.artificialLight.mainLighting.brightness">
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 重点照明 -->
-      <div class="light-type-card">
-        <h6>重点照明</h6>
-        <div class="type-info">
-          <span class="label">类型:</span>
-          <span class="value">{{ lightingAnalysis.artificialLight.accentLighting.type }}</span>
-        </div>
-        <div class="type-info">
-          <span class="label">位置:</span>
-          <div class="location-tags">
-            @for (loc of lightingAnalysis.artificialLight.accentLighting.locations; track loc) {
-              <span class="location-tag">{{ loc }}</span>
-            }
-          </div>
-        </div>
-      </div>
-
-      <!-- 环境照明 -->
-      <div class="light-type-card">
-        <h6>环境照明</h6>
-        <div class="type-info">
-          <span class="label">氛围:</span>
-          <span class="value mood-{{ lightingAnalysis.artificialLight.ambientLighting.mood }}">
-            {{ lightingAnalysis.artificialLight.ambientLighting.mood }}
-          </span>
-        </div>
-        <div class="type-info">
-          <span class="label">色温:</span>
-          <span class="value">{{ lightingAnalysis.artificialLight.ambientLighting.colorTemperature }}K</span>
-        </div>
-      </div>
+      {/* 需求编辑器 */}
+      {selectedProduct && (
+        <RequirementEditor
+          product={selectedProduct}
+          onSave={async (updatedRequirements) => {
+            selectedProduct.set("requirements", updatedRequirements);
+            await selectedProduct.save();
+            loadProjectProducts();
+            onRequirementUpdate();
+          }}
+        />
+      )}
+
+      {/* 一致性检查结果 */}
+      {consistencyResult && (
+        <ConsistencyCheckResult result={consistencyResult} />
+      )}
     </div>
-
-    <!-- 整体照明氛围 -->
-    <div class="lighting-mood-section">
-      <h5>照明氛围</h5>
-      <span class="mood-badge mood-{{ lightingAnalysis.lightingMood }}">
-        {{ getLightingMoodName(lightingAnalysis.lightingMood) }}
-      </span>
-    </div>
-  } @else {
-    <div class="empty-state">
-      上传照明场景图后将显示AI光照分析
-    </div>
-  }
-</div>
+  );
+};
 ```
 
-### 2.6 需求映射总览
+### 2.3 跨产品一致性管理
 
-#### 2.6.1 需求完成度检查
+#### 2.3.1 一致性检查规则
 ```typescript
-// 检查四大需求是否全部完成
-areAllRequirementsCompleted(): boolean {
-  const hasColorData = !!this.colorAnalysisResult || !!this.colorIndicators;
-  const hasSpaceData = !!this.spaceAnalysis || !!this.spaceIndicators;
-  const hasMaterialData = !!this.materialAnalysisData?.length || !!this.materialIndicators;
-  const hasLightingData = !!this.lightingAnalysis || !!this.lightingIndicators;
-
-  return hasColorData && hasSpaceData && hasMaterialData && hasLightingData;
-}
-```
-
-#### 2.6.2 需求数据汇总
-```typescript
-// 汇总所有需求数据
-getRequirementSummary(): RequirementSummary {
-  return {
-    colorRequirement: {
-      description: this.colorDescription,
-      referenceImages: this.referenceImages,
-      analysisResult: this.colorAnalysisResult,
-      indicators: this.colorIndicators
-    },
-    spaceRequirement: {
-      cadFiles: this.cadFiles,
-      dimensions: this.manualDimensions,
-      analysisResult: this.spaceAnalysis,
-      indicators: this.spaceIndicators
-    },
-    materialRequirement: {
-      materialImages: this.materialImages,
-      analysisResult: this.materialAnalysisData,
-      indicators: this.materialIndicators
-    },
-    lightingRequirement: {
-      lightingImages: this.lightingImages,
-      analysisResult: this.lightingAnalysis,
-      indicators: this.lightingIndicators
-    },
-    completionRate: this.calculateCompletionRate()
-  };
-}
+class ConsistencyRules {
+  // 风格一致性检查
+  checkStyleConsistency(products: Product[]): Conflict[] {
+    const conflicts: Conflict[] = [];
+    const styleMap = new Map<string, Product[]>();
 
-calculateCompletionRate(): number {
-  let completed = 0;
-  const total = 4;
-
-  if (this.colorAnalysisResult) completed++;
-  if (this.spaceAnalysis) completed++;
-  if (this.materialAnalysisData?.length) completed++;
-  if (this.lightingAnalysis) completed++;
+    // 收集所有产品的风格信息
+    products.forEach(product => {
+      const style = this.extractStyleFromRequirements(product.requirements);
+      if (!styleMap.has(style)) {
+        styleMap.set(style, []);
+      }
+      styleMap.get(style)!.push(product);
+    });
 
-  return Math.round((completed / total) * 100);
-}
-```
+    // 检查风格冲突
+    if (styleMap.size > 1) {
+      conflicts.push({
+        type: "style_conflict",
+        description: "发现风格不一致的产品设计",
+        affectedProducts: products,
+        recommendation: "建议统一整体风格或明确各产品的风格定位"
+      });
+    }
 
-#### 2.6.3 需求沟通完成触发
-```typescript
-// 当所有需求完成后触发
-completeRequirementsCommunication(): void {
-  if (!this.areAllRequirementsCompleted()) {
-    alert('请完成所有需求采集项:色彩氛围、空间结构、材质权重、照明需求');
-    return;
+    return conflicts;
   }
 
-  // 1. 保存需求数据
-  const summary = this.getRequirementSummary();
-
-  // 2. 通知父组件推进到方案确认阶段
-  this.stageCompleted.emit({
-    stage: 'requirements-communication',
-    allStagesCompleted: true,
-    data: summary
-  });
-
-  // 3. 显示成功提示
-  alert('需求沟通完成!即将进入方案确认阶段');
-}
-```
-
-## 3. 方案确认环节
-
-### 3.1 方案生成逻辑
+  // 预算约束检查
+  checkBudgetConstraints(products: Product[]): Conflict[] {
+    const conflicts: Conflict[] = [];
+    const totalBudget = products.reduce((sum, product) => {
+      return sum + (product.quotation?.price || 0);
+    }, 0);
+
+    // 获取项目总预算约束
+    const projectBudget = this.getProjectBudget(products[0].project);
+
+    if (totalBudget > projectBudget) {
+      conflicts.push({
+        type: "budget_exceeded",
+        description: `总预算${totalBudget}超过项目预算${projectBudget}`,
+        affectedProducts: products,
+        recommendation: "建议调整产品报价或增加项目预算"
+      });
+    }
 
-#### 3.1.1 AI方案生成触发
-```typescript
-// 基于需求数据生成初步设计方案
-generateDesignProposal(): void {
-  if (!this.areRequiredStagesCompleted()) {
-    alert('请先完成需求沟通的所有采集项');
-    return;
+    return conflicts;
   }
 
-  this.isAnalyzing = true;
-  this.analysisProgress = 0;
+  // 时间约束检查
+  checkTimelineConstraints(products: Product[]): Conflict[] {
+    const conflicts: Conflict[] = [];
+    const totalTimeline = products.reduce((sum, product) => {
+      return sum + (product.requirements?.constraints?.timeline || 0);
+    }, 0);
+
+    // 检查产品间的时间依赖
+    const dependencies = this.analyzeProductDependencies(products);
+    dependencies.forEach(dep => {
+      if (dep.conflict) {
+        conflicts.push({
+          type: "timeline_conflict",
+          description: `产品${dep.fromProduct.productName}与${dep.toProduct.productName}存在时间冲突`,
+          affectedProducts: [dep.fromProduct, dep.toProduct],
+          recommendation: "建议调整产品执行顺序或时间安排"
+        });
+      }
+    });
 
-  // 模拟方案生成进度
-  const progressInterval = setInterval(() => {
-    this.analysisProgress += Math.random() * 15;
-    if (this.analysisProgress >= 100) {
-      this.analysisProgress = 100;
-      clearInterval(progressInterval);
-      this.completeProposalGeneration();
-    }
-  }, 500);
+    return conflicts;
+  }
 }
 ```
 
-#### 3.1.2 方案数据结构
-```typescript
-interface ProposalAnalysis {
-  id: string;
-  name: string;
-  version: string;
-  createdAt: Date;
-  status: 'analyzing' | 'completed' | 'approved' | 'rejected';
-
-  // 材质方案
-  materials: MaterialAnalysis[];
-
-  // 设计风格
-  designStyle: {
-    primaryStyle: string;
-    styleElements: Array<{
-      element: string;
-      description: string;
-      influence: number; // 影响程度 0-100
-    }>;
-    characteristics: Array<{
-      feature: string;
-      value: string;
-      importance: 'high' | 'medium' | 'low';
-    }>;
-    compatibility: {
-      withMaterials: string[];
-      withColors: string[];
-      score: number; // 兼容性评分 0-100
-    };
-  };
-
-  // 色彩方案
-  colorScheme: {
-    palette: Array<{
-      color: string;
-      hex: string;
-      rgb: string;
-      percentage: number;
-      role: 'dominant' | 'secondary' | 'accent' | 'neutral';
-    }>;
-    harmony: {
-      type: string;
-      temperature: 'warm' | 'cool' | 'neutral';
-      contrast: number;
-    };
-    psychology: {
-      mood: string;
-      atmosphere: string;
-      suitability: string[];
-    };
-  };
-
-  // 空间布局
-  spaceLayout: {
-    dimensions: {
-      length: number;
-      width: number;
-      height: number;
-      area: number;
-      volume: number;
-    };
-    functionalZones: Array<{
-      zone: string;
-      area: number;
-      percentage: number;
-      requirements: string[];
-      furniture: string[];
-    }>;
-    circulation: {
-      mainPaths: string[];
-      pathWidth: number;
-      efficiency: number;
-    };
-    lighting: {
-      natural: {
-        direction: string[];
-        intensity: string;
-        duration: string;
-      };
-      artificial: {
-        zones: string[];
-        requirements: string[];
-      };
-    };
-  };
-
-  // 预算方案
-  budget: {
-    total: number;
-    breakdown: Array<{
-      category: string;
-      amount: number;
-      percentage: number;
-    }>;
-  };
-
-  // 时间规划
-  timeline: Array<{
-    phase: string;
-    duration: number;
-    dependencies: string[];
-  }>;
-
-  // 可行性评估
-  feasibility: {
-    technical: number;  // 技术可行性 0-100
-    budget: number;     // 预算可行性 0-100
-    timeline: number;   // 时间可行性 0-100
-    overall: number;    // 综合可行性 0-100
-  };
-}
-```
+### 2.4 需求确认流程
 
-#### 3.1.3 方案生成实现(简化示例)
+#### 2.4.1 方案生成与确认
 ```typescript
-// project-detail.ts lines 3237-3512
-private completeProposalGeneration(): void {
-  this.isAnalyzing = false;
+class DesignProposalService {
+  // 基于需求生成设计方案
+  async generateDesignProposal(
+    products: Product[],
+    customerRequirements: CustomerRequirement
+  ): Promise<DesignProposal> {
+    const proposal: DesignProposal = {
+      id: generateId(),
+      projectId: products[0].project.id,
+      products: [],
+      overallDesign: {},
+      timeline: {},
+      budget: {},
+      status: "draft"
+    };
 
-  // 基于需求指标生成方案
-  this.proposalAnalysis = {
-    id: 'proposal-' + Date.now(),
-    name: '现代简约风格方案',
-    version: 'v1.0',
-    createdAt: new Date(),
-    status: 'completed',
+    // 为每个产品生成设计方案
+    for (const product of products) {
+      const productDesign = await this.generateProductDesign(product, customerRequirements);
+      proposal.products.push(productDesign);
+    }
 
-    // 材质方案:基于materialIndicators
-    materials: this.generateMaterialProposal(),
+    // 生成整体设计方案
+    proposal.overallDesign = await this.generateOverallDesign(products, customerRequirements);
 
-    // 设计风格:基于整体需求
-    designStyle: this.generateStyleProposal(),
+    // 生成时间计划
+    proposal.timeline = this.generateTimeline(proposal.products);
 
-    // 色彩方案:基于colorIndicators
-    colorScheme: this.generateColorSchemeProposal(),
+    // 生成预算方案
+    proposal.budget = this.generateBudget(proposal.products);
 
-    // 空间布局:基于spaceIndicators
-    spaceLayout: this.generateSpaceLayoutProposal(),
+    return proposal;
+  }
 
-    // 预算方案:基于quotationData
-    budget: this.generateBudgetProposal(),
+  // 客户确认方案
+  async confirmProposal(
+    proposalId: string,
+    customerFeedback: CustomerFeedback
+  ): Promise<ProposalConfirmationResult> {
+    const proposal = await this.getProposal(proposalId);
 
-    // 时间规划
-    timeline: this.generateTimelineProposal(),
+    if (customerFeedback.approved) {
+      // 方案确认通过,更新产品状态
+      await this.updateProductStatus(proposal.products, "in_progress");
 
-    // 可行性评估
-    feasibility: this.assessFeasibility()
-  };
+      return {
+        success: true,
+        message: "方案已确认,项目进入执行阶段"
+      };
+    } else {
+      // 方案需要修改
+      await this.handleProposalRevision(proposal, customerFeedback);
 
-  console.log('方案生成完成:', this.proposalAnalysis);
+      return {
+        success: false,
+        message: "方案需要修改,请根据客户反馈调整设计"
+      };
+    }
+  }
 }
 ```
 
-### 3.2 方案展示与确认
+## 3. 需求确认界面设计
 
-#### 3.2.1 方案概览面板
+### 3.1 产品需求管理界面
 ```html
-<div class="proposal-overview-panel">
-  <h3>设计方案概览</h3>
-
-  @if (proposalAnalysis && proposalAnalysis.status === 'completed') {
-    <!-- 方案基本信息 -->
-    <div class="proposal-header">
-      <div class="proposal-name">{{ proposalAnalysis.name }}</div>
-      <div class="proposal-meta">
-        <span class="version">{{ proposalAnalysis.version }}</span>
-        <span class="created-date">{{ formatDate(proposalAnalysis.createdAt) }}</span>
-      </div>
+<!-- 产品需求管理主界面 -->
+<div class="requirement-confirmation-container">
+  <!-- 头部导航 -->
+  <div class="requirement-header">
+    <h2>需求确认 - {{ project.title }}</h2>
+    <div class="progress-indicator">
+      <div class="step active">需求沟通</div>
+      <div class="step">方案确认</div>
+      <div class="step">客户确认</div>
     </div>
+  </div>
 
-    <!-- 可行性评分卡片 -->
-    <div class="feasibility-cards">
-      <div class="feasibility-card">
-        <span class="label">技术可行性</span>
-        <div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.technical)">
-          <span class="score">{{ proposalAnalysis.feasibility.technical }}</span>
-        </div>
-      </div>
-      <div class="feasibility-card">
-        <span class="label">预算可行性</span>
-        <div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.budget)">
-          <span class="score">{{ proposalAnalysis.feasibility.budget }}</span>
+  <!-- 产品列表 -->
+  <div class="product-section">
+    <h3>空间设计产品列表</h3>
+    <div class="product-grid">
+      <div v-for="product in products" :key="product.id"
+           class="product-card"
+           :class="{ active: selectedProduct?.id === product.id }"
+           @click="selectProduct(product)">
+
+        <div class="product-header">
+          <h4>{{ product.productName }}</h4>
+          <span class="product-type">{{ getProductTypeLabel(product.productType) }}</span>
         </div>
-      </div>
-      <div class="feasibility-card">
-        <span class="label">时间可行性</span>
-        <div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.timeline)">
-          <span class="score">{{ proposalAnalysis.feasibility.timeline }}</span>
+
+        <div class="product-info">
+          <div class="space-info">
+            <span class="area">{{ product.space.area }}㎡</span>
+            <span class="complexity">{{ product.space.complexity }}</span>
+          </div>
+
+          <div class="designer-info">
+            <img :src="product.profile.avatar" class="designer-avatar" />
+            <span>{{ product.profile.name }}</span>
+          </div>
         </div>
-      </div>
-      <div class="feasibility-card overall">
-        <span class="label">综合可行性</span>
-        <div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.overall)">
-          <span class="score">{{ proposalAnalysis.feasibility.overall }}</span>
+
+        <div class="requirement-summary">
+          <div class="requirement-item">
+            <label>风格:</label>
+            <span>{{ getStyleLabel(product.requirements.colorRequirement.temperature) }}</span>
+          </div>
+          <div class="requirement-item">
+            <label>材质:</label>
+            <span>{{ product.requirements.materialRequirement.preferred.join(', ') }}</span>
+          </div>
         </div>
-      </div>
-    </div>
 
-    <!-- 方案摘要 -->
-    <div class="proposal-summary">
-      <div class="summary-section">
-        <h4>材质方案</h4>
-        <p>{{ getMaterialCategories() }}</p>
-      </div>
-      <div class="summary-section">
-        <h4>设计风格</h4>
-        <p>{{ getStyleSummary() }}</p>
-      </div>
-      <div class="summary-section">
-        <h4>色彩方案</h4>
-        <p>{{ getColorSummary() }}</p>
-      </div>
-      <div class="summary-section">
-        <h4>空间效率</h4>
-        <p>{{ getSpaceEfficiency() }}%</p>
+        <div class="product-status">
+          <span class="status-badge" :class="product.status">
+            {{ getStatusLabel(product.status) }}
+          </span>
+        </div>
       </div>
     </div>
+  </div>
 
-    <!-- 操作按钮 -->
-    <div class="proposal-actions">
-      <button class="btn-secondary" (click)="viewProposalDetails()">
-        查看详情
-      </button>
-      <button class="btn-primary"
-              (click)="confirmProposal()"
-              [disabled]="!canEditStage('方案确认')">
-        确认方案
-      </button>
-    </div>
-
-  } @else if (isAnalyzing) {
-    <!-- 方案生成中 -->
-    <div class="analyzing-state">
-      <div class="spinner"></div>
-      <p>AI正在分析需求并生成设计方案...</p>
-      <div class="progress-bar">
-        <div class="progress-fill" [style.width.%]="analysisProgress"></div>
+  <!-- 需求编辑器 -->
+  <div class="requirement-editor" v-if="selectedProduct">
+    <h3>产品需求编辑 - {{ selectedProduct.productName }}</h3>
+
+    <div class="requirement-tabs">
+      <div class="tab"
+           v-for="tab in requirementTabs"
+           :key="tab.key"
+           :class="{ active: activeTab === tab.key }"
+           @click="activeTab = tab.key">
+        {{ tab.label }}
       </div>
-      <span class="progress-text">{{ analysisProgress.toFixed(0) }}%</span>
     </div>
 
-  } @else {
-    <!-- 未生成方案 -->
-    <div class="empty-state">
-      <p>完成需求沟通后可生成设计方案</p>
-      <button class="btn-primary"
-              (click)="generateDesignProposal()"
-              [disabled]="!areRequiredStagesCompleted()">
-        生成设计方案
-      </button>
+    <!-- 色彩需求 -->
+    <div class="tab-content" v-show="activeTab === 'color'">
+      <ColorRequirementEditor
+        :requirement="selectedProduct.requirements.colorRequirement"
+        @update="updateColorRequirement" />
     </div>
-  }
-</div>
-```
 
-#### 3.2.2 方案详情弹窗
-```html
-<div class="proposal-detail-modal" *ngIf="showProposalDetailModal">
-  <div class="modal-overlay" (click)="closeProposalDetailModal()"></div>
-  <div class="modal-content">
-    <div class="modal-header">
-      <h3>设计方案详情</h3>
-      <button class="close-btn" (click)="closeProposalDetailModal()">×</button>
+    <!-- 材质需求 -->
+    <div class="tab-content" v-show="activeTab === 'material'">
+      <MaterialRequirementEditor
+        :requirement="selectedProduct.requirements.materialRequirement"
+        @update="updateMaterialRequirement" />
     </div>
 
-    <div class="modal-body">
-      <!-- 材质方案详情 -->
-      <section class="detail-section">
-        <h4>材质方案</h4>
-        <div class="material-list">
-          @for (material of proposalAnalysis.materials; track material.category) {
-            <div class="material-detail-card">
-              <div class="material-name">{{ material.category }}</div>
-              <div class="material-specs">
-                <div class="spec-item">
-                  <span class="label">类型:</span>
-                  <span>{{ material.specifications.type }}</span>
-                </div>
-                <div class="spec-item">
-                  <span class="label">等级:</span>
-                  <span>{{ material.specifications.grade }}</span>
-                </div>
-                <div class="spec-item">
-                  <span class="label">使用区域:</span>
-                  <span>{{ material.usage.area }}</span>
-                </div>
-                <div class="spec-item">
-                  <span class="label">占比:</span>
-                  <span>{{ material.usage.percentage }}%</span>
-                </div>
-              </div>
-            </div>
-          }
-        </div>
-      </section>
-
-      <!-- 设计风格详情 -->
-      <section class="detail-section">
-        <h4>设计风格:{{ proposalAnalysis.designStyle.primaryStyle }}</h4>
-        <div class="style-elements">
-          @for (elem of proposalAnalysis.designStyle.styleElements; track elem.element) {
-            <div class="style-element">
-              <span class="element-name">{{ elem.element }}</span>
-              <p class="element-desc">{{ elem.description }}</p>
-              <div class="influence-bar">
-                <div class="bar-fill" [style.width.%]="elem.influence"></div>
-                <span>{{ elem.influence }}%</span>
-              </div>
-            </div>
-          }
-        </div>
-      </section>
-
-      <!-- 色彩方案详情 -->
-      <section class="detail-section">
-        <h4>色彩方案</h4>
-        <div class="color-palette">
-          @for (color of proposalAnalysis.colorScheme.palette; track color.hex) {
-            <div class="color-item">
-              <div class="color-swatch" [style.background-color]="color.hex"></div>
-              <div class="color-info">
-                <span class="color-name">{{ color.color }}</span>
-                <span class="color-hex">{{ color.hex }}</span>
-                <span class="color-role">{{ color.role }}</span>
-                <span class="color-percentage">{{ color.percentage }}%</span>
-              </div>
-            </div>
-          }
-        </div>
-        <div class="color-psychology">
-          <h5>色彩心理</h5>
-          <p><strong>氛围:</strong>{{ proposalAnalysis.colorScheme.psychology.atmosphere }}</p>
-          <p><strong>情绪:</strong>{{ proposalAnalysis.colorScheme.psychology.mood }}</p>
-        </div>
-      </section>
-
-      <!-- 空间布局详情 -->
-      <section class="detail-section">
-        <h4>空间布局</h4>
-        <div class="space-dimensions">
-          <p><strong>总面积:</strong>{{ proposalAnalysis.spaceLayout.dimensions.area }}m²</p>
-          <p><strong>层高:</strong>{{ proposalAnalysis.spaceLayout.dimensions.height }}m</p>
-        </div>
-        <div class="functional-zones">
-          <h5>功能分区</h5>
-          @for (zone of proposalAnalysis.spaceLayout.functionalZones; track zone.zone) {
-            <div class="zone-item">
-              <div class="zone-header">
-                <span class="zone-name">{{ zone.zone }}</span>
-                <span class="zone-area">{{ zone.area }}m² ({{ zone.percentage }}%)</span>
-              </div>
-              <div class="zone-details">
-                <p><strong>功能需求:</strong>{{ zone.requirements.join('、') }}</p>
-                <p><strong>家具配置:</strong>{{ zone.furniture.join('、') }}</p>
-              </div>
-            </div>
-          }
-        </div>
-      </section>
-
-      <!-- 预算方案详情 -->
-      <section class="detail-section">
-        <h4>预算方案</h4>
-        <div class="budget-total">
-          <span>总预算:</span>
-          <span class="amount">¥{{ proposalAnalysis.budget.total.toLocaleString() }}</span>
-        </div>
-        <div class="budget-breakdown">
-          @for (item of proposalAnalysis.budget.breakdown; track item.category) {
-            <div class="budget-item">
-              <div class="budget-bar">
-                <span class="category">{{ item.category }}</span>
-                <div class="bar">
-                  <div class="bar-fill" [style.width.%]="item.percentage"></div>
-                </div>
-                <span class="amount">¥{{ item.amount.toLocaleString() }}</span>
-              </div>
-            </div>
-          }
-        </div>
-      </section>
-
-      <!-- 时间规划详情 -->
-      <section class="detail-section">
-        <h4>时间规划</h4>
-        <div class="timeline">
-          @for (phase of proposalAnalysis.timeline; track phase.phase) {
-            <div class="timeline-item">
-              <div class="phase-name">{{ phase.phase }}</div>
-              <div class="phase-duration">预计{{ phase.duration }}天</div>
-              @if (phase.dependencies.length > 0) {
-                <div class="phase-dependencies">
-                  依赖:{{ phase.dependencies.join('、') }}
-                </div>
-              }
-            </div>
-          }
-        </div>
-      </section>
+    <!-- 照明需求 -->
+    <div class="tab-content" v-show="activeTab === 'lighting'">
+      <LightingRequirementEditor
+        :requirement="selectedProduct.requirements.lightingRequirement"
+        @update="updateLightingRequirement" />
     </div>
 
-    <div class="modal-footer">
-      <button class="btn-secondary" (click)="closeProposalDetailModal()">关闭</button>
-      <button class="btn-primary" (click)="confirmProposalFromDetail()">确认方案</button>
+    <!-- 具体需求 -->
+    <div class="tab-content" v-show="activeTab === 'specific'">
+      <SpecificRequirementEditor
+        :requirement="selectedProduct.requirements.specificRequirements"
+        @update="updateSpecificRequirement" />
     </div>
   </div>
-</div>
-```
-
-### 3.3 方案确认流程
-
-#### 3.3.1 确认操作
-```typescript
-// project-detail.ts lines 2577-2585
-confirmProposal(): void {
-  console.log('确认方案按钮被点击');
-
-  if (!this.proposalAnalysis || this.proposalAnalysis.status !== 'completed') {
-    alert('请先生成设计方案');
-    return;
-  }
-
-  // 标记方案为已确认
-  this.proposalAnalysis.status = 'approved';
-
-  // 保存方案数据到项目
-  this.saveProposalToProject();
-
-  // 使用统一的阶段推进方法
-  this.advanceToNextStage('方案确认');
-
-  console.log('已跳转到建模阶段');
-}
-```
-
-#### 3.3.2 方案数据持久化
-```typescript
-saveProposalToProject(): void {
-  if (!this.proposalAnalysis) return;
-
-  const proposalData = {
-    projectId: this.projectId,
-    proposalId: this.proposalAnalysis.id,
-    proposal: this.proposalAnalysis,
-    approvedAt: new Date(),
-    approvedBy: this.getCurrentDesignerName()
-  };
-
-  this.projectService.saveProposal(proposalData).subscribe({
-    next: (result) => {
-      console.log('方案已保存:', result);
-    },
-    error: (error) => {
-      console.error('方案保存失败:', error);
-      alert('方案保存失败,请重试');
-    }
-  });
-}
-```
-
-## 4. 数据流转与同步
-
-### 4.1 需求数据流转图
-
-```mermaid
-sequenceDiagram
-    participant User as 用户
-    participant UI as 需求沟通UI
-    participant Service as AnalysisService
-    participant AI as AI引擎
-    participant Parent as 项目详情页
-
-    User->>UI: 上传参考图/CAD
-    UI->>Service: 调用分析接口
-    Service->>AI: 发送分析请求
-    AI-->>Service: 返回分析结果
-    Service-->>UI: 返回结构化数据
-    UI->>UI: 映射到设计指标
-    UI->>Parent: emit requirementDataUpdated
-    Parent->>Parent: 更新 requirementKeyInfo
-    Parent->>Parent: 同步到左侧信息面板
-```
-
-### 4.2 父子组件数据同步
-
-**子组件向父组件传递需求数据**:
-```typescript
-// requirements-confirm-card.component.ts
-@Output() requirementDataUpdated = new EventEmitter<any>();
-
-// 当任一需求数据更新时触发
-onDataUpdated(): void {
-  const data = {
-    colorAnalysisResult: this.colorAnalysisResult,
-    colorIndicators: this.colorIndicators,
-    spaceAnalysis: this.spaceAnalysis,
-    spaceIndicators: this.spaceIndicators,
-    materialAnalysisData: this.materialAnalysisData,
-    materialIndicators: this.materialIndicators,
-    lightingAnalysis: this.lightingAnalysis,
-    lightingIndicators: this.lightingIndicators,
-    detailedAnalysis: {
-      enhancedColorAnalysis: this.enhancedColorAnalysis,
-      formAnalysis: this.formAnalysis,
-      textureAnalysis: this.textureAnalysis,
-      patternAnalysis: this.patternAnalysis,
-      lightingAnalysis: this.lightingAnalysis
-    },
-    materials: [
-      ...this.referenceImages.map(img => ({ ...img, type: 'image' })),
-      ...this.cadFiles.map(file => ({ ...file, type: 'cad' }))
-    ]
-  };
 
-  this.requirementDataUpdated.emit(data);
-}
-```
-
-**父组件接收并处理**:
-```typescript
-// project-detail.ts lines 3071-3187
-onRequirementDataUpdated(data: any): void {
-  console.log('收到需求数据更新:', data);
-
-  // 1. 同步关键信息到左侧面板
-  this.syncRequirementKeyInfo(data);
-
-  // 2. 更新项目信息显示
-  this.updateProjectInfoFromRequirementData(data);
-}
-
-private syncRequirementKeyInfo(requirementData: any): void {
-  if (requirementData) {
-    // 同步色彩氛围信息
-    if (requirementData.colorIndicators) {
-      this.requirementKeyInfo.colorAtmosphere = {
-        description: requirementData.colorIndicators.colorRange || '',
-        mainColor: `rgb(${requirementData.colorIndicators.mainColor?.r || 0}, ...)`,
-        colorTemp: `${requirementData.colorIndicators.colorTemperature || 0}K`,
-        materials: []
-      };
-    }
-
-    // 同步空间结构信息
-    if (requirementData.spaceIndicators) {
-      this.requirementKeyInfo.spaceStructure = {
-        lineRatio: requirementData.spaceIndicators.lineRatio || 0,
-        blankRatio: requirementData.spaceIndicators.blankRatio || 0,
-        flowWidth: requirementData.spaceIndicators.flowWidth || 0,
-        aspectRatio: requirementData.spaceIndicators.aspectRatio || 0,
-        ceilingHeight: requirementData.spaceIndicators.ceilingHeight || 0
-      };
-    }
-
-    // 同步材质权重信息
-    if (requirementData.materialIndicators) {
-      this.requirementKeyInfo.materialWeights = {
-        fabricRatio: requirementData.materialIndicators.fabricRatio || 0,
-        woodRatio: requirementData.materialIndicators.woodRatio || 0,
-        metalRatio: requirementData.materialIndicators.metalRatio || 0,
-        smoothness: requirementData.materialIndicators.smoothness || 0,
-        glossiness: requirementData.materialIndicators.glossiness || 0
-      };
-    }
-
-    // 处理详细分析数据
-    if (requirementData.detailedAnalysis) {
-      this.enhancedColorAnalysis = requirementData.detailedAnalysis.enhancedColorAnalysis;
-      this.formAnalysis = requirementData.detailedAnalysis.formAnalysis;
-      this.textureAnalysis = requirementData.detailedAnalysis.textureAnalysis;
-      this.patternAnalysis = requirementData.detailedAnalysis.patternAnalysis;
-      this.lightingAnalysis = requirementData.detailedAnalysis.lightingAnalysis;
-    }
-
-    // 拆分参考图片和CAD文件
-    const materials = Array.isArray(requirementData?.materials) ? requirementData.materials : [];
-    this.referenceImages = materials.filter((m: any) => m?.type === 'image');
-    this.cadFiles = materials.filter((m: any) => m?.type === 'cad');
-
-    // 触发变更检测
-    this.cdr.detectChanges();
-  }
-}
-```
-
-### 4.3 左侧信息面板实时更新
+  <!-- 一致性检查 -->
+  <div class="consistency-check" v-if="products.length > 1">
+    <h3>跨产品一致性检查</h3>
+    <button class="check-button" @click="checkConsistency">
+      执行一致性检查
+    </button>
 
-**需求关键信息展示**(project-detail.html lines 350-450):
-```html
-<div class="requirement-key-info-panel">
-  <h4>需求关键信息</h4>
-
-  <!-- 色彩氛围 -->
-  <div class="key-info-section">
-    <h5>色彩氛围</h5>
-    @if (requirementKeyInfo.colorAtmosphere.description) {
-      <p class="info-value">{{ requirementKeyInfo.colorAtmosphere.description }}</p>
-      <div class="color-preview">
-        <div class="color-swatch" [style.background-color]="requirementKeyInfo.colorAtmosphere.mainColor"></div>
-        <span>主色 {{ requirementKeyInfo.colorAtmosphere.colorTemp }}</span>
+    <div class="check-results" v-if="consistencyResult">
+      <div class="result-item"
+           v-for="result in consistencyResult.conflicts"
+           :key="result.type"
+           :class="result.type">
+        <div class="conflict-header">
+          <span class="conflict-type">{{ getConflictTypeLabel(result.type) }}</span>
+          <span class="conflict-severity">{{ result.severity }}</span>
+        </div>
+        <div class="conflict-description">{{ result.description }}</div>
+        <div class="conflict-recommendation">{{ result.recommendation }}</div>
       </div>
-    } @else {
-      <p class="empty-hint">待采集</p>
-    }
+    </div>
   </div>
 
-  <!-- 空间结构 -->
-  <div class="key-info-section">
-    <h5>空间结构</h5>
-    @if (requirementKeyInfo.spaceStructure.aspectRatio > 0) {
-      <div class="info-grid">
-        <div class="info-item">
-          <span class="label">空间比例:</span>
-          <span class="value">{{ requirementKeyInfo.spaceStructure.aspectRatio.toFixed(1) }}</span>
-        </div>
-        <div class="info-item">
-          <span class="label">层高:</span>
-          <span class="value">{{ requirementKeyInfo.spaceStructure.ceilingHeight }}m</span>
-        </div>
-        <div class="info-item">
-          <span class="label">线条占比:</span>
-          <span class="value">{{ (requirementKeyInfo.spaceStructure.lineRatio * 100).toFixed(0) }}%</span>
-        </div>
-        <div class="info-item">
-          <span class="label">留白占比:</span>
-          <span class="value">{{ (requirementKeyInfo.spaceStructure.blankRatio * 100).toFixed(0) }}%</span>
-        </div>
+  <!-- 方案预览 -->
+  <div class="proposal-preview">
+    <h3>设计方案预览</h3>
+    <button class="generate-button" @click="generateProposal">
+      生成设计方案
+    </button>
+
+    <div class="proposal-content" v-if="designProposal">
+      <div class="proposal-overview">
+        <h4>整体设计概览</h4>
+        <p>{{ designProposal.overallDesign.description }}</p>
       </div>
-    } @else {
-      <p class="empty-hint">待采集</p>
-    }
-  </div>
 
-  <!-- 材质权重 -->
-  <div class="key-info-section">
-    <h5>材质权重</h5>
-    @if (requirementKeyInfo.materialWeights.woodRatio > 0 ||
-          requirementKeyInfo.materialWeights.fabricRatio > 0 ||
-          requirementKeyInfo.materialWeights.metalRatio > 0) {
-      <div class="material-bars">
-        <div class="material-bar">
-          <span class="material-label">木质</span>
-          <div class="bar">
-            <div class="bar-fill wood" [style.width.%]="requirementKeyInfo.materialWeights.woodRatio"></div>
-          </div>
-          <span class="percentage">{{ requirementKeyInfo.materialWeights.woodRatio }}%</span>
-        </div>
-        <div class="material-bar">
-          <span class="material-label">布艺</span>
-          <div class="bar">
-            <div class="bar-fill fabric" [style.width.%]="requirementKeyInfo.materialWeights.fabricRatio"></div>
+      <div class="proposal-budget">
+        <h4>预算方案</h4>
+        <div class="budget-breakdown">
+          <div v-for="item in designProposal.budget.breakdown" :key="item.type">
+            <span class="budget-type">{{ item.type }}:</span>
+            <span class="budget-amount">¥{{ item.amount.toLocaleString() }}</span>
           </div>
-          <span class="percentage">{{ requirementKeyInfo.materialWeights.fabricRatio }}%</span>
-        </div>
-        <div class="material-bar">
-          <span class="material-label">金属</span>
-          <div class="bar">
-            <div class="bar-fill metal" [style.width.%]="requirementKeyInfo.materialWeights.metalRatio"></div>
+          <div class="budget-total">
+            <span>总计: ¥{{ designProposal.budget.total.toLocaleString() }}</span>
           </div>
-          <span class="percentage">{{ requirementKeyInfo.materialWeights.metalRatio }}%</span>
         </div>
       </div>
-    } @else {
-      <p class="empty-hint">待采集</p>
-    }
-  </div>
-
-  <!-- 预设氛围 -->
-  <div class="key-info-section">
-    <h5>预设氛围</h5>
-    @if (requirementKeyInfo.presetAtmosphere.name) {
-      <p class="info-value">{{ requirementKeyInfo.presetAtmosphere.name }}</p>
-      <div class="atmosphere-details">
-        <span>色温:{{ requirementKeyInfo.presetAtmosphere.colorTemp }}</span>
-        <span>主材:{{ requirementKeyInfo.presetAtmosphere.materials.join('、') }}</span>
-      </div>
-    } @else {
-      <p class="empty-hint">待采集</p>
-    }
+    </div>
   </div>
 </div>
 ```
 
-## 5. 权限控制
-
-### 5.1 需求确认阶段权限矩阵
-
-| 操作 | 客服 | 设计师 | 组长 | 技术 |
-|-----|------|--------|------|------|
-| 查看需求沟通 | ✅ | ✅ | ✅ | ✅ |
-| 上传参考图 | ✅ | ✅ | ✅ | ❌ |
-| 上传CAD文件 | ✅ | ✅ | ✅ | ❌ |
-| 触发AI分析 | ✅ | ✅ | ✅ | ❌ |
-| 手动编辑指标 | ❌ | ✅ | ✅ | ❌ |
-| 生成设计方案 | ❌ | ✅ | ✅ | ❌ |
-| 确认方案 | ❌ | ✅ | ✅ | ❌ |
-| 推进到建模阶段 | ❌ | ✅ | ✅ | ❌ |
-
-### 5.2 权限控制实现
-
-**组件级别**:
+### 3.2 需求确认流程界面
 ```html
-<!-- 需求沟通卡片只读模式 -->
-<app-requirements-confirm-card
-  [readonly]="!canEditStage('需求沟通')"
-  ...>
-</app-requirements-confirm-card>
-
-<!-- 方案确认按钮权限 -->
-<button class="btn-primary"
-        (click)="confirmProposal()"
-        [disabled]="!canEditStage('方案确认') || !proposalAnalysis">
-  确认方案
-</button>
-```
-
-**操作级别**:
-```typescript
-generateDesignProposal(): void {
-  // 检查权限
-  if (!this.canEditStage('方案确认')) {
-    alert('您没有权限生成设计方案');
-    return;
-  }
-
-  // 检查前置条件
-  if (!this.areRequiredStagesCompleted()) {
-    alert('请先完成需求沟通的所有采集项');
-    return;
-  }
-
-  // 执行方案生成
-  this.isAnalyzing = true;
-  // ...
-}
-```
-
-## 6. 异常处理
-
-### 6.1 文件上传失败
-```typescript
-uploadFiles(files: File[]): Observable<any[]> {
-  const formData = new FormData();
-  files.forEach(file => formData.append('files', file));
-
-  return this.http.post<any>('/api/upload', formData).pipe(
-    catchError(error => {
-      let errorMessage = '文件上传失败';
-
-      if (error.status === 413) {
-        errorMessage = '文件过大,请上传小于10MB的文件';
-      } else if (error.status === 415) {
-        errorMessage = '文件格式不支持';
-      } else if (error.status === 500) {
-        errorMessage = '服务器错误,请稍后重试';
-      }
-
-      return throwError(() => new Error(errorMessage));
-    })
-  );
-}
-```
-
-### 6.2 AI分析失败
-```typescript
-triggerColorAnalysis(imageUrl: string): void {
-  this.isAnalyzingColors = true;
-
-  this.colorAnalysisService.analyzeImage(imageUrl).pipe(
-    retry(2), // 失败后重试2次
-    timeout(30000), // 30秒超时
-    catchError(error => {
-      this.isAnalyzingColors = false;
-
-      let errorMessage = 'AI色彩分析失败';
-
-      if (error.name === 'TimeoutError') {
-        errorMessage = '分析超时,请稍后重试';
-      } else if (error.status === 400) {
-        errorMessage = '图片格式不符合要求';
-      }
-
-      alert(errorMessage);
-      return of(null);
-    })
-  ).subscribe({
-    next: (result) => {
-      if (result) {
-        this.colorAnalysisResult = result;
-        // ...
-      }
-    }
-  });
-}
-```
-
-### 6.3 方案生成失败
-```typescript
-generateDesignProposal(): void {
-  this.isAnalyzing = true;
-  this.analysisProgress = 0;
-
-  // 设置超时保护
-  const timeout = setTimeout(() => {
-    if (this.isAnalyzing) {
-      this.isAnalyzing = false;
-      alert('方案生成超时,请重试');
-    }
-  }, 60000); // 60秒超时
-
-  // 模拟生成进度
-  const progressInterval = setInterval(() => {
-    this.analysisProgress += Math.random() * 15;
-    if (this.analysisProgress >= 100) {
-      this.analysisProgress = 100;
-      clearInterval(progressInterval);
-      clearTimeout(timeout);
-
-      try {
-        this.completeProposalGeneration();
-      } catch (error) {
-        console.error('方案生成失败:', error);
-        alert('方案生成失败,请重试');
-        this.isAnalyzing = false;
-      }
-    }
-  }, 500);
-}
-```
-
-## 7. 性能优化
-
-### 7.1 图片懒加载
-```typescript
-// 使用Intersection Observer实现图片懒加载
-@ViewChild('imageContainer') imageContainer?: ElementRef;
-
-ngAfterViewInit(): void {
-  if ('IntersectionObserver' in window) {
-    const observer = new IntersectionObserver((entries) => {
-      entries.forEach(entry => {
-        if (entry.isIntersecting) {
-          const img = entry.target as HTMLImageElement;
-          const src = img.dataset['src'];
-          if (src) {
-            img.src = src;
-            observer.unobserve(img);
-          }
-        }
-      });
-    });
+<!-- 需求确认流程界面 -->
+<div class="requirement-confirmation-flow">
+  <!-- 步骤指示器 -->
+  <div class="flow-steps">
+    <div class="step"
+         v-for="(step, index) in confirmationSteps"
+         :key="index"
+         :class="{
+           active: currentStep === index,
+           completed: currentStep > index
+         }">
+      <div class="step-number">{{ index + 1 }}</div>
+      <div class="step-label">{{ step.label }}</div>
+      <div class="step-description">{{ step.description }}</div>
+    </div>
+  </div>
 
-    const images = this.imageContainer?.nativeElement.querySelectorAll('img[data-src]');
-    images?.forEach((img: HTMLImageElement) => observer.observe(img));
-  }
-}
-```
+  <!-- 步骤内容 -->
+  <div class="step-content">
+    <!-- 需求沟通步骤 -->
+    <div v-if="currentStep === 0" class="communication-step">
+      <CommunicationInterface
+        :products="products"
+        @requirement-collected="handleRequirementCollected"
+        @communication-completed="handleCommunicationCompleted" />
+    </div>
 
-### 7.2 分析结果缓存
-```typescript
-// ColorAnalysisService with caching
-private analysisCache = new Map<string, ColorAnalysisResult>();
-
-analyzeImage(imageUrl: string): Observable<ColorAnalysisResult> {
-  // 检查缓存
-  const cached = this.analysisCache.get(imageUrl);
-  if (cached) {
-    console.log('使用缓存的分析结果');
-    return of(cached);
-  }
+    <!-- 方案确认步骤 -->
+    <div v-if="currentStep === 1" class="proposal-step">
+      <ProposalInterface
+        :products="products"
+        :requirements="collectedRequirements"
+        @proposal-generated="handleProposalGenerated"
+        @proposal-confirmed="handleProposalConfirmed" />
+    </div>
 
-  // 调用API分析
-  return this.http.post<ColorAnalysisResult>('/api/analyze/color', { imageUrl }).pipe(
-    tap(result => {
-      // 缓存结果(限制缓存大小)
-      if (this.analysisCache.size >= 50) {
-        const firstKey = this.analysisCache.keys().next().value;
-        this.analysisCache.delete(firstKey);
-      }
-      this.analysisCache.set(imageUrl, result);
-    })
-  );
-}
-```
+    <!-- 客户确认步骤 -->
+    <div v-if="currentStep === 2" class="confirmation-step">
+      <CustomerConfirmationInterface
+        :proposal="generatedProposal"
+        @customer-approved="handleCustomerApproved"
+        @customer-rejected="handleCustomerRejected" />
+    </div>
+  </div>
 
-### 7.3 大文件分片上传
-```typescript
-uploadLargeFile(file: File): Observable<UploadProgress> {
-  const chunkSize = 1024 * 1024; // 1MB per chunk
-  const chunks = Math.ceil(file.size / chunkSize);
-  const uploadProgress$ = new Subject<UploadProgress>();
-
-  let uploadedChunks = 0;
-
-  for (let i = 0; i < chunks; i++) {
-    const start = i * chunkSize;
-    const end = Math.min(start + chunkSize, file.size);
-    const chunk = file.slice(start, end);
-
-    const formData = new FormData();
-    formData.append('chunk', chunk);
-    formData.append('chunkIndex', i.toString());
-    formData.append('totalChunks', chunks.toString());
-    formData.append('fileName', file.name);
-
-    this.http.post('/api/upload/chunk', formData).subscribe({
-      next: () => {
-        uploadedChunks++;
-        uploadProgress$.next({
-          progress: (uploadedChunks / chunks) * 100,
-          status: 'uploading'
-        });
+  <!-- 操作按钮 -->
+  <div class="flow-actions">
+    <button v-if="currentStep > 0"
+            class="prev-button"
+            @click="previousStep">
+      上一步
+    </button>
 
-        if (uploadedChunks === chunks) {
-          uploadProgress$.next({ progress: 100, status: 'completed' });
-          uploadProgress$.complete();
-        }
-      },
-      error: (error) => {
-        uploadProgress$.error(error);
-      }
-    });
-  }
+    <button v-if="currentStep < confirmationSteps.length - 1"
+            class="next-button"
+            :disabled="!canProceedToNext"
+            @click="nextStep">
+      下一步
+    </button>
 
-  return uploadProgress$.asObservable();
-}
+    <button v-if="currentStep === confirmationSteps.length - 1"
+            class="complete-button"
+            :disabled="!canCompleteFlow"
+            @click="completeConfirmation">
+      完成需求确认
+    </button>
+  </div>
+</div>
 ```
 
-## 8. 测试用例
+## 4. 技术实现要点
 
-### 8.1 需求采集流程测试
-```typescript
-describe('Requirements Collection Flow', () => {
-  it('should complete all four requirement types', async () => {
-    // 1. 上传色彩参考图
-    await uploadFile('color-reference.jpg', 'colorInput');
-    expect(component.colorAnalysisResult).toBeTruthy();
-
-    // 2. 上传CAD文件
-    await uploadFile('floor-plan.dwg', 'cadInput');
-    expect(component.spaceAnalysis).toBeTruthy();
-
-    // 3. 上传材质图片
-    await uploadFile('material-ref.jpg', 'materialInput');
-    expect(component.materialAnalysisData.length).toBeGreaterThan(0);
-
-    // 4. 上传照明场景图
-    await uploadFile('lighting-scene.jpg', 'lightingInput');
-    expect(component.lightingAnalysis).toBeTruthy();
-
-    // 5. 验证完成度
-    expect(component.areAllRequirementsCompleted()).toBeTruthy();
-  });
-});
-```
+### 4.1 数据操作优化
+- **批量操作**:支持批量更新多个Product的需求字段
+- **事务处理**:确保跨产品需求更新的事务一致性
+- **缓存机制**:缓存需求分析结果,提高响应速度
 
-### 8.2 方案生成测试
-```typescript
-describe('Proposal Generation', () => {
-  it('should generate design proposal based on requirements', async () => {
-    // 准备需求数据
-    component.colorIndicators = mockColorIndicators;
-    component.spaceIndicators = mockSpaceIndicators;
-    component.materialIndicators = mockMaterialIndicators;
-    component.lightingIndicators = mockLightingIndicators;
-
-    // 触发方案生成
-    component.generateDesignProposal();
-
-    // 等待生成完成
-    await waitForCondition(() => component.proposalAnalysis !== null);
-
-    // 验证方案数据
-    expect(component.proposalAnalysis.status).toBe('completed');
-    expect(component.proposalAnalysis.materials.length).toBeGreaterThan(0);
-    expect(component.proposalAnalysis.designStyle).toBeTruthy();
-    expect(component.proposalAnalysis.colorScheme).toBeTruthy();
-    expect(component.proposalAnalysis.spaceLayout).toBeTruthy();
-    expect(component.proposalAnalysis.feasibility.overall).toBeGreaterThan(70);
-  });
-});
-```
+### 4.2 性能优化
+- **懒加载**:按需加载产品需求数据
+- **增量更新**:只同步变更的需求字段
+- **索引优化**:为Product表的requirements字段建立适当的索引
 
-### 8.3 阶段推进测试
-```typescript
-describe('Stage Progression', () => {
-  it('should advance from requirements to proposal to modeling', async () => {
-    // 1. 完成需求沟通
-    component.completeRequirementsCommunication();
-    expect(component.currentStage).toBe('方案确认');
-
-    // 2. 生成并确认方案
-    component.generateDesignProposal();
-    await waitForCondition(() => component.proposalAnalysis !== null);
-
-    component.confirmProposal();
-    expect(component.currentStage).toBe('建模');
-    expect(component.expandedStages['建模']).toBeTruthy();
-  });
-});
-```
+### 4.3 用户体验优化
+- **实时同步**:需求变更实时同步到相关界面
+- **智能提示**:基于历史数据提供需求填写建议
+- **可视化展示**:直观展示需求一致性检查结果
 
 ---
 
-**文档版本**:v1.0.0
-**创建日期**:2025-10-16
-**最后更新**:2025-10-16
-**维护人**:产品团队
+**文档版本**: v3.0 (Product表统一空间管理)
+**最后更新**: 2025-10-20
+**维护者**: YSS Development Team

+ 17 - 0
docs/task/2025102008-project-multi-space.md

@@ -0,0 +1,17 @@
+请您分析所有项目管理和project-detail相关功能,帮我梳理每个项目,多空间场景的具体需求和数据关系。
+从项目订单到归档,订单环节Project.data.quotation中会产生多个空间的报价。
+需求方案环节上传的参考图、cad图、需求描述也是区分多个空间的,只是单空间项目较多。
+交付阶段白模图片、软装小图、渲染进度、后期大图,都是区分多个空间不同交付物的。
+售后阶段,复盘分析项目各阶段情况(根据阶段开始时间,所需资料上传时间,下阶段时间),再根据不同空间对应不同人员的完成情况进行分析。
+最后全公司人效环节,也会用上面的数据进行分析。
+请您根据现有数据结构合理设计不同空间场景下,项目管理相关功能以及各个页面的具体需求变化,还有各个阶段数据范式数据变化的具体场景。
+
+可以将分析结果写在./docs/prd/项目-空间任务逻辑.md
+
+并且重新设计和更新相关文档:
+docs/prd/项目-交付执行.md
+docs/prd/项目-售后归档.md
+docs/prd/项目-订单分配.md
+docs/prd/项目-需求确认.md
+
+特别是./rules/schemas.md如果要新增或细化,需要更新数据范式文档

+ 831 - 0
rules/schemas-unified.md

@@ -0,0 +1,831 @@
+---
+category: schema
+title: YSS项目Parse Server数据范式 - 统一空间管理
+subtitle: 映三色设计师项目管理系统完整数据表结构(统一空间管理版本)
+name: 'yss-schemas-unified-space'
+label: database
+---
+
+# Parse Server 数据范式 - 映三色项目管理系统(统一空间管理)
+
+## 概述
+
+本文档定义了映三色(YSS)设计师项目管理系统的完整Parse Server数据范式。系统采用**多租户架构**,以Company为核心,支持客服、设计师、组长等多角色协作的全流程项目管理。
+
+**核心特性**:
+- 🏢 多租户架构,以Company为核心租户隔离
+- 👥 统一员工表(Profile)和客户表(ContactInfo)
+- 📋 项目与群聊灵活关联(Project ←→ GroupChat)
+- 🏠 **统一空间项目管理**:通过单一ProjectSpace表解决多空间项目管理问题
+- 🎨 设计师项目全生命周期管理
+- 💰 财务报价与结算流程
+- 📊 质量控制与客户反馈
+
+**空间管理优化**:
+- 🔄 **统一空间表**:ProjectSpace表统一管理所有空间相关功能
+- 💰 **空间报价**:通过`.quotation`字段管理空间级别的报价
+- 📁 **空间文件**:ProjectFile通过`.space`字段关联到具体空间
+- 👥 **空间团队**:通过`.assignedTeam`字段管理空间团队成员
+- 🎯 **空间需求**:通过`.requirements`字段管理空间详细需求
+- 📸 **空间全景**:通过`.panorama`字段管理空间全景图信息
+- ⭐ **空间评价**:通过`.reviews`字段管理空间评价和反馈
+
+---
+
+## 数据表关系图
+
+
+- Department 项目组(部门)
+  - name String 项目组名称
+  - type "project" 项目组
+  - leader Pointer<Profile> 组长
+  - company Pointer<Company> 指向当前帐套
+
+```plantuml
+@startuml
+!define TABLE(name,desc) class name as "desc" << (T,#FFAAAA) >>
+!define FIELD(name,type) name : type
+
+skinparam classAttributeIconSize 0
+skinparam class {
+    BackgroundColor LightYellow
+    BorderColor Black
+    ArrowColor Black
+}
+
+' ============ 核心租户与人员 ============
+TABLE(Company, "Company\n企业表") {
+    FIELD(objectId, String)
+    FIELD(name, String)
+    FIELD(corpId, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(Profile, "Profile\n员工档案表") {
+    FIELD(objectId, String)
+    FIELD(name, String)
+    FIELD(mobile, String)
+    FIELD(department, Pointer→Department)
+    FIELD(company, Pointer→Company)
+    FIELD(userId, String)
+    FIELD(roleName, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ContactInfo, "ContactInfo\n客户信息表") {
+    FIELD(objectId, String)
+    FIELD(name, String)
+    FIELD(mobile, String)
+    FIELD(company, Pointer→Company)
+    FIELD(external_userid, String)
+    FIELD(source, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 企微集成 ============
+TABLE(GroupChat, "GroupChat\n企微群聊表") {
+    FIELD(objectId, String)
+    FIELD(chat_id, String)
+    FIELD(name, String)
+    FIELD(company, Pointer→Company)
+    FIELD(project, Pointer→Project)
+    FIELD(member_list, Array)
+    FIELD(joinUrl, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ProjectGroup, "ProjectGroup\n项目群组关联表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(groupChat, Pointer→GroupChat)
+    FIELD(isPrimary, Boolean)
+    FIELD(createdAt, Date)
+}
+
+' ============ 项目模块 ============
+TABLE(Project, "Project\n项目表") {
+    FIELD(objectId, String)
+    FIELD(title, String)
+    FIELD(company, Pointer→Company)
+    FIELD(customer, Pointer→ContactInfo)
+    FIELD(assignee, Pointer→Profile)
+    FIELD(status, String)
+    FIELD(currentStage, String)
+    FIELD(deadline, Date)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 统一空间项目管理 ============
+TABLE(ProjectSpace, "ProjectSpace\n统一项目空间表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(name, String)
+    FIELD(type, String)
+    FIELD(area, Number)
+    FIELD(priority, Number)
+    FIELD(status, String)
+    FIELD(complexity, String)
+    FIELD(metadata, Object)
+    FIELD(quotation, Object)
+    FIELD(panorama, Object)
+    FIELD(assignedTeam, Array)
+    FIELD(deliveryFiles, Array)
+    FIELD(dependencies, Array)
+    FIELD(requirements, Object)
+    FIELD(reviews, Array)
+    FIELD(estimatedBudget, Number)
+    FIELD(estimatedDuration, Number)
+    FIELD(order, Number)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ProjectRequirement, "ProjectRequirement\n需求信息表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(company, Pointer→Company)
+    FIELD(spaces, Array)
+    FIELD(designRequirements, Object)
+    FIELD(materialAnalysis, Object)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ProjectTeam, "ProjectTeam\n项目团队表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(profile, Pointer→Profile)
+    FIELD(role, String)
+    FIELD(workload, Number)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 交付物与文件 ============
+TABLE(Product, "Product\n产品即交付物表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(company, Pointer→Company)
+    FIELD(stage, String)
+    FIELD(processType, String)
+    FIELD(space, Pointer→ProjectSpace)
+    FIELD(fileUrl, String)
+    FIELD(reviewStatus, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' NovaFile.id为Attachment.objectId
+TABLE(ProjectFile, "ProjectFile\n项目文件表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(space, Pointer→ProjectSpace)
+    FIELD(attach, Attachment)
+    FIELD(uploadedBy, Pointer→Profile)
+    FIELD(fileType, String)
+    FIELD(fileUrl, String)
+    FIELD(fileName, String)
+    FIELD(fileSize, Number)
+    FIELD(stage, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 财务模块 ============
+TABLE(ProjectSettlement, "ProjectSettlement\n结算记录表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(company, Pointer→Company)
+    FIELD(stage, String)
+    FIELD(amount, Number)
+    FIELD(percentage, Number)
+    FIELD(status, String)
+    FIELD(dueDate, Date)
+    FIELD(settledAt, Date)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ProjectVoucher, "ProjectVoucher\n付款凭证表") {
+    FIELD(objectId, String)
+    FIELD(settlement, Pointer→ProjectSettlement)
+    FIELD(project, Pointer→Project)
+    FIELD(amount, Number)
+    FIELD(voucherUrl, String)
+    FIELD(recognizedInfo, Object)
+    FIELD(verifiedBy, Pointer→Profile)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 质量与反馈 ============
+TABLE(ProjectFeedback, "ProjectFeedback\n客户反馈表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(customer, Pointer→ContactInfo)
+    FIELD(stage, String)
+    FIELD(feedbackType, String)
+    FIELD(content, String)
+    FIELD(rating, Number)
+    FIELD(status, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ProductCheck, "ProductCheck\n产品质量检查表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(checkType, String)
+    FIELD(checkedBy, Pointer→Profile)
+    FIELD(checkedAt, Date)
+    FIELD(isPassed, Boolean)
+    FIELD(items, Array)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+TABLE(ProjectIssue, "ProjectIssue\n异常记录表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(reportedBy, Pointer→Profile)
+    FIELD(exceptionType, String)
+    FIELD(severity, String)
+    FIELD(description, String)
+    FIELD(status, String)
+    FIELD(resolution, String)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 跟进记录 ============
+TABLE(ContactFollow, "ContactFollow\n跟进记录表") {
+    FIELD(objectId, String)
+    FIELD(project, Pointer→Project)
+    FIELD(sender, Pointer→Profile/ContactInfo)
+    FIELD(content, String)
+    FIELD(type, String)
+    FIELD(stage, String)
+    FIELD(attachments, Array)
+    FIELD(data, Object)
+    FIELD(isDeleted, Boolean)
+}
+
+' ============ 关系连线 ============
+
+' Company 一对多关系
+Company "1" --> "n" Profile : 企业员工
+Company "1" --> "n" ContactInfo : 企业客户
+Company "1" --> "n" Project : 企业项目
+Company "1" --> "n" GroupChat : 企业群聊
+
+' 项目核心关系
+Project "n" --> "1" Company : 所属企业
+Project "n" --> "1" ContactInfo : 客户
+Project "n" --> "1" Profile : 负责人(assignee)
+Project "1" --> "1" ProjectRequirement : 需求信息
+Project "1" <--> "n" GroupChat : ProjectGroup\n群聊关联
+Project "1" --> "n" ProjectTeam : 项目团队
+
+' 统一空间项目管理关系
+Project "1" --> "n" ProjectSpace : 项目空间
+ProjectSpace "1" --> "n" Product : 空间交付物
+ProjectSpace "1" --> "n" ProjectFile : 空间文件
+
+' 交付与财务
+Project "1" --> "n" Product : 交付物
+Project "1" --> "n" ProjectFile : 项目文件
+Project "1" --> "n" ProjectSettlement : 结算记录
+ProjectSettlement "1" --> "n" ProjectVoucher : 付款凭证
+
+' 质量与沟通
+Project "1" --> "n" ProjectFeedback : 客户反馈
+Project "1" --> "n" ProductCheck : 质量检查
+Project "1" --> "n" ProjectIssue : 异常记录
+Project "1" --> "n" ContactFollow : 跟进记录
+
+' 群聊关系
+GroupChat "n" --> "1" Company : 所属企业
+GroupChat "n" --> "1" Project : 关联项目(可选)
+
+@enduml
+```
+
+---
+
+## 核心数据表详解
+
+### 1. Company(企业表)
+
+**用途**: 多租户系统的核心,所有数据通过 company 字段进行租户隔离。
+
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "cDL6R1hgSi" |
+| name | String | 是 | 企业名称 | "映三色设计" |
+| corpId | String | 否 | 企业微信CorpID | "ww1234567890" |
+| data | Object | 否 | 扩展数据 | { settings: {...}, modules: [...] } |
+| isDeleted | Boolean | 否 | 软删除标记 | false |
+| createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
+| updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
+
+---
+
+### 2. Profile(员工档案表)
+
+**用途**: 存储企业员工档案信息,统一管理客服、设计师、组长等所有角色。
+
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "prof001" |
+| name | String | 是 | 员工姓名 | "张三" |
+| mobile | String | 否 | 手机号 | "13800138000" |
+| department | Pointer | 是 | 所属小组 | → Department |
+| company | Pointer | 是 | 所属企业 | → Company |
+| userId | String | 否 | 企微UserID | "zhangsan" |
+| roleName | String | 是 | 员工角色 | "客服" / "组员" / "组长" |
+| data | Object | 否 | 扩展数据 | { avatar, department, skills, ... } |
+| isDeleted | Boolean | 否 | 软删除标记 | false |
+| createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
+| updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
+
+**role 枚举值**:
+- `客服`: 客户服务人员,负责接单、跟进
+- `组员`: 设计师,负责具体设计工作
+- `组长`: 团队负责人,负责审核、分配
+- `财务`: 财务人员
+- `人事`: 人事人员
+- `管理员`: 系统管理员
+
+---
+
+### 3. ContactInfo(客户信息表)
+
+**用途**: 统一管理所有客户信息,支持企微外部联系人同步。
+
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "contact001" |
+| name | String | 是 | 客户姓名 | "李四" |
+| mobile | String | 否 | 手机号 | "13900139000" |
+| company | Pointer | 是 | 所属企业 | → Company |
+| external_userid | String | 否 | 企微外部联系人ID | "wmxxx" |
+| source | String | 否 | 来源渠道 | "朋友圈" / "信息流" / "转介绍" |
+| data | Object | 否 | 扩展数据 | { avatar, wechat, tags, ... } |
+| isDeleted | Boolean | 否 | 软删除标记 | false |
+| createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
+| updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
+
+---
+
+### 4. Project(项目表)
+
+**用途**: 项目管理的核心表,记录设计项目的全生命周期信息。
+
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "proj001" |
+| title | String | 是 | 项目标题 | "李总现代简约全案" |
+| company | Pointer | 是 | 所属企业 | → Company |
+| customer | Pointer | 是 | 客户 | → ContactInfo |
+| assignee | Pointer | 否 | 负责设计师 | → Profile |
+| status | String | 是 | 项目状态 | "进行中" |
+| currentStage | String | 是 | 当前阶段 | "建模" |
+| deadline | Date | 否 | 截止时间 | 2024-12-31T00:00:00.000Z |
+| data | Object | 否 | 扩展数据 | { requirements, stageHistory, ... } |
+| isDeleted | Boolean | 否 | 软删除标记 | false |
+| createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
+| updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
+
+---
+
+### 5. ProjectSpace(统一项目空间表)⭐
+
+**功能描述**: 统一管理项目空间的所有相关信息,包含报价、全景图、团队分配、需求等全生命周期数据。
+
+**字段说明**:
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "space001" |
+| project | Pointer | 是 | 所属项目 | → Project |
+| name | String | 是 | 空间名称 | "主卧" |
+| type | String | 是 | 空间类型 | "bedroom" |
+| area | Number | 否 | 面积(平方米) | 18.5 |
+| priority | Number | 否 | 优先级(1-10) | 8 |
+| status | String | 否 | 状态 | "in_progress" |
+| complexity | String | 否 | 复杂度 | "medium" |
+| metadata | Object | 否 | 空间元数据 | {dimensions, features} |
+| **quotation** | **Object** | **否** | **空间报价信息** | **{price, breakdown}** |
+| **panorama** | **Object** | **否** | **全景图信息** | **{url, hotspots}** |
+| **assignedTeam** | **Array** | **否** | **分配团队** | **[{profile, role}]** |
+| **deliveryFiles** | **Array** | **否** | **交付文件列表** | **[{fileId, type}]** |
+| **dependencies** | **Array** | **否** | **空间依赖关系** | **[{fromSpace, type}]** |
+| **requirements** | **Object** | **否** | **空间详细需求** | **{color, material}** |
+| **reviews** | **Array** | **否** | **空间评价列表** | **[{rating, comments}]** |
+| estimatedBudget | Number | 否 | 预估预算 | 35000 |
+| estimatedDuration | Number | 否 | 预估工期(天) | 7 |
+| order | Number | 否 | 排序顺序 | 2 |
+| isDeleted | Boolean | 否 | 是否删除 | false |
+
+**type 枚举值**:
+- `living_room`: 客厅
+- `bedroom`: 卧室
+- `kitchen`: 厨房
+- `bathroom`: 卫生间
+- `dining_room`: 餐厅
+- `study`: 书房
+- `balcony`: 阳台
+- `corridor`: 走廊
+- `storage`: 储物间
+- `entrance`: 玄关
+- `other`: 其他
+
+**status 枚举值**:
+- `not_started`: 未开始
+- `in_progress`: 进行中
+- `awaiting_review`: 待审核
+- `completed`: 已完成
+- `blocked`: 已阻塞
+- `delayed`: 已延期
+
+**quotation 字段结构示例**:
+```json
+{
+  "price": 35000,
+  "currency": "CNY",
+  "breakdown": {
+    "design": 15000,
+    "modeling": 10000,
+    "rendering": 8000,
+    "softDecor": 2000
+  },
+  "unitPrice": 1891,
+  "estimatedDays": 7,
+  "status": "approved",
+  "approvedBy": {"__type": "Pointer", "className": "Profile", "objectId": "prof001"},
+  "validUntil": "2024-12-31T00:00:00.000Z"
+}
+```
+
+**panorama 字段结构示例**:
+```json
+{
+  "url": "https://...",
+  "thumbnailUrl": "https://...",
+  "previewImages": ["https://...", "https://..."],
+  "hotspots": [
+    {
+      "id": "hotspot001",
+      "position": {"x": 0.5, "y": 0.3},
+      "type": "info",
+      "title": "定制衣柜",
+      "description": "实木定制衣柜,内部空间合理布局"
+    }
+  ],
+  "resolution": {"width": 8192, "height": 4096},
+  "fileSize": 15728640,
+  "renderTime": 1800,
+  "status": "completed"
+}
+```
+
+**assignedTeam 字段结构示例**:
+```json
+[
+  {
+    "profile": {"__type": "Pointer", "className": "Profile", "objectId": "prof001"},
+    "profileName": "张设计师",
+    "role": "primary_designer",
+    "workload": 0.6,
+    "assignedAt": "2024-10-01T10:00:00.000Z",
+    "assignedBy": {"__type": "Pointer", "className": "Profile", "objectId": "prof002"},
+    "status": "active",
+    "notes": "主要负责空间整体设计"
+  },
+  {
+    "profile": {"__type": "Pointer", "className": "Profile", "objectId": "prof003"},
+    "profileName": "李建模师",
+    "role": "modeling_designer",
+    "workload": 0.4,
+    "assignedAt": "2024-10-02T09:00:00.000Z",
+    "status": "active"
+  }
+]
+```
+
+**deliveryFiles 字段结构示例**:
+```json
+[
+  {
+    "fileId": {"__type": "Pointer", "className": "ProjectFile", "objectId": "file001"},
+    "fileName": "主卧效果图v2.jpg",
+    "fileType": "rendering",
+    "stage": "rendering",
+    "uploadedAt": "2024-10-15T14:30:00.000Z",
+    "uploadedBy": {"__type": "Pointer", "className": "Profile", "objectId": "prof001"},
+    "reviewStatus": "approved"
+  }
+]
+```
+
+**dependencies 字段结构示例**:
+```json
+[
+  {
+    "fromSpace": {"__type": "Pointer", "className": "ProjectSpace", "objectId": "space002"},
+    "fromSpaceName": "客厅",
+    "type": "style_reference",
+    "description": "需要与客厅风格保持一致",
+    "priority": "high",
+    "status": "active"
+  }
+]
+```
+
+**requirements 字段结构示例**:
+```json
+{
+  "colorRequirement": {
+    "primaryHue": 180,
+    "saturation": 45,
+    "temperature": "暖色调",
+    "colorDistribution": [
+      {"hex": "#F5F5DC", "percentage": 40, "name": "米白色"},
+      {"hex": "#8B4513", "percentage": 30, "name": "原木色"}
+    ]
+  },
+  "materialRequirement": {
+    "preferred": ["实木", "环保材料"],
+    "avoid": ["塑料", "合成材料"],
+    "budget": { "min": 20000, "max": 40000 }
+  },
+  "lightingRequirement": {
+    "naturalLight": "充足",
+    "lightColor": "暖白",
+    "lightIntensity": "中等",
+    "specialRequirements": ["床头阅读灯", "氛围灯"]
+  },
+  "specificRequirements": [
+    "需要大储物空间",
+    "独立卫浴",
+    "飘窗设计"
+  ],
+  "referenceImages": ["https://...", "https://..."],
+  "referenceFiles": ["file002", "file003"]
+}
+```
+
+**reviews 字段结构示例**:
+```json
+[
+  {
+    "reviewId": "review001",
+    "satisfactionScore": 4.5,
+    "spaceSpecificRatings": {
+      "design": 5,
+      "functionality": 4,
+      "material": 4,
+      "lighting": 5
+    },
+    "usageFeedback": {
+      "positive": ["储物空间充足", "光线舒适"],
+      "improvements": ["可以增加插座数量"]
+    },
+    "comments": "整体设计很满意,储物功能强大",
+    "afterPhotos": ["https://...", "https://..."],
+    "submittedAt": "2024-11-15T10:00:00.000Z",
+    "submittedBy": {"__type": "Pointer", "className": "ContactInfo", "objectId": "contact001"}
+  }
+]
+```
+
+**使用场景示例**:
+```typescript
+// 创建空间
+const ProjectSpace = Parse.Object.extend("ProjectSpace");
+const space = new ProjectSpace();
+space.set("project", project.toPointer());
+space.set("name", "主卧");
+space.set("type", "bedroom");
+space.set("area", 18.5);
+
+// 设置空间报价
+space.set("quotation", {
+  price: 35000,
+  currency: "CNY",
+  breakdown: { design: 15000, modeling: 10000, rendering: 8000, softDecor: 2000 },
+  status: "pending"
+});
+
+// 设置空间团队
+space.set("assignedTeam", [
+  {
+    profile: designer.toPointer(),
+    profileName: designer.get("name"),
+    role: "primary_designer",
+    workload: 0.8,
+    assignedAt: new Date()
+  }
+]);
+
+await space.save();
+
+// 查询项目的所有空间
+const spaceQuery = new Parse.Query("ProjectSpace");
+spaceQuery.equalTo("project", projectId);
+spaceQuery.notEqualTo("isDeleted", true);
+spaceQuery.ascending("order");
+const spaces = await spaceQuery.find();
+
+// 查询设计师负责的空间
+const designerSpaceQuery = new Parse.Query("ProjectSpace");
+designerSpaceQuery.equalTo("project", projectId);
+designerSpaceQuery.equalTo("assignedTeam.profile", designerId);
+designerSpaceQuery.equalTo("assignedTeam.status", "active");
+```
+
+---
+
+### 6. Product(交付物表)
+
+**用途**: 记录项目各阶段的交付物,如效果图、施工图等。**已更新支持空间关联**。
+
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "deliv001" |
+| project | Pointer | 是 | 所属项目 | → Project |
+| company | Pointer | 是 | 所属企业 | → Company |
+| stage | String | 是 | 所属阶段 | "建模" / "渲染" / "后期" |
+| processType | String | 否 | 工序类型 | "modeling" / "rendering" |
+| **space** | **Pointer** | **否** | **所属空间** | **→ ProjectSpace** |
+| fileUrl | String | 是 | 文件URL | "https://..." |
+| reviewStatus | String | 是 | 审核状态 | "pending" / "approved" |
+| data | Object | 否 | 扩展数据 | { thumbnailUrl, version, ... } |
+| isDeleted | Boolean | 否 | 软删除标记 | false |
+| createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
+| updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
+
+---
+
+### 7. ProjectFile(项目文件表)
+
+**用途**: 存储项目相关的所有文件,如CAD图纸、参考图等。**已更新支持空间关联**。
+
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "file001" |
+| project | Pointer | 是 | 所属项目 | → Project |
+| **space** | **Pointer** | **否** | **所属空间** | **→ ProjectSpace** |
+| attach | Pointer | 是 | 附件 | → Attachment |
+| uploadedBy | Pointer | 是 | 上传人 | → Profile |
+| fileType | String | 是 | 文件类型 | "cad" / "reference" / "document" |
+| fileUrl | String | 是 | 文件URL | "https://..." |
+| fileName | String | 是 | 文件名 | "户型图.dwg" |
+| fileSize | Number | 否 | 文件大小(字节) | 1024000 |
+| stage | String | 否 | 关联阶段 | "需求沟通" |
+| data | Object | 否 | 扩展数据 | { thumbnailUrl, ... } |
+| isDeleted | Boolean | 否 | 软删除标记 | false |
+| createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
+| updatedAt | Date | 自动 | 更新时间 | 2024-01-01T00:00:00.000Z |
+
+**使用场景示例**:
+```typescript
+// 上传空间相关文件
+const ProjectFile = Parse.Object.extend("ProjectFile");
+const file = new ProjectFile();
+file.set("project", project.toPointer());
+file.set("space", space.toPointer()); // 关联到具体空间
+file.set("fileName", "主卧参考图.jpg");
+file.set("fileType", "reference");
+file.set("uploadedBy", profile.toPointer());
+await file.save();
+
+// 查询空间的所有文件
+const fileQuery = new Parse.Query("ProjectFile");
+fileQuery.equalTo("space", spaceId);
+fileQuery.notEqualTo("isDeleted", true);
+const spaceFiles = await fileQuery.find();
+```
+
+---
+
+## 统一空间管理的优势
+
+### 1. 数据结构简化
+- **减少表数量**: 从15个空间相关表简化为1个统一表
+- **降低复杂度**: 消除复杂的表间关联关系
+- **提高性能**: 减少JOIN查询操作
+
+### 2. 功能完整性
+- **空间报价**: 通过`.quotation`字段实现空间级报价管理
+- **空间全景**: 通过`.panorama`字段管理全景图和热点信息
+- **团队协作**: 通过`.assignedTeam`字段管理空间团队成员
+- **文件管理**: 通过`.deliveryFiles`字段管理空间交付物
+- **需求跟踪**: 通过`.requirements`字段管理详细需求
+- **评价反馈**: 通过`.reviews`字段管理空间评价
+
+### 3. 使用便利性
+- **一次查询**: 获取空间所有相关信息
+- **原子操作**: 空间信息可原子性更新
+- **数据一致性**: 避免分布式数据一致性问题
+- **扩展灵活**: Object字段支持灵活扩展
+
+### 4. 实现示例对比
+
+**原有方式(多表操作)**:
+```typescript
+// 需要多次查询
+const space = await spaceQuery.get(spaceId);
+const quotation = await quotationQuery.equalTo("space", spaceId).first();
+const team = await teamQuery.equalTo("space", spaceId).find();
+const files = await fileQuery.equalTo("space", spaceId).find();
+const reviews = await reviewQuery.equalTo("space", spaceId).find();
+```
+
+**统一方式(单表操作)**:
+```typescript
+// 一次查询获取所有信息
+const space = await spaceQuery.get(spaceId);
+const quotation = space.get("quotation");
+const team = space.get("assignedTeam");
+const files = space.get("deliveryFiles");
+const reviews = space.get("reviews");
+```
+
+---
+
+## 数据迁移指南
+
+### 从多表结构迁移到统一ProjectSpace
+
+```typescript
+// 1. 迁移空间报价数据
+const oldQuotations = await new Parse.Query("SpaceQuotation").find();
+for (const quotation of oldQuotations) {
+  const space = await quotation.get("space");
+  space.set("quotation", {
+    price: quotation.get("totalAmount"),
+    currency: "CNY",
+    breakdown: quotation.get("priceBreakdown"),
+    status: "migrated"
+  });
+  await space.save();
+}
+
+// 2. 迁移空间团队数据
+const oldAssignments = await new Parse.Query("SpaceAssignment").find();
+for (const assignment of oldAssignments) {
+  const space = await assignment.get("space");
+  const team = space.get("assignedTeam") || [];
+  team.push({
+    profile: assignment.get("assigneeId"),
+    profileName: assignment.get("assigneeName"),
+    role: assignment.get("role"),
+    workload: assignment.get("workload"),
+    assignedAt: assignment.get("assignedAt"),
+    assignedBy: assignment.get("assignedBy"),
+    status: "migrated"
+  });
+  space.set("assignedTeam", team);
+  await space.save();
+}
+
+// 3. 迁移空间需求
+const oldRequirements = await new Parse.Query("SpaceRequirement").find();
+for (const requirement of oldRequirements) {
+  const space = await requirement.get("space");
+  space.set("requirements", {
+    colorRequirement: requirement.get("colorRequirement"),
+    materialRequirement: requirement.get("materialRequirement"),
+    lightingRequirement: requirement.get("lightingRequirement"),
+    specificRequirements: requirement.get("specificRequirements"),
+    constraints: requirement.get("constraints"),
+    referenceImages: requirement.get("referenceImages"),
+    referenceFiles: requirement.get("referenceFiles")
+  });
+  await space.save();
+}
+```
+
+---
+
+## 总结
+
+通过统一空间管理的重构,YSS项目管理系统实现了:
+
+✅ **架构简化**: 从15个空间相关表简化为1个ProjectSpace表
+✅ **功能完整**: 保留所有空间管理功能,无缝迁移
+✅ **性能提升**: 减少查询复杂度,提高响应速度
+✅ **维护便利**: 降低数据模型维护成本
+✅ **扩展灵活**: Object字段支持未来功能扩展
+✅ **数据一致**: 避免多表数据一致性问题
+
+这个统一的空间管理方案解决了多空间项目管理的复杂性,通过单一ProjectSpace表实现了报价、全景图、团队协作、文件管理、需求跟踪、评价反馈等全生命周期管理,大大简化了系统架构并提高了开发效率。
+
+---
+
+**文档版本**: v2.0(统一空间管理)
+**最后更新**: 2025-10-20
+**维护者**: YSS Development Team

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 235 - 656
rules/schemas.md


+ 5 - 5
src/modules/project/pages/project-detail/stages/stage-aftercare.component.html

@@ -115,11 +115,11 @@
                 @for (star of [1,2,3,4,5]; track star) {
                   <svg
                     class="star-icon"
-                    [class.active]="star <= customerFeedback.rating"
+                    [class.active]="star <= customerFeedback.overallRating"
                     (click)="setRating('rating', star)"
                     xmlns="http://www.w3.org/2000/svg"
                     viewBox="0 0 512 512">
-                    @if (star <= customerFeedback.rating) {
+                    @if (star <= customerFeedback.overallRating) {
                       <path d="M394 480a16 16 0 01-9.39-3L256 383.76 127.39 477a16 16 0 01-24.55-18.08L153 310.35 23 221.2a16 16 0 019-29.2h160.38l48.4-148.95a16 16 0 0130.44 0l48.4 149H480a16 16 0 019.05 29.2L359 310.35l50.13 148.53A16 16 0 01394 480z"/>
                     } @else {
                       <path d="M394 480a16 16 0 01-9.39-3L256 383.76 127.39 477a16 16 0 01-24.55-18.08L153 310.35 23 221.2a16 16 0 019-29.2h160.38l48.4-148.95a16 16 0 0130.44 0l48.4 149H480a16 16 0 019.05 29.2L359 310.35l50.13 148.53A16 16 0 01394 480z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
@@ -236,10 +236,10 @@
                 @for (star of [1,2,3,4,5]; track star) {
                   <svg
                     class="star-icon"
-                    [class.active]="star <= customerFeedback.rating"
+                    [class.active]="star <= customerFeedback.overallRating"
                     xmlns="http://www.w3.org/2000/svg"
                     viewBox="0 0 512 512">
-                    @if (star <= customerFeedback.rating) {
+                    @if (star <= customerFeedback.overallRating) {
                       <path d="M394 480a16 16 0 01-9.39-3L256 383.76 127.39 477a16 16 0 01-24.55-18.08L153 310.35 23 221.2a16 16 0 019-29.2h160.38l48.4-148.95a16 16 0 0130.44 0l48.4 149H480a16 16 0 019.05 29.2L359 310.35l50.13 148.53A16 16 0 01394 480z"/>
                     } @else {
                       <path d="M394 480a16 16 0 01-9.39-3L256 383.76 127.39 477a16 16 0 01-24.55-18.08L153 310.35 23 221.2a16 16 0 019-29.2h160.38l48.4-148.95a16 16 0 0130.44 0l48.4 149H480a16 16 0 019.05 29.2L359 310.35l50.13 148.53A16 16 0 01394 480z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
@@ -247,7 +247,7 @@
                   </svg>
                 }
               </div>
-              <p class="rating-text">{{ customerFeedback.rating }}.0 分</p>
+              <p class="rating-text">{{ customerFeedback.overallRating }}.0 分</p>
             </div>
 
             @if (customerFeedback.comments) {

+ 515 - 64
src/modules/project/pages/project-detail/stages/stage-aftercare.component.ts

@@ -1,9 +1,11 @@
-import { Component, OnInit, Input, inject } from '@angular/core';
+import { Component, OnInit, Input, inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
 import { NovaUploadService } from 'fmode-ng/storage';
+import { ProjectFileService } from '../../../services/project-file.service';
+import { MultiSpaceService, ProjectSpace } from '../../../services/multi-space.service';
 
 const Parse = FmodeParse.with('nova');
 
@@ -22,7 +24,8 @@ const Parse = FmodeParse.with('nova');
   standalone: true,
   imports: [CommonModule, FormsModule],
   templateUrl: './stage-aftercare.component.html',
-  styleUrls: ['./stage-aftercare.component.scss']
+  styleUrls: ['./stage-aftercare.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class StageAftercareComponent implements OnInit {
   @Input() project: FmodeObject | null = null;
@@ -34,31 +37,49 @@ export class StageAftercareComponent implements OnInit {
   cid: string = '';
   projectId: string = '';
 
+  // 多空间管理
+  projectSpaces: ProjectSpace[] = [];
+  isMultiSpaceProject: boolean = false;
+  activeSpaceId: string = '';
+  selectedSpaceIds: string[] = [];
+
+  // 售后视图切换
+  aftercareView: string = 'overview'; // overview | spaces | complaints | panorama
+
   // 尾款信息
   finalPayment = {
     totalAmount: 0,
     paidAmount: 0,
     remainingAmount: 0,
     paymentVouchers: [] as Array<{
+      id: string;
       url: string;
       amount: number;
       paymentTime: Date;
       paymentMethod: string;
       ocrResult?: any;
+      spaceId?: string; // 支持按空间分摊尾款
     }>,
     status: 'pending' // pending | partial | completed
   };
 
-  // 客户评价
+  // 客户评价(支持整体评价和空间评价)
   customerFeedback = {
     submitted: false,
-    rating: 0,
+    overallRating: 0,
     serviceRating: 0,
     qualityRating: 0,
     timelinessRating: 0,
     comments: '',
     improvements: '',
-    wouldRecommend: true
+    wouldRecommend: true,
+    spaceFeedbacks: [] as Array<{
+      spaceId: string;
+      spaceName: string;
+      rating: number;
+      comments: string;
+      issues: string[];
+    }>
   };
 
   // 项目复盘
@@ -69,8 +90,36 @@ export class StageAftercareComponent implements OnInit {
     challenges: string[];
     lessons: string[];
     recommendations: string[];
+    spaceRetrospectives: Array<{
+      spaceId: string;
+      spaceName: string;
+      performance: number; // 1-5
+      issues: string[];
+      improvements: string[];
+    }>;
   } | null = null;
 
+  // 售后投诉管理
+  complaints = [] as Array<{
+    id: string;
+    spaceId?: string;
+    spaceName?: string;
+    type: string; // quality | service | timeline | other
+    description: string;
+    severity: 'low' | 'medium' | 'high';
+    status: 'open' | 'processing' | 'resolved';
+    reportedBy: string;
+    reportedTime: Date;
+    assignedTo?: string;
+    responses: Array<{
+      content: string;
+      respondedBy: string;
+      responseTime: Date;
+    }>;
+    resolution?: string;
+    resolvedTime?: Date;
+  }>;
+
   // 归档状态
   archiveStatus = {
     archived: false,
@@ -78,6 +127,22 @@ export class StageAftercareComponent implements OnInit {
     archivedBy: null as { id: string; name: string } | null
   };
 
+  // 全景照片收集
+  panoramaCollection = {
+    enabled: false,
+    images: [] as Array<{
+      id: string;
+      spaceId: string;
+      spaceName: string;
+      url: string;
+      uploadTime: Date;
+      uploadedBy: string;
+      type: string; // before | after | detail
+      description?: string;
+    }>,
+    status: 'pending' // pending | collecting | completed
+  };
+
   // 加载状态
   loading: boolean = true;
   uploading: boolean = false;
@@ -88,7 +153,10 @@ export class StageAftercareComponent implements OnInit {
   private uploadService: NovaUploadService = inject(NovaUploadService);
 
   constructor(
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private projectFileService: ProjectFileService,
+    private multiSpaceService: MultiSpaceService,
+    private cdr: ChangeDetectorRef
   ) {}
 
   async ngOnInit() {
@@ -124,37 +192,14 @@ export class StageAftercareComponent implements OnInit {
         this.canEdit = ['客服', '组长', '管理员'].includes(role);
       }
 
+      // 加载项目空间数据
       if (this.project) {
-        const data = this.project.get('data') || {};
-
-        // 加载尾款信息
-        if (data.finalPayment) {
-          this.finalPayment = data.finalPayment;
-        } else {
-          // 从报价总额初始化
-          const quotation = data.quotation;
-          if (quotation) {
-            this.finalPayment.totalAmount = quotation.total;
-            this.finalPayment.remainingAmount = quotation.total;
-          }
-        }
-
-        // 加载客户评价
-        if (data.customerFeedback) {
-          this.customerFeedback = data.customerFeedback;
-        }
-
-        // 加载项目复盘
-        if (data.projectRetrospective) {
-          this.projectRetrospective = data.projectRetrospective;
-        }
-
-        // 加载归档状态
-        if (data.archiveStatus) {
-          this.archiveStatus = data.archiveStatus;
-        }
+        await this.loadProjectSpaces();
+        await this.loadAftercareData();
       }
 
+      this.cdr.markForCheck();
+
     } catch (err) {
       console.error('加载失败:', err);
     } finally {
@@ -163,47 +208,280 @@ export class StageAftercareComponent implements OnInit {
   }
 
   /**
-   * 上传支付凭证
+   * 加载项目空间数据
    */
-  async uploadPaymentVoucher(event: any) {
-    const file = event.target.files[0];
-    if (!file) return;
+  async loadProjectSpaces(): Promise<void> {
+    if (!this.project) return;
 
-    // 简单的文件类型验证
-    if (!file.type.startsWith('image/')) {
-      alert('请上传图片文件');
-      return;
+    try {
+      this.projectSpaces = await this.multiSpaceService.getProjectSpaces(this.project.id!);
+      this.isMultiSpaceProject = this.projectSpaces.length > 1;
+
+      // 如果有空间,默认选中第一个
+      if (this.projectSpaces.length > 0 && !this.activeSpaceId) {
+        this.activeSpaceId = this.projectSpaces[0].id;
+      }
+
+    } catch (error) {
+      console.error('加载项目空间失败:', error);
     }
+  }
 
-    // 验证文件大小 (10MB)
-    if (file.size > 10 * 1024 * 1024) {
-      alert('图片大小不能超过10MB');
-      return;
+  /**
+   * 加载售后数据
+   */
+  async loadAftercareData(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      const data = this.project.get('data') || {};
+
+      // 加载尾款信息
+      if (data.finalPayment) {
+        this.finalPayment = { ...this.finalPayment, ...data.finalPayment };
+      } else {
+        // 从报价总额初始化
+        const quotation = data.quotation;
+        if (quotation) {
+          this.finalPayment.totalAmount = quotation.total;
+          this.finalPayment.remainingAmount = quotation.total;
+        }
+      }
+
+      // 加载客户评价
+      if (data.customerFeedback) {
+        this.customerFeedback = { ...this.customerFeedback, ...data.customerFeedback };
+      }
+
+      // 加载项目复盘
+      if (data.projectRetrospective) {
+        this.projectRetrospective = data.projectRetrospective;
+      }
+
+      // 加载投诉记录
+      if (data.complaints) {
+        this.complaints = data.complaints;
+      }
+
+      // 加载全景照片收集
+      if (data.panoramaCollection) {
+        this.panoramaCollection = { ...this.panoramaCollection, ...data.panoramaCollection };
+      }
+
+      // 加载归档状态
+      if (data.archiveStatus) {
+        this.archiveStatus = data.archiveStatus;
+      }
+
+    } catch (error) {
+      console.error('加载售后数据失败:', error);
     }
+  }
+
+  // ===== 多空间售后管理方法 =====
+
+  /**
+   * 切换售后视图
+   */
+  onAftercareViewChange(event: any): void {
+    this.aftercareView = event.detail.value;
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 选择空间
+   */
+  selectSpace(spaceId: string): void {
+    this.activeSpaceId = spaceId;
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 切换空间选择状态
+   */
+  toggleSpaceSelection(spaceId: string): void {
+    const index = this.selectedSpaceIds.indexOf(spaceId);
+    if (index > -1) {
+      this.selectedSpaceIds.splice(index, 1);
+    } else {
+      this.selectedSpaceIds.push(spaceId);
+    }
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 上传支付凭证
+   */
+  async uploadPaymentVoucher(event: any, spaceId?: string) {
+    const files = event.target.files;
+    if (!files || files.length === 0) return;
 
     try {
       this.uploading = true;
 
-      // 使用 NovaUploadService 上传文件
-      const fileResult: any = await this.uploadService.upload(file);
-      const url = fileResult.url;
-
-      // 暂时不使用OCR,需要手动输入金额和支付方式
-      this.finalPayment.paymentVouchers.push({
-        url: url,
-        amount: 0,
-        paymentTime: new Date(),
-        paymentMethod: '待确认',
-        ocrResult: { note: '请手动核对金额和支付方式' }
-      });
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
 
-      await this.saveDraft();
+        // 简单的文件类型验证
+        if (!file.type.startsWith('image/')) {
+          console.warn(`文件 ${file.name} 不是图片格式,跳过`);
+          continue;
+        }
+
+        // 验证文件大小 (10MB)
+        if (file.size > 10 * 1024 * 1024) {
+          console.warn(`文件 ${file.name} 超过10MB限制,跳过`);
+          continue;
+        }
+
+        // 使用ProjectFileService上传文件
+        const uploadedFile = await this.projectFileService.uploadProjectFile(
+          file,
+          this.projectId,
+          'payment_voucher',
+          spaceId,
+          'aftercare',
+          {
+            paymentType: 'final_payment',
+            uploadSource: 'aftercare'
+          }
+        );
+
+        // 暂时不使用OCR,需要手动输入金额和支付方式
+        this.finalPayment.paymentVouchers.push({
+          id: uploadedFile.md5 || '',
+          url: uploadedFile.url || '',
+          amount: 0,
+          paymentTime: new Date(),
+          paymentMethod: '待确认',
+          ocrResult: { note: '请手动核对金额和支付方式' },
+          spaceId: spaceId
+        });
+      }
 
-      alert('凭证已上传,请手动核对金额和支付方式');
+      this.cdr.markForCheck();
+      // await this.saveDraft();
+      // alert('凭证已上传,请手动核对金额和支付方式');
 
     } catch (error: any) {
       console.error('上传失败:', error);
-      alert('上传失败: ' + (error?.message || '未知错误'));
+      // alert('上传失败: ' + (error?.message || '未知错误'));
+    } finally {
+      this.uploading = false;
+    }
+  }
+
+  /**
+   * 添加空间评价
+   */
+  addSpaceFeedback(spaceId: string): void {
+    const space = this.projectSpaces.find(s => s.id === spaceId);
+    if (!space) return;
+
+    const existingFeedback = this.customerFeedback.spaceFeedbacks.find(f => f.spaceId === spaceId);
+    if (!existingFeedback) {
+      this.customerFeedback.spaceFeedbacks.push({
+        spaceId,
+        spaceName: space.name || this.multiSpaceService.getSpaceTypeName(space.type),
+        rating: 0,
+        comments: '',
+        issues: []
+      });
+      this.cdr.markForCheck();
+    }
+  }
+
+  /**
+   * 更新空间评价
+   */
+  updateSpaceFeedback(spaceId: string, field: string, value: any): void {
+    const feedback = this.customerFeedback.spaceFeedbacks.find(f => f.spaceId === spaceId);
+    if (feedback) {
+      (feedback as any)[field] = value;
+      this.cdr.markForCheck();
+    }
+  }
+
+  /**
+   * 创建投诉记录
+   */
+  async createComplaint(complaint: {
+    spaceId?: string;
+    type: string;
+    description: string;
+    severity: 'low' | 'medium' | 'high';
+  }): Promise<void> {
+    const space = complaint.spaceId ? this.projectSpaces.find(s => s.id === complaint.spaceId) : undefined;
+
+    const newComplaint = {
+      id: `complaint_${Date.now()}`,
+      spaceId: complaint.spaceId,
+      spaceName: space?.name || this.multiSpaceService.getSpaceTypeName(space?.type || ''),
+      type: complaint.type,
+      description: complaint.description,
+      severity: complaint.severity,
+      status: 'open' as const,
+      reportedBy: this.customer?.get('name') || '客户',
+      reportedTime: new Date(),
+      responses: []
+    };
+
+    this.complaints.push(newComplaint);
+    this.cdr.markForCheck();
+
+    // await this.saveDraft();
+  }
+
+  /**
+   * 上传全景照片
+   */
+  async uploadPanoramaImage(event: any, spaceId: string, type: string) {
+    const files = event.target.files;
+    if (!files || files.length === 0) return;
+
+    const space = this.projectSpaces.find(s => s.id === spaceId);
+    if (!space) return;
+
+    try {
+      this.uploading = true;
+
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+
+        if (!file.type.startsWith('image/')) {
+          console.warn(`文件 ${file.name} 不是图片格式,跳过`);
+          continue;
+        }
+
+        const uploadedFile = await this.projectFileService.uploadProjectFile(
+          file,
+          this.projectId,
+          'panorama',
+          spaceId,
+          'aftercare',
+          {
+            imageType: type,
+            uploadSource: 'panorama_collection'
+          }
+        );
+
+        this.panoramaCollection.images.push({
+          id: uploadedFile.md5 || '',
+          spaceId,
+          spaceName: space.name || this.multiSpaceService.getSpaceTypeName(space.type),
+          url: uploadedFile.url || '',
+          uploadTime: new Date(),
+          uploadedBy: this.currentUser?.get('name') || '',
+          type,
+          description: ''
+        });
+      }
+
+      this.panoramaCollection.enabled = true;
+      this.cdr.markForCheck();
+
+    } catch (error) {
+      console.error('上传全景照片失败:', error);
     } finally {
       this.uploading = false;
     }
@@ -213,7 +491,7 @@ export class StageAftercareComponent implements OnInit {
    * 提交客户评价
    */
   async submitFeedback() {
-    if (this.customerFeedback.rating === 0) {
+    if (this.customerFeedback.overallRating === 0) {
       alert('请选择综合评分');
       return;
     }
@@ -248,7 +526,7 @@ export class StageAftercareComponent implements OnInit {
         title: this.project.get('title') || '',
         type: this.project.get('projectType') || '',
         duration: this.calculateProjectDuration(),
-        customerRating: this.customerFeedback.rating,
+        customerRating: this.customerFeedback.overallRating,
         challenges: this.extractChallenges()
       };
 
@@ -273,7 +551,16 @@ export class StageAftercareComponent implements OnInit {
           '建立快速响应机制',
           '增加可视化沟通工具',
           '完善项目管理流程'
-        ]
+        ],
+        spaceRetrospectives: this.isMultiSpaceProject
+          ? this.projectSpaces.map(space => ({
+              spaceId: space.id,
+              spaceName: space.name || this.multiSpaceService.getSpaceTypeName(space.type),
+              performance: this.customerFeedback.spaceFeedbacks.find(f => f.spaceId === space.id)?.rating || 0,
+              issues: [],
+              improvements: []
+            }))
+          : []
       };
 
       await this.saveDraft();
@@ -432,4 +719,168 @@ export class StageAftercareComponent implements OnInit {
     };
     return map[this.finalPayment.status] || 'medium';
   }
+
+  // ===== 工具方法 =====
+
+  /**
+   * 获取空间图标
+   */
+  getSpaceIcon(spaceType: string): string {
+    return this.multiSpaceService.getSpaceIcon(spaceType);
+  }
+
+  /**
+   * 获取空间类型名称
+   */
+  getSpaceTypeName(spaceType: string): string {
+    return this.multiSpaceService.getSpaceTypeName(spaceType);
+  }
+
+  /**
+   * 获取空间显示名称
+   */
+  getSpaceDisplayName(space: ProjectSpace): string {
+    return space.name || this.getSpaceTypeName(space.type);
+  }
+
+  /**
+   * 获取当前空间的评价
+   */
+  getCurrentSpaceFeedback(): any {
+    if (!this.activeSpaceId) return null;
+    return this.customerFeedback.spaceFeedbacks.find(f => f.spaceId === this.activeSpaceId);
+  }
+
+  /**
+   * 获取空间的投诉
+   */
+  getSpaceComplaints(spaceId?: string): any[] {
+    if (!spaceId) return this.complaints;
+    return this.complaints.filter(c => c.spaceId === spaceId);
+  }
+
+  /**
+   * 获取空间的全景照片
+   */
+  getSpacePanoramaImages(spaceId?: string): any[] {
+    if (!spaceId) return this.panoramaCollection.images;
+    return this.panoramaCollection.images.filter(img => img.spaceId === spaceId);
+  }
+
+  /**
+   * 获取投诉类型名称
+   */
+  getComplaintTypeName(type: string): string {
+    const typeMap: Record<string, string> = {
+      'quality': '质量问题',
+      'service': '服务问题',
+      'timeline': '进度问题',
+      'other': '其他问题'
+    };
+    return typeMap[type] || type;
+  }
+
+  /**
+   * 获取投诉严重程度名称
+   */
+  getComplaintSeverityName(severity: string): string {
+    const severityMap: Record<string, string> = {
+      'low': '轻微',
+      'medium': '一般',
+      'high': '严重'
+    };
+    return severityMap[severity] || severity;
+  }
+
+  /**
+   * 获取投诉状态名称
+   */
+  getComplaintStatusName(status: string): string {
+    const statusMap: Record<string, string> = {
+      'open': '待处理',
+      'processing': '处理中',
+      'resolved': '已解决'
+    };
+    return statusMap[status] || status;
+  }
+
+  /**
+   * 计算空间完成度
+   */
+  calculateSpaceCompletion(spaceId: string): number {
+    const spaceFeedback = this.customerFeedback.spaceFeedbacks.find(f => f.spaceId === spaceId);
+    if (!spaceFeedback || spaceFeedback.rating === 0) return 0;
+
+    const spaceComplaints = this.getSpaceComplaints(spaceId);
+    const unresolvedComplaints = spaceComplaints.filter(c => c.status !== 'resolved').length;
+
+    // 基于评分和投诉情况计算完成度
+    let completion = spaceFeedback.rating * 20; // 评分占100%
+    if (unresolvedComplaints > 0) {
+      completion -= unresolvedComplaints * 10; // 每个未解决投诉扣10分
+    }
+
+    return Math.max(0, Math.min(100, completion));
+  }
+
+  /**
+   * 计算整体项目完成度
+   */
+  calculateOverallCompletion(): number {
+    if (this.projectSpaces.length === 0) return 0;
+
+    const totalCompletion = this.projectSpaces.reduce((total, space) => {
+      return total + this.calculateSpaceCompletion(space.id);
+    }, 0);
+
+    return Math.round(totalCompletion / this.projectSpaces.length);
+  }
+
+  /**
+   * 获取待处理投诉数量
+   */
+  getOpenComplaintsCount(): number {
+    return this.complaints.filter(c => c.status === 'open').length;
+  }
+
+  /**
+   * 获取空间的支付凭证
+   */
+  getSpacePaymentVouchers(spaceId?: string): any[] {
+    if (!spaceId) return this.finalPayment.paymentVouchers;
+    return this.finalPayment.paymentVouchers.filter(v => v.spaceId === spaceId);
+  }
+
+  /**
+   * 格式化文件大小
+   */
+  formatFileSize(bytes: number): string {
+    return this.projectFileService.formatFileSize(bytes);
+  }
+
+  /**
+   * 生成项目报告摘要
+   */
+  generateProjectSummary(): string {
+    if (!this.projectRetrospective) return '';
+
+    const summary = [];
+    summary.push(`项目整体满意度:${this.customerFeedback.overallRating}/5星`);
+
+    if (this.isMultiSpaceProject) {
+      summary.push(`涉及${this.projectSpaces.length}个空间`);
+
+      const spaceRatings = this.customerFeedback.spaceFeedbacks.map(f =>
+        `${f.spaceName}:${f.rating}/5星`
+      );
+      if (spaceRatings.length > 0) {
+        summary.push(`各空间评分:${spaceRatings.join(',')}`);
+      }
+    }
+
+    summary.push(`尾款状态:${this.getPaymentStatusText()}`);
+    summary.push(`投诉记录:${this.complaints.length}条`);
+
+    return summary.join(' | ');
+  }
 }

+ 1 - 1
src/modules/project/pages/project-detail/stages/stage-delivery.component.html

@@ -73,7 +73,7 @@
                         @if (canEdit && isDesigner && deliverable.status === 'draft') {
                           <button
                             class="delete-button"
-                            (click)="deleteFile(group.spaceName, deliverable.processType, $index)">
+                            (click)="deleteFile(group.spaceName, deliverable.processType, file?.id)">
                             <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
                               <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144m224 0L144 368"/>
                             </svg>

+ 533 - 57
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -1,9 +1,11 @@
-import { Component, OnInit, Input, inject } from '@angular/core';
+import { Component, OnInit, Input, inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
 import { NovaUploadService } from 'fmode-ng/storage';
+import { ProjectFileService } from '../../../services/project-file.service';
+import { MultiSpaceService, ProjectSpace } from '../../../services/multi-space.service';
 
 const Parse = FmodeParse.with('nova');
 
@@ -21,7 +23,8 @@ const Parse = FmodeParse.with('nova');
   standalone: true,
   imports: [CommonModule, FormsModule],
   templateUrl: './stage-delivery.component.html',
-  styleUrls: ['./stage-delivery.component.scss']
+  styleUrls: ['./stage-delivery.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class StageDeliveryComponent implements OnInit {
   @Input() project: FmodeObject | null = null;
@@ -33,6 +36,15 @@ export class StageDeliveryComponent implements OnInit {
   cid: string = '';
   projectId: string = '';
 
+  // 多空间管理
+  projectSpaces: ProjectSpace[] = [];
+  isMultiSpaceProject: boolean = false;
+  activeSpaceId: string = '';
+  selectedSpaceIds: string[] = [];
+
+  // 交付视图切换
+  deliveryView: string = 'spaces'; // spaces | processes | timeline
+
   // 用户角色
   role: string = '';
   isDesigner: boolean = false;
@@ -40,11 +52,13 @@ export class StageDeliveryComponent implements OnInit {
 
   // 交付物列表(按空间+工序组织)
   deliverables: Array<{
+    id: string;
     spaceId: string;
     spaceName: string;
     processType: string; // modeling | softDecor | rendering | postProcess
     processName: string;
     files: Array<{
+      id: string;
       url: string;
       name: string;
       type: string; // image | video | document
@@ -53,6 +67,8 @@ export class StageDeliveryComponent implements OnInit {
         id: string;
         name: string;
       };
+      size?: number;
+      metadata?: any;
     }>;
     status: string; // draft | submitted | approved | rejected
     qualityCheck?: {
@@ -60,6 +76,7 @@ export class StageDeliveryComponent implements OnInit {
       items: Array<{
         label: string;
         passed: boolean;
+        notes?: string;
       }>;
     };
     review?: {
@@ -71,16 +88,50 @@ export class StageDeliveryComponent implements OnInit {
       result: string; // approved | rejected
       comments: string;
       issues: Array<{
+        id: string;
         description: string;
         priority: string; // high | medium | low
         status: string; // open | resolved
+        resolvedBy?: string;
+        resolvedTime?: Date;
       }>;
     };
+    dependencies?: string[]; // 依赖的其他交付物
+    timeline?: {
+      startDate?: Date;
+      dueDate?: Date;
+      completedDate?: Date;
+      estimatedHours?: number;
+      actualHours?: number;
+    };
+    batchId?: string; // 批次ID,用于批量操作
   }> = [];
 
   // 当前选中的空间和工序
-  selectedSpace: string = '';
-  selectedProcess: string = '';
+  selectedSpaceId: string = '';
+  selectedProcessType: string = '';
+
+  // 跨空间协调
+  crossSpaceDependencies: Array<{
+    id: string;
+    fromSpaceId: string;
+    toSpaceId: string;
+    fromProcess: string;
+    toProcess: string;
+    description: string;
+    status: string; // pending | completed | blocked
+  }> = [];
+
+  // 批次管理
+  deliveryBatches: Array<{
+    id: string;
+    name: string;
+    description: string;
+    deliverableIds: string[];
+    status: string;
+    createdAt: Date;
+    dueDate?: Date;
+  }> = [];
 
   // 质量自查清单模板
   qualityCheckTemplates = {
@@ -119,7 +170,10 @@ export class StageDeliveryComponent implements OnInit {
   private uploadService: NovaUploadService = inject(NovaUploadService);
 
   constructor(
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private projectFileService: ProjectFileService,
+    private multiSpaceService: MultiSpaceService,
+    private cdr: ChangeDetectorRef
   ) {}
 
   async ngOnInit() {
@@ -161,18 +215,16 @@ export class StageDeliveryComponent implements OnInit {
       this.isTeamLeader = this.role === '组长';
       this.canEdit = ['组员', '组长', '管理员'].includes(this.role);
 
-      // 加载交付物数据
+      // 加载项目空间数据
       if (this.project) {
-        const data = this.project.get('data') || {};
-
-        if (data.deliverables) {
-          this.deliverables = data.deliverables;
-        } else {
-          // 初始化交付物结构(基于报价明细)
-          this.initializeDeliverables();
-        }
+        await this.loadProjectSpaces();
+        await this.loadDeliverablesData();
+        await this.loadCrossSpaceDependencies();
+        await this.loadDeliveryBatches();
       }
 
+      this.cdr.markForCheck();
+
     } catch (err) {
       console.error('加载失败:', err);
     } finally {
@@ -180,14 +232,126 @@ export class StageDeliveryComponent implements OnInit {
     }
   }
 
+  /**
+   * 加载项目空间数据
+   */
+  async loadProjectSpaces(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      if(this.project?.id){  
+        this.projectSpaces = await this.multiSpaceService.getProjectSpaces(this.project.id);
+      }
+      this.isMultiSpaceProject = this.projectSpaces.length > 1;
+
+      // 如果有空间,默认选中第一个
+      if (this.projectSpaces.length > 0 && !this.activeSpaceId) {
+        this.activeSpaceId = this.projectSpaces[0].id;
+      }
+
+    } catch (error) {
+      console.error('加载项目空间失败:', error);
+    }
+  }
+
+  /**
+   * 加载交付物数据
+   */
+  async loadDeliverablesData(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      // 加载项目文件(交付物)
+      const projectFiles = await this.projectFileService.getProjectFiles(this.projectId, {
+        fileType: 'deliverable'
+      });
+
+      // 从项目数据加载交付物结构
+      const data = this.project.get('data') || {};
+      let savedDeliverables = data.deliverables || [];
+
+      // 如果没有保存的交付物,初始化结构
+      if (savedDeliverables.length === 0) {
+        savedDeliverables = this.initializeDeliverables();
+      }
+
+      // 合并文件数据
+      for (const deliverable of savedDeliverables) {
+        const deliverableFiles = projectFiles.filter((file: FmodeObject) =>
+          file.get('data')?.deliverableId === deliverable.id
+        );
+
+        deliverable.files = deliverableFiles.map((file: FmodeObject) => ({
+          id: file.id,
+          url: file.get('fileUrl'),
+          name: file.get('fileName'),
+          type: this.getFileType(file.get('fileName')),
+          uploadTime: file.createdAt,
+          uploadBy: {
+            id: file.get('uploadedBy')?.id,
+            name: file.get('uploadedBy')?.get('name')
+          },
+          size: file.get('fileSize'),
+          metadata: file.get('metadata')
+        }));
+      }
+
+      this.deliverables = savedDeliverables;
+
+    } catch (error) {
+      console.error('加载交付物数据失败:', error);
+    }
+  }
+
+  /**
+   * 加载跨空间依赖
+   */
+  async loadCrossSpaceDependencies(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      if(this.project?.id){
+        let dependencies
+        dependencies = await this.multiSpaceService.getSpaceDependencies(this.project.id);
+        
+        this.crossSpaceDependencies = dependencies.map((dep: any) => ({
+          id: dep.id,
+          fromSpaceId: dep.fromSpace?.objectId,
+          toSpaceId: dep.toSpace?.objectId,
+          fromProcess: dep.fromProcess || '',
+          toProcess: dep.toProcess || '',
+          description: dep.description,
+          status: dep.status
+        }));
+      }
+
+    } catch (error) {
+      console.error('加载跨空间依赖失败:', error);
+    }
+  }
+
+  /**
+   * 加载交付批次
+   */
+  async loadDeliveryBatches(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      const data = this.project.get('data') || {};
+      this.deliveryBatches = data.deliveryBatches || [];
+
+    } catch (error) {
+      console.error('加载交付批次失败:', error);
+    }
+  }
+
   /**
    * 初始化交付物结构
    */
   initializeDeliverables() {
     const data = this.project?.get('data') || {};
     const quotation = data.quotation;
-
-    if (!quotation) return;
+    const deliverables: any[] = [];
 
     const processTypeMap: any = {
       'modeling': '建模',
@@ -196,71 +360,202 @@ export class StageDeliveryComponent implements OnInit {
       'postProcess': '后期'
     };
 
-    for (const space of quotation.spaces) {
-      for (const [processKey, processData] of Object.entries(space.processes)) {
-        if ((processData as any).enabled) {
-          this.deliverables.push({
-            spaceId: space.name,
-            spaceName: space.name,
-            processType: processKey,
-            processName: processTypeMap[processKey],
-            files: [],
-            status: 'draft'
-          });
+    if (quotation && quotation.spaces) {
+      // 从报价单初始化
+      for (const space of quotation.spaces) {
+        for (const [processKey, processData] of Object.entries(space.processes || {})) {
+          if ((processData as any).enabled) {
+            deliverables.push(this.createDeliverable(
+              space.id || space.name,
+              space.name,
+              processKey,
+              processTypeMap[processKey]
+            ));
+          }
+        }
+      }
+    } else {
+      // 从项目空间初始化
+      for (const space of this.projectSpaces) {
+        // 为每个空间创建基础工序交付物
+        const defaultProcesses = ['modeling', 'softDecor', 'rendering', 'postProcess'];
+        for (const processType of defaultProcesses) {
+          deliverables.push(this.createDeliverable(
+            space.id,
+            space.name || this.multiSpaceService.getSpaceTypeName(space.type),
+            processType,
+            processTypeMap[processType]
+          ));
         }
       }
     }
+
+    return deliverables;
+  }
+
+  /**
+   * 创建交付物对象
+   */
+  private createDeliverable(spaceId: string, spaceName: string, processType: string, processName: string): any {
+    return {
+      id: `${spaceId}_${processType}_${Date.now()}`,
+      spaceId,
+      spaceName,
+      processType,
+      processName,
+      files: [],
+      status: 'draft',
+      qualityCheck: {
+        checked: false,
+        items: this.qualityCheckTemplates[processType as keyof typeof this.qualityCheckTemplates]?.map(item => ({ ...item })) || []
+      },
+      timeline: {
+        estimatedHours: this.getEstimatedHours(processType)
+      }
+    };
+  }
+
+  /**
+   * 获取预估工时
+   */
+  private getEstimatedHours(processType: string): number {
+    const hoursMap: Record<string, number> = {
+      'modeling': 8,
+      'softDecor': 6,
+      'rendering': 4,
+      'postProcess': 2
+    };
+    return hoursMap[processType] || 4;
+  }
+
+  /**
+   * 获取文件类型
+   */
+  private getFileType(fileName: string): string {
+    const extension = fileName.split('.').pop()?.toLowerCase();
+    if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension || '')) {
+      return 'image';
+    } else if (['mp4', 'mov', 'avi', 'mkv'].includes(extension || '')) {
+      return 'video';
+    } else {
+      return 'document';
+    }
+  }
+
+  // ===== 多空间交付管理方法 =====
+
+  /**
+   * 切换交付视图
+   */
+  onDeliveryViewChange(event: any): void {
+    this.deliveryView = event.detail.value;
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 选择空间
+   */
+  selectSpace(spaceId: string): void {
+    this.activeSpaceId = spaceId;
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 切换空间选择状态
+   */
+  toggleSpaceSelection(spaceId: string): void {
+    const index = this.selectedSpaceIds.indexOf(spaceId);
+    if (index > -1) {
+      this.selectedSpaceIds.splice(index, 1);
+    } else {
+      this.selectedSpaceIds.push(spaceId);
+    }
+    this.cdr.markForCheck();
   }
 
   /**
    * 获取指定空间和工序的交付物
    */
-  getDeliverable(spaceName: string, processType: string): any {
+  getDeliverable(spaceId: string, processType: string): any {
     return this.deliverables.find(
-      d => d.spaceName === spaceName && d.processType === processType
+      d => d.spaceId === spaceId && d.processType === processType
     );
   }
 
+  /**
+   * 获取空间的交付物
+   */
+  getSpaceDeliverables(spaceId?: string): any[] {
+    if (!spaceId) return this.deliverables;
+    return this.deliverables.filter(d => d.spaceId === spaceId);
+  }
+
+  /**
+   * 获取工序的交付物
+   */
+  getProcessDeliverables(processType: string): any[] {
+    return this.deliverables.filter(d => d.processType === processType);
+  }
+
   /**
    * 上传交付物文件
    */
-  async uploadFile(event: any, spaceName: string, processType: string) {
-    const file = event.target.files[0];
-    if (!file) return;
+  async uploadFile(event: any, spaceId: string, processType: string) {
+    const files = event.target.files;
+    if (!files || files.length === 0) return;
 
-    // 验证文件大小 (50MB)
-    if (file.size > 50 * 1024 * 1024) {
-      alert('文件大小不能超过50MB');
-      return;
-    }
+    const deliverable = this.getDeliverable(spaceId, processType);
+    if (!deliverable) return;
 
     try {
       this.uploading = true;
 
-      // 使用 NovaUploadService 上传文件
-      const fileResult: any = await this.uploadService.upload(file);
-      const url = fileResult.url;
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
 
-      const deliverable = this.getDeliverable(spaceName, processType);
-      if (deliverable) {
+        // 验证文件大小 (50MB)
+        if (file.size > 50 * 1024 * 1024) {
+          console.warn(`文件 ${file.name} 超过50MB限制,跳过`);
+          continue;
+        }
+
+        // 使用ProjectFileService上传文件
+        const uploadedFile = await this.projectFileService.uploadProjectFile(
+          file,
+          this.projectId,
+          'deliverable',
+          spaceId,
+          processType,
+          {
+            deliverableId: deliverable.id,
+            processType,
+            uploadSource: 'delivery'
+          }
+        );
+
+        // 添加到交付物文件列表
         deliverable.files.push({
-          url: url,
-          name: file.name,
-          type: file.type.startsWith('image/') ? 'image' : file.type.startsWith('video/') ? 'video' : 'document',
+          id: uploadedFile.md5,
+          url: uploadedFile.url,
+          name: uploadedFile.name,
+          type: this.getFileType(uploadedFile.name),
           uploadTime: new Date(),
           uploadBy: {
-            id: this.currentUser!.id,
-            name: this.currentUser!.get('name')
-          }
+            id: this.currentUser?.id,
+            name: this.currentUser?.get('name')
+          },
+          size: uploadedFile.size,
+          metadata: uploadedFile.metadata
         });
-
-        await this.saveDraft();
-        alert('上传成功');
       }
 
+      this.cdr.markForCheck();
+      // await this.saveDraft();
+      // alert('上传成功');
+
     } catch (err) {
       console.error('上传失败:', err);
-      alert('上传失败');
+      // alert('上传失败');
     } finally {
       this.uploading = false;
     }
@@ -269,11 +564,20 @@ export class StageDeliveryComponent implements OnInit {
   /**
    * 删除文件
    */
-  async deleteFile(spaceName: string, processType: string, fileIndex: number) {
-    const deliverable = this.getDeliverable(spaceName, processType);
-    if (deliverable) {
-      deliverable.files.splice(fileIndex, 1);
-      await this.saveDraft();
+  async deleteFile(spaceId: string, processType: string, fileId: string) {
+    try {
+      // 从数据库删除
+      await this.projectFileService.deleteProjectFile(fileId);
+
+      const deliverable = this.getDeliverable(spaceId, processType);
+      if (deliverable) {
+        deliverable.files = deliverable.files.filter((file: any) => file.id !== fileId);
+        this.cdr.markForCheck();
+      }
+
+      // await this.saveDraft();
+    } catch (error) {
+      console.error('删除文件失败:', error);
     }
   }
 
@@ -422,6 +726,7 @@ export class StageDeliveryComponent implements OnInit {
 
       const data = this.project.get('data') || {};
       data.deliverables = this.deliverables;
+      data.deliveryBatches = this.deliveryBatches;
 
       this.project.set('data', data);
       await this.project.save();
@@ -430,6 +735,50 @@ export class StageDeliveryComponent implements OnInit {
       console.error('保存失败:', err);
     } finally {
       this.saving = false;
+      this.cdr.markForCheck();
+    }
+  }
+
+  // ===== 批次管理方法 =====
+
+  /**
+   * 创建交付批次
+   */
+  async createDeliveryBatch(name: string, description: string): Promise<void> {
+    const batch = {
+      id: `batch_${Date.now()}`,
+      name,
+      description,
+      deliverableIds: [...this.selectedSpaceIds],
+      status: 'draft',
+      createdAt: new Date()
+    };
+
+    this.deliveryBatches.push(batch);
+    this.selectedSpaceIds = [];
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 批量提交交付物
+   */
+  async batchSubmitDeliverables(batchId: string): Promise<void> {
+    const batch = this.deliveryBatches.find(b => b.id === batchId);
+    if (!batch) return;
+
+    try {
+      for (const deliverableId of batch.deliverableIds) {
+        const deliverable = this.deliverables.find(d => d.id === deliverableId);
+        if (deliverable && deliverable.status === 'draft') {
+          await this.submitForReview(deliverable.spaceId, deliverable.processType);
+        }
+      }
+
+      batch.status = 'submitted';
+      this.cdr.markForCheck();
+
+    } catch (error) {
+      console.error('批量提交失败:', error);
     }
   }
 
@@ -513,4 +862,131 @@ export class StageDeliveryComponent implements OnInit {
     const approvedCount = this.deliverables.filter(d => d.status === 'approved').length;
     return Math.round((approvedCount / this.deliverables.length) * 100);
   }
+
+  // ===== 工具方法 =====
+
+  /**
+   * 获取空间图标
+   */
+  getSpaceIcon(spaceType: string): string {
+    return this.multiSpaceService.getSpaceIcon(spaceType);
+  }
+
+  /**
+   * 获取空间类型名称
+   */
+  getSpaceTypeName(spaceType: string): string {
+    return this.multiSpaceService.getSpaceTypeName(spaceType);
+  }
+
+  /**
+   * 获取空间显示名称
+   */
+  getSpaceDisplayName(space: ProjectSpace): string {
+    return space.name || this.getSpaceTypeName(space.type);
+  }
+
+  /**
+   * 获取当前空间的交付物
+   */
+  getCurrentSpaceDeliverables(): any[] {
+    if (!this.activeSpaceId) return this.deliverables;
+    return this.deliverables.filter(d => d.spaceId === this.activeSpaceId);
+  }
+
+  /**
+   * 过滤交付物
+   */
+  getFilteredDeliverables(): any[] {
+    let filtered = this.deliverables;
+
+    if (this.deliveryView === 'spaces' && this.activeSpaceId) {
+      filtered = filtered.filter(d => d.spaceId === this.activeSpaceId);
+    }
+
+    if (this.deliveryView === 'processes' && this.selectedProcessType) {
+      filtered = filtered.filter(d => d.processType === this.selectedProcessType);
+    }
+
+    return filtered;
+  }
+
+  /**
+   * 格式化文件大小
+   */
+  formatFileSize(bytes: number): string {
+    return this.projectFileService.formatFileSize(bytes);
+  }
+
+  /**
+   * 计算空间完成进度
+   */
+  getSpaceProgress(spaceId: string): number {
+    const spaceDeliverables = this.getSpaceDeliverables(spaceId);
+    if (spaceDeliverables.length === 0) return 0;
+    const approvedCount = spaceDeliverables.filter(d => d.status === 'approved').length;
+    return Math.round((approvedCount / spaceDeliverables.length) * 100);
+  }
+
+  /**
+   * 计算工序完成进度
+   */
+  getProcessProgress(processType: string): number {
+    const processDeliverables = this.getProcessDeliverables(processType);
+    if (processDeliverables.length === 0) return 0;
+    const approvedCount = processDeliverables.filter(d => d.status === 'approved').length;
+    return Math.round((approvedCount / processDeliverables.length) * 100);
+  }
+
+  /**
+   * 获取待处理问题数量
+   */
+  getOpenIssuesCount(): number {
+    return this.deliverables.reduce((total, d) => {
+      return total + (d.review?.issues?.filter(issue => issue.status === 'open').length || 0);
+    }, 0);
+  }
+
+  /**
+   * 检查跨空间依赖
+   */
+  checkCrossSpaceDependencies(deliverableId: string): string[] {
+    const dependencies: string[] = [];
+
+    for (const dep of this.crossSpaceDependencies) {
+      // 这里可以根据实际业务逻辑检查依赖关系
+      if (dep.status === 'pending') {
+        dependencies.push(dep.description);
+      }
+    }
+
+    return dependencies;
+  }
+
+  /**
+   * 获取交付优先级
+   */
+  getDeliverablePriority(deliverable: any): 'high' | 'medium' | 'low' {
+    // 根据依赖关系和工序重要性计算优先级
+    const hasDependencies = this.checkCrossSpaceDependencies(deliverable.id).length > 0;
+
+    if (hasDependencies) return 'high';
+    if (deliverable.processType === 'modeling') return 'high';
+    if (deliverable.processType === 'softDecor') return 'medium';
+    return 'low';
+  }
+
+  /**
+   * 获取预计完成时间
+   */
+  getEstimatedCompletionDate(deliverable: any): Date | null {
+    if (!deliverable.timeline?.estimatedHours) return null;
+
+    const startDate = deliverable.timeline.startDate || new Date();
+    const workingDays = Math.ceil(deliverable.timeline.estimatedHours / 8); // 假设每天8小时
+    const completionDate = new Date(startDate);
+    completionDate.setDate(completionDate.getDate() + workingDays);
+
+    return completionDate;
+  }
 }

+ 598 - 117
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -1,8 +1,10 @@
-import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
+import { Component, OnInit, Input, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { FmodeObject, FmodeParse, NovaStorage, NovaFile } from 'fmode-ng/core';
+import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
+import { ProjectFileService } from '../../../services/project-file.service';
+import { MultiSpaceService, ProjectSpace } from '../../../services/multi-space.service';
 import {
   QUOTATION_PRICE_TABLE,
   STYLE_LEVELS,
@@ -19,20 +21,23 @@ import { QuotationEditorComponent } from '../../../components/quotation-editor.c
 const Parse = FmodeParse.with('nova');
 
 /**
- * 订单分配阶段组件
+ * 订单分配阶段组件 - 支持多空间项目管理
  *
  * 功能:
- * 1. 家装/工装项目类型选择
- * 2. 快速场景选择,自动生成报价表(基于quotation.md规则)
- * 3. 项目组(Department)→ 组员(Profile)两级设计师分配
- * 4. 提交审批
+ * 1. 智能识别多空间项目类型
+ * 2. 按空间维度组织报价和管理
+ * 3. 家装/工装项目类型选择
+ * 4. 快速场景选择,自动生成多空间报价表
+ * 5. 项目组(Department)→ 组员(Profile)两级设计师分配
+ * 6. 提交审批
  */
 @Component({
   selector: 'app-stage-order',
   standalone: true,
   imports: [CommonModule, FormsModule, QuotationEditorComponent],
   templateUrl: './stage-order.component.html',
-  styleUrls: ['./stage-order.component.scss']
+  styleUrls: ['./stage-order.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class StageOrderComponent implements OnInit {
   @Input() project: FmodeObject | null = null;
@@ -40,6 +45,9 @@ export class StageOrderComponent implements OnInit {
   @Input() currentUser: FmodeObject | null = null;
   @Input() canEdit: boolean = false;
 
+  onProjectTypeChange(){
+
+  }
   // 项目基本信息
   projectInfo = {
     title: '',
@@ -47,25 +55,37 @@ export class StageOrderComponent implements OnInit {
     renderType: '', // 静态单张 | 360全景
     deadline: '',
     description: '',
-    priceLevel: '一级' // 一级(老客户) | 二级(中端组) | 三级(高端组)
+    priceLevel: '一级', // 一级(老客户) | 二级(中端组) | 三级(高端组)
+    spaceType: 'single' // single | multi
+  };
+
+  // 空间管理
+  projectSpaces: ProjectSpace[] = [];
+  isMultiSpaceProject: boolean = false;
+  activeSpaceId: string = '';
+
+  // 空间类型映射
+  spaceTypeMap:any = {
+    'living_room': '客厅',
+    'bedroom': '卧室',
+    'kitchen': '厨房',
+    'bathroom': '卫生间',
+    'dining_room': '餐厅',
+    'study': '书房',
+    'balcony': '阳台',
+    'corridor': '走廊',
+    'storage': '储物间',
+    'entrance': '玄关',
+    'other': '其他'
   };
 
-  hasSpacesDisabled(){
-    return !this.canEdit || !this.commercialScenes?.spaces?.some(s => s?.selected)
-  }
-  hasRoomsDisabled(){
-    return !this.canEdit || !this.homeScenes?.rooms?.some((r:any) => r?.selected)
-  }
-  getLevels(levels:any){
-    if(!levels) return []
-    return Object.keys(levels)
-  }
   // 场景选择(家装)
   homeScenes = {
     spaceType: '', // 平层 | 跃层 | 挑空
     styleLevel: '', // 基础风格组 | 中级风格组 | 高级风格组 | 顶级风格组
     rooms: [] as Array<{
-      name: string; // 客厅、主卧、次卧等
+      name: string;
+      spaceType: string;
       selected: boolean;
       basePrice: number;
       adjustments: {
@@ -93,10 +113,11 @@ export class StageOrderComponent implements OnInit {
     }>
   };
 
-  // 报价明细(最终生成)
+  // 报价明细(最终生成)- 支持多空间
   quotation = {
     spaces: [] as Array<{
       name: string;
+      spaceId?: string;
       processes: {
         modeling: { enabled: boolean; price: number; unit: string; quantity: number };
         softDecor: { enabled: boolean; price: number; unit: string; quantity: number };
@@ -105,7 +126,13 @@ export class StageOrderComponent implements OnInit {
       };
       subtotal: number;
     }>,
-    total: 0
+    total: 0,
+    spaceBreakdown: [] as Array<{
+      spaceName: string;
+      spaceId: string;
+      amount: number;
+      percentage: number;
+    }>
   };
 
   // 工序类型定义
@@ -138,6 +165,7 @@ export class StageOrderComponent implements OnInit {
   saving: boolean = false;
   loadingMembers: boolean = false;
   loadingTeams: boolean = false;
+  loadingSpaces: boolean = false;
 
   // 路由参数
   cid: string = '';
@@ -155,39 +183,32 @@ export class StageOrderComponent implements OnInit {
   @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
   @ViewChild('dropZone') dropZone!: ElementRef<HTMLDivElement>;
 
-  private storage: NovaStorage | null = null;
   isUploading: boolean = false;
   uploadProgress: number = 0;
-  projectFiles: Array<{
+  projectFiles: Array<any|{
     id: string;
     name: string;
     url: string;
     type: string;
     size: number;
+    fileType: string;
     uploadedBy: string;
     uploadedAt: Date;
+    spaceId?: string;
   }> = [];
 
   // 企业微信拖拽相关
   dragOver: boolean = false;
   wxFileDropSupported: boolean = false;
 
-  constructor(private route: ActivatedRoute) {
-    this.initStorage();
+  constructor(
+    private route: ActivatedRoute,
+    private projectFileService: ProjectFileService,
+    private multiSpaceService: MultiSpaceService
+  ) {
     this.checkWxWorkSupport();
   }
 
-  // 初始化 NovaStorage
-  private async initStorage(): Promise<void> {
-    try {
-      const cid = localStorage.getItem('company') || this.cid || 'cDL6R1hgSi';
-      this.storage = await NovaStorage.withCid(cid);
-      console.log('✅ Stage-order NovaStorage 初始化成功, cid:', cid);
-    } catch (error) {
-      console.error('❌ Stage-order NovaStorage 初始化失败:', error);
-    }
-  }
-
   // 检查企业微信拖拽支持
   private checkWxWorkSupport(): void {
     // 检查是否在企业微信环境中
@@ -244,14 +265,16 @@ export class StageOrderComponent implements OnInit {
 
   // 处理企业微信文件拖拽
   private async handleWxWorkFileDrop(event: DragEvent): Promise<void> {
-    if (!this.project || !this.storage) return;
+    if (!this.project) return;
 
     const files = Array.from(event.dataTransfer?.files || []);
     if (files.length === 0) return;
 
     console.log('🎯 接收到企业微信拖拽文件:', files.map(f => f.name));
 
-    await this.uploadFiles(files, '企业微信拖拽');
+    // 上传到当前选中的空间,如果没有选中则上传到项目根目录
+    const spaceId = this.activeSpaceId || '';
+    await this.uploadFiles(files, '企业微信拖拽', spaceId);
   }
 
   async ngOnInit() {
@@ -299,12 +322,22 @@ export class StageOrderComponent implements OnInit {
         const data = this.project.get('data') || {};
         if (data.quotation) {
           this.quotation = data.quotation;
+          // 检查是否为多空间项目
+          this.isMultiSpaceProject = this.quotation.spaces.length > 1;
+          this.projectInfo.spaceType = this.isMultiSpaceProject ? 'multi' : 'single';
         }
 
         if (data.priceLevel) {
           this.projectInfo.priceLevel = data.priceLevel;
         }
 
+        if (data.spaceType) {
+          this.projectInfo.spaceType = data.spaceType;
+        }
+
+        // 加载项目空间
+        await this.loadProjectSpaces();
+
         // 加载场景选择数据
         if (data.homeScenes) {
           this.homeScenes = data.homeScenes;
@@ -349,9 +382,94 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 项目类型改变,初始化场景数据
+   * 加载项目空间
+   */
+  async loadProjectSpaces(): Promise<void> {
+    if (!this.project) return;
+
+    try {
+      this.loadingSpaces = true;
+
+      // 从ProjectSpace表加载空间数据
+      this.projectSpaces = await this.multiSpaceService.getProjectSpaces(this.project.id || '');
+
+      // 如果没有空间数据,但从项目数据中有报价信息,则转换创建默认空间
+      if (this.projectSpaces.length === 0) {
+        const data = this.project.get('data') || {};
+        if (data.quotation?.spaces) {
+          await this.createSpacesFromQuotation(data.quotation.spaces);
+        }
+      }
+
+      // 设置默认选中第一个空间
+      if (this.projectSpaces.length > 0 && !this.activeSpaceId) {
+        this.activeSpaceId = this.projectSpaces[0].id;
+      }
+
+    } catch (error) {
+      console.error('加载项目空间失败:', error);
+    } finally {
+      this.loadingSpaces = false;
+    }
+  }
+
+  /**
+   * 从报价数据创建空间记录
+   */
+  private async createSpacesFromQuotation(quotationSpaces: any[]): Promise<void> {
+    for (const spaceData of quotationSpaces) {
+      try {
+        const space: Partial<ProjectSpace> = {
+          name: spaceData.name,
+          type: this.inferSpaceType(spaceData.name),
+          priority: 5,
+          status: 'not_started',
+          complexity: 'medium',
+          estimatedBudget: this.calculateSpaceRate(spaceData),
+          order: quotationSpaces.indexOf(spaceData)
+        };
+
+        await this.multiSpaceService.createSpace(this.project!.id || '', space);
+      } catch (error) {
+        console.error(`创建空间 ${spaceData.name} 失败:`, error);
+      }
+    }
+
+    // 重新加载空间列表
+    await this.loadProjectSpaces();
+  }
+
+  /**
+   * 推断空间类型
+   */
+  private inferSpaceType(spaceName: string): string {
+    const lowerName = spaceName.toLowerCase();
+    for (const [type, name] of Object.entries(this.spaceTypeMap)) {
+      if (typeof name == "string" && lowerName.includes(name)) {
+        return type;
+      }
+    }
+    return 'other';
+  }
+
+  /**
+   * 计算空间预算
    */
-  onProjectTypeChange() {
+  private calculateSpaceRate(spaceData: any): number {
+    let total = 0;
+    for (const process of Object.values(spaceData.processes || {})) {
+      const proc = process as any;
+      if (proc.enabled) {
+        total += proc.price * proc.quantity;
+      }
+    }
+    return total;
+  }
+
+  /**
+   * 切换空间类型
+   */
+  onSpaceTypeChange() {
     this.quotation.spaces = [];
     this.quotation.total = 0;
 
@@ -371,6 +489,180 @@ export class StageOrderComponent implements OnInit {
     }
   }
 
+  /**
+   * 项目空间模式改变(单空间/多空间)
+   */
+  async onProjectSpaceModeChange() {
+    this.isMultiSpaceProject = this.projectInfo.spaceType === 'multi';
+
+    if (!this.isMultiSpaceProject) {
+      // 单空间模式:重置为第一个空间
+      if (this.projectSpaces.length > 0) {
+        this.activeSpaceId = this.projectSpaces[0].id;
+      }
+      this.quotation.spaces = this.quotation.spaces.slice(0, 1);
+      this.calculateTotal();
+    } else {
+      // 多空间模式:恢复所有空间
+      await this.regenerateQuotationFromSpaces();
+    }
+  }
+
+  /**
+   * 添加新空间
+   */
+  async addSpace() {
+    const spaceName = prompt('请输入空间名称:');
+    if (!spaceName) return;
+
+    try {
+      const spaceData: Partial<ProjectSpace> = {
+        name: spaceName,
+        type: 'other',
+        priority: 5,
+        status: 'not_started',
+        complexity: 'medium',
+        order: this.projectSpaces.length
+      };
+
+      const createdSpace = await this.multiSpaceService.createSpace(this.projectId!, spaceData);
+      this.projectSpaces.push(createdSpace);
+
+      // 如果是第一个空间,设置为当前空间
+      if (this.projectSpaces.length === 1) {
+        this.activeSpaceId = createdSpace.id;
+      }
+
+      // 重新生成报价
+      await this.regenerateQuotationFromSpaces();
+
+      alert('空间添加成功');
+    } catch (error) {
+      console.error('添加空间失败:', error);
+      alert('添加失败,请重试');
+    }
+  }
+
+  /**
+   * 编辑空间
+   */
+  async editSpace(spaceId: string) {
+    const space = this.projectSpaces.find(s => s.id === spaceId);
+    if (!space) return;
+
+    const newName = prompt('修改空间名称:', space.name);
+    if (!newName || newName === space.name) return;
+
+    try {
+      await this.multiSpaceService.updateSpace(spaceId, { name: newName });
+      space.name = newName;
+
+      // 更新报价中的空间名称
+      const quotationSpace = this.quotation.spaces.find(s => s.spaceId === spaceId);
+      if (quotationSpace) {
+        quotationSpace.name = newName;
+      }
+
+      // 保存到项目数据
+      await this.saveDraft();
+
+      alert('空间更新成功');
+    } catch (error) {
+      console.error('更新空间失败:', error);
+      alert('更新失败,请重试');
+    }
+  }
+
+  /**
+   * 删除空间
+   */
+  async deleteSpace(spaceId: string) {
+    if (!confirm('确定要删除这个空间吗?相关数据将被清除。')) return;
+
+    try {
+      await this.multiSpaceService.deleteSpace(spaceId);
+
+      // 从本地列表中移除
+      this.projectSpaces = this.projectSpaces.filter(s => s.id !== spaceId);
+
+      // 如果删除的是当前空间,切换到第一个空间
+      if (this.activeSpaceId === spaceId && this.projectSpaces.length > 0) {
+        this.activeSpaceId = this.projectSpaces[0].id;
+      }
+
+      // 重新生成报价
+      await this.regenerateQuotationFromSpaces();
+
+      alert('空间删除成功');
+    } catch (error) {
+      console.error('删除空间失败:', error);
+      alert('删除失败,请重试');
+    }
+  }
+
+  /**
+   * 从空间重新生成报价
+   */
+  private async regenerateQuotationFromSpaces(): Promise<void> {
+    this.quotation.spaces = [];
+
+    // 为每个空间生成报价
+    for (const space of this.projectSpaces) {
+      if (this.projectInfo.projectType === '家装') {
+        const roomData = this.homeScenes.rooms.find(r => r.name === space.name);
+        if (roomData) {
+          const finalPrice = calculateFinalPrice(
+            roomData.basePrice,
+            '家装',
+            roomData.adjustments
+          );
+
+          const processes = getDefaultProcesses('家装', finalPrice);
+
+          this.quotation.spaces.push({
+            name: space.name,
+            spaceId: space.id,
+            processes: processes as any,
+            subtotal: finalPrice + processes.postProcess.price
+          });
+        }
+      } else if (this.projectInfo.projectType === '工装') {
+        const spaceData = this.commercialScenes.spaces.find(s => s.name === space.name);
+        if (spaceData) {
+          const finalPrice = calculateFinalPrice(
+            spaceData.basePrice,
+            '工装',
+            spaceData.adjustments
+          );
+
+          const processes = getDefaultProcesses('工装', finalPrice);
+
+          this.quotation.spaces.push({
+            name: space.name,
+            spaceId: space.id,
+            processes: processes as any,
+            subtotal: finalPrice + processes.postProcess.price
+          });
+        }
+      }
+    }
+
+    this.calculateTotal();
+    this.updateSpaceBreakdown();
+  }
+
+  /**
+   * 更新空间占比
+   */
+  private updateSpaceBreakdown(): void {
+    this.quotation.spaceBreakdown = this.quotation.spaces.map(space => ({
+      spaceName: space.name,
+      spaceId: space.spaceId || '',
+      amount: space.subtotal,
+      percentage: this.quotation.total > 0 ? Math.round((space.subtotal / this.quotation.total) * 100) : 0
+    }));
+  }
+
   /**
    * 家装场景:空间类型和风格等级改变,生成预设房间列表
    */
@@ -395,6 +687,7 @@ export class StageOrderComponent implements OnInit {
 
       return {
         name,
+        spaceType,
         selected: false,
         basePrice,
         adjustments: {
@@ -472,11 +765,7 @@ export class StageOrderComponent implements OnInit {
         const finalPrice = calculateFinalPrice(
           room.basePrice,
           '家装',
-          {
-            extraFunction: room.adjustments.extraFunction,
-            complexity: room.adjustments.complexity,
-            design: room.adjustments.design
-          }
+          room.adjustments
         );
 
         // 使用配置文件生成默认工序
@@ -484,6 +773,7 @@ export class StageOrderComponent implements OnInit {
 
         this.quotation.spaces.push({
           name: room.name,
+          spaceId: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
           processes: processes as any,
           subtotal: finalPrice + processes.postProcess.price
         });
@@ -494,18 +784,14 @@ export class StageOrderComponent implements OnInit {
         const finalPrice = calculateFinalPrice(
           space.basePrice,
           '工装',
-          {
-            extraFunction: space.adjustments.extraFunction,
-            complexity: space.adjustments.complexity,
-            design: space.adjustments.design,
-            panoramic: space.adjustments.panoramic
-          }
+          space.adjustments
         );
 
         const processes = getDefaultProcesses('工装', finalPrice);
 
         this.quotation.spaces.push({
           name: space.name,
+          spaceId: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
           processes: processes as any,
           subtotal: finalPrice + processes.postProcess.price
         });
@@ -513,6 +799,7 @@ export class StageOrderComponent implements OnInit {
     }
 
     this.calculateTotal();
+    this.updateSpaceBreakdown();
   }
 
   /**
@@ -531,16 +818,61 @@ export class StageOrderComponent implements OnInit {
     this.quotation.total = total;
   }
 
+  /**
+   * 获取空间进度
+   */
+  getSpaceProgress(spaceId: string): number {
+    if (!spaceId || !this.isMultiSpaceProject) return 0;
+    return this.multiSpaceService.calculateSpaceProgress(spaceId, this.processTypes.map(p => p.key));
+  }
+
+  /**
+   * 获取空间状态
+   */
+  getSpaceStatus(space: ProjectSpace): string {
+    if (!space) return 'unknown';
+    return space.status;
+  }
+
+  /**
+   * 获取空间状态颜色
+   */
+  getSpaceStatusColor(status: string): string {
+    const colorMap: Record<string, string> = {
+      'not_started': 'medium',
+      'in_progress': 'warning',
+      'awaiting_review': 'info',
+      'completed': 'success',
+      'blocked': 'danger',
+      'delayed': 'danger'
+    };
+    return colorMap[status] || 'medium';
+  }
+
+  /**
+   * 获取空间状态文本
+   */
+  getSpaceStatusText(status: string): string {
+    const textMap: Record<string, string> = {
+      'not_started': '未开始',
+      'in_progress': '进行中',
+      'awaiting_review': '待审核',
+      'completed': '已完成',
+      'blocked': '已阻塞',
+      'delayed': '已延期'
+    };
+    return textMap[status] || status;
+  }
+
   /**
    * 选择项目组(Department)
    */
   async selectDepartment(department: FmodeObject) {
-    if (this.canEdit || this.project?.get('assignee')?.id){
-      
+    if (this.canEdit || this.project?.get('assignee')?.id) {
       this.selectedDepartment = department;
       this.selectedDesigner = null;
       this.departmentMembers = [];
-      
+
       await this.loadDepartmentMembers(department);
     }
   }
@@ -550,7 +882,7 @@ export class StageOrderComponent implements OnInit {
    */
   async loadDepartmentMembers(department: FmodeObject) {
     let departmentId = department.id
-    if(!departmentId) return []
+    if (!departmentId) return []
     try {
       this.loadingMembers = true;
 
@@ -769,6 +1101,7 @@ export class StageOrderComponent implements OnInit {
    */
   onQuotationChange(updatedQuotation: any) {
     this.quotation = updatedQuotation;
+    this.updateSpaceBreakdown();
   }
 
   /**
@@ -776,6 +1109,7 @@ export class StageOrderComponent implements OnInit {
    */
   onTotalChange(total: number) {
     this.quotation.total = total;
+    this.updateSpaceBreakdown();
   }
 
   /**
@@ -792,6 +1126,7 @@ export class StageOrderComponent implements OnInit {
       this.project.set('renderType', this.projectInfo.renderType);
       this.project.set('deadline', this.projectInfo.deadline);
       this.project.set('description', this.projectInfo.description);
+      this.project.set('spaceType', this.projectInfo.spaceType);
 
       const data = this.project.get('data') || {};
       data.quotation = this.quotation;
@@ -908,22 +1243,18 @@ export class StageOrderComponent implements OnInit {
     if (!this.project) return;
 
     try {
-      const query = new Parse.Query('ProjectFile');
-      query.equalTo('project', this.project.toPointer());
-      query.include('uploadedBy');
-      query.descending('uploadedAt');
-      query.limit(50);
-
-      const files = await query.find();
-
-      this.projectFiles = files.map((file: FmodeObject) => ({
-        id: file.id || '',
-        name: file.get('name') || file.get('originalName') || '',
+      // 使用ProjectFileService加载项目文件
+      const files = await this.projectFileService.getProjectFiles(this.projectId);
+      this.projectFiles = files.map(file => ({
+        id: file.id,
+        name: file.get('name') || '',
         url: file.get('url') || '',
         type: file.get('type') || '',
         size: file.get('size') || 0,
-        uploadedBy: file.get('uploadedBy')?.get('name') || '未知用户',
-        uploadedAt: file.get('uploadedAt') || file.createdAt
+        fileType: file.get('fileType') || '',
+        uploadedBy: file.get('uploadedBy') || '',
+        uploadedAt: file.get('uploadedAt') || new Date(),
+        spaceId: file.get('spaceId') || undefined
       }));
 
       console.log(`✅ 加载了 ${this.projectFiles.length} 个项目文件`);
@@ -943,12 +1274,14 @@ export class StageOrderComponent implements OnInit {
   /**
    * 处理文件选择
    */
-  onFileSelect(event: Event): void {
+  async onFileSelect(event: Event): Promise<void> {
     const target = event.target as HTMLInputElement;
     const files = Array.from(target.files || []);
 
     if (files.length > 0) {
-      this.uploadFiles(files, '手动选择');
+      // 上传到当前选中的空间,如果没有选中则上传到项目根目录
+      const spaceId = this.activeSpaceId || '';
+      await this.uploadFiles(files, '手动选择', spaceId);
     }
 
     // 清空input值,允许重复选择同一文件
@@ -958,8 +1291,12 @@ export class StageOrderComponent implements OnInit {
   /**
    * 上传文件到 ProjectFile 表
    */
-  private async uploadFiles(files: File[], source: string): Promise<void> {
-    if (!this.project || !this.storage || !this.currentUser) {
+  private async uploadFiles(
+    files: File[],
+    source: string,
+    spaceId?: string
+  ): Promise<void> {
+    if (!this.project || !this.currentUser) {
       console.error('❌ 缺少必要信息,无法上传文件');
       return;
     }
@@ -977,50 +1314,54 @@ export class StageOrderComponent implements OnInit {
         this.uploadProgress = ((i + 1) / files.length) * 100;
 
         try {
-          // 使用 NovaStorage 上传文件,指定项目路径前缀
-          const uploaded: NovaFile = await this.storage.upload(file, {
-            prefixKey: `projects/${this.projectId}/`,
-            onProgress: (p) => {
-              const fileProgress = (i / files.length) * 100 + (p.total.percent / files.length);
+          // 使用 ProjectFileService 上传文件
+          const uploadedFile = await this.projectFileService.uploadProjectFile(
+            file,
+            this.projectId,
+            'order',
+            spaceId,
+            undefined,
+            {
+              source,
+              uploadTime: new Date(),
+              uploader: this.currentUser.get('name')
+            },
+            (progress) => {
+              const fileProgress = (i / files.length) * 100 + (progress / files.length);
               this.uploadProgress = fileProgress;
             }
-          });
+          );
 
-          // 保存文件信息到 ProjectFile 表
-          const projectFile = new Parse.Object('ProjectFile');
-          projectFile.set('project', this.project.toPointer());
-          projectFile.set('name', file.name);
-          projectFile.set('originalName', file.name);
-          projectFile.set('url', uploaded.url);
-          projectFile.set('key', uploaded.key);
-          projectFile.set('type', file.type);
-          projectFile.set('size', file.size);
-          projectFile.set('uploadedBy', this.currentUser.toPointer());
-          projectFile.set('uploadedAt', new Date());
-          projectFile.set('source', source); // 标记来源:企业微信拖拽或手动选择
-          projectFile.set('md5', uploaded.md5);
-          projectFile.set('metadata', uploaded.metadata);
-
-          const savedFile = await projectFile.save();
+          // 保存文件信息到 Attachment表和ProjectFile表
+          const projectFile = await this.projectFileService.saveToProjectFile(
+            uploadedFile.attachment,
+            this.projectId,
+            'order',
+            spaceId,
+            undefined
+          );
 
           // 添加到本地列表
-          this.projectFiles.unshift({
-            id: savedFile.id || '',
+          const fileData = {
+            id: projectFile.id || '',
             name: file.name,
-            url: uploaded.url || '',
+            url: uploadedFile.url,
             type: file.type,
             size: file.size,
-            uploadedBy: this.currentUser.get('name'),
-            uploadedAt: new Date()
-          });
+            fileType: this.projectFileService.getFileTypeLabel(file.type),
+            uploadedBy: this.currentUser.get('name') || '',
+            uploadedAt: new Date(),
+            spaceId: spaceId || ''
+          };
 
+          this.projectFiles.unshift(fileData);
           results.push({
             success: true,
-            file: uploaded,
+            file: uploadedFile,
             name: file.name
           });
 
-          console.log('✅ 文件上传成功:', file.name, uploaded.key);
+          console.log('✅ 文件上传成功:', file.name, uploadedFile.key);
 
         } catch (error) {
           console.error('❌ 文件上传失败:', file.name, error);
@@ -1038,10 +1379,8 @@ export class StageOrderComponent implements OnInit {
 
       if (failCount === 0) {
         console.log(`🎉 所有 ${successCount} 个文件上传成功`);
-        // 可以显示成功提示
       } else {
         console.warn(`⚠️ ${successCount} 个文件成功,${failCount} 个文件失败`);
-        // 可以显示部分失败的提示
       }
 
     } catch (error) {
@@ -1067,7 +1406,7 @@ export class StageOrderComponent implements OnInit {
     this.dragOver = false;
   }
 
-  onDrop(event: DragEvent): void {
+  async onDrop(event: DragEvent): Promise<void> {
     event.preventDefault();
     event.stopPropagation();
     this.dragOver = false;
@@ -1077,7 +1416,9 @@ export class StageOrderComponent implements OnInit {
     const files = Array.from(event.dataTransfer?.files || []);
     if (files.length > 0) {
       console.log('🎯 接收到拖拽文件:', files.map(f => f.name));
-      this.uploadFiles(files, '拖拽上传');
+      // 上传到当前选中的空间,如果没有选中则上传到项目根目录
+      const spaceId = this.activeSpaceId || '';
+      await this.uploadFiles(files, '拖拽上传', spaceId);
     }
   }
 
@@ -1088,17 +1429,12 @@ export class StageOrderComponent implements OnInit {
     if (!this.canEdit) return;
 
     try {
-      const query = new Parse.Query('ProjectFile');
-      const file = await query.get(fileId);
-
-      if (file) {
-        await file.destroy();
+      await this.projectFileService.deleteProjectFile(fileId);
 
-        // 从本地列表中移除
-        this.projectFiles = this.projectFiles.filter(f => f.id !== fileId);
+      // 从本地列表中移除
+      this.projectFiles = this.projectFiles.filter(f => f.id !== fileId);
 
-        console.log('✅ 文件删除成功:', fileId);
-      }
+      console.log('✅ 文件删除成功:', fileId);
     } catch (error) {
       console.error('❌ 文件删除失败:', error);
       alert('删除失败,请稍后重试');
@@ -1151,4 +1487,149 @@ export class StageOrderComponent implements OnInit {
     document.body.removeChild(link);
   }
 
-}
+  // UI 辅助方法
+  hasSpacesDisabled(): boolean {
+    return !this.canEdit || !this.commercialScenes?.spaces?.some(s => s?.selected)
+  }
+
+  hasRoomsDisabled(): boolean {
+    return !this.canEdit || !this.homeScenes?.rooms?.some((r: { selected?: boolean }) => r?.selected === true)
+  }
+
+  getLevels(levels: any): string[] {
+    if (!levels) return []
+    return Object.keys(levels)
+  }
+
+  /**
+   * 获取空间类型的显示名称
+   */
+  getSpaceTypeName(spaceType: string): string {
+    let name = this.spaceTypeMap?.[spaceType] || '其他';
+    return name
+  }
+
+  /**
+   * 获取空间图标
+   */
+  getSpaceIcon(spaceType: string): string {
+    const iconMap: Record<string, string> = {
+      'living_room': 'living-room',
+      'bedroom': 'bedroom',
+      'kitchen': 'kitchen',
+      'bathroom': 'bathroom',
+      'dining_room': 'dining-room',
+      'study': 'study',
+      'balcony': 'balcony',
+      'corridor': 'corridor',
+      'storage': 'storage',
+      'entrance': 'entrance',
+      'other': 'other'
+    };
+    return iconMap[spaceType] || 'room';
+  }
+
+  /**
+   * 获取进程颜色
+   */
+  getProcessColor(processType: string): string {
+    const colorMap: any = {
+      'modeling': 'primary',
+      'softDecor': 'secondary',
+      'rendering': 'tertiary',
+      'postProcess': 'success'
+    };
+    return colorMap[processType] || 'medium';
+  }
+
+  /**
+   * 获取状态颜色
+   */
+  getStatusColor(status: string): string {
+    const colorMap: any = {
+      'draft': 'medium',
+      'submitted': 'warning',
+      'approved': 'success',
+      'rejected': 'danger'
+    };
+    return colorMap[status] || 'medium';
+  }
+
+  /**
+   * 获取状态文本
+   */
+  getStatusText(status: string): string {
+    const textMap: any = {
+      'draft': '草稿',
+      'submitted': '待审核',
+      'approved': '已通过',
+      'rejected': '已驳回'
+    };
+    return textMap[status] || status;
+  }
+
+  /**
+   * 按空间分组交付物
+   */
+  get deliverablesBySpace(): Array<{ spaceId: string; spaceName: string; items: any[] }> {
+    if (!this.isMultiSpaceProject) {
+      return [{
+        spaceId: 'default',
+        spaceName: '单空间项目',
+        items: []
+      }];
+    }
+
+    const grouped = new Map<string, any[]>();
+
+    for (const space of this.projectSpaces) {
+      const deliverables = this.quotation.spaces.filter(d => d.spaceId === space.id);
+      if (deliverables.length > 0) {
+        grouped.set(space.id, deliverables);
+      }
+    }
+
+    return Array.from(grouped.entries()).map(([spaceId, items]) => ({
+      spaceId,
+      spaceName: items[0]?.name || '未知空间',
+      items
+    }));
+  }
+
+  /**
+   * 计算完成进度
+   */
+  get completionProgress(): number {
+    if (this.quotation.spaces.length === 0) return 0;
+    const approvedCount = this.quotation.spaces.filter(d => {
+      return Object.values(d.processes).some((p: any) => p.enabled);
+    }).length;
+    return Math.round((approvedCount / this.quotation.spaces.length) * 100);
+  }
+
+  /**
+   * 获取总进度(多空间)
+   */
+  getOverallProgress(): number {
+    if (!this.isMultiSpaceProject) return this.completionProgress;
+
+    let totalProgress = 0;
+    let totalSpaces = 0;
+
+    for (const space of this.projectSpaces) {
+      const spaceProgress = this.getSpaceProgress(space.id);
+      totalProgress += spaceProgress;
+      totalSpaces++;
+    }
+
+    return totalSpaces > 0 ? Math.round(totalProgress / totalSpaces) : 0;
+  }
+
+  /**
+   * 获取当前选中的空间信息
+   */
+  get currentSpace(): ProjectSpace | null {
+    if (!this.isMultiSpaceProject || !this.activeSpaceId) return null;
+    return this.projectSpaces.find(s => s.id === this.activeSpaceId) || null;
+  }
+}

+ 449 - 287
src/modules/project/pages/project-detail/stages/stage-requirements.component.html

@@ -11,317 +11,448 @@
 <!-- 确认需求内容 -->
 @if (!loading) {
   <div class="stage-requirements-container">
-    <!-- 1. 参考图片 -->
-    <div class="card reference-images-card">
-      <div class="card-header">
-        <h3 class="card-title">
-          <svg class="icon" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M432 112V96a48.14 48.14 0 00-48-48H64a48.14 48.14 0 00-48 48v256a48.14 48.14 0 0048 48h16v16a48.14 48.14 0 0048 48h320a48.14 48.14 0 0048-48V160a48.14 48.14 0 00-48-48zM96 352H64a16 16 0 01-16-16V96a16 16 0 0116-16h320a16 16 0 0116 16v16H128a48.14 48.14 0 00-48 48zm352 64a16 16 0 01-16 16H128a16 16 0 01-16-16V160a16 16 0 0116-16h304a16 16 0 0116 16z"/>
-            <circle cx="213.33" cy="229.33" r="26.67" fill="currentColor"/>
-            <path fill="currentColor" d="M384 336H213l36.67-73.33 32 48 48-64L384 336z"/>
-          </svg>
-          参考图片
-        </h3>
-        <p class="card-subtitle">上传风格、空间或材质参考图</p>
-      </div>
-      <div class="card-content">
-        <div class="images-grid">
-          @for (image of referenceImages; track $index) {
-            <div class="image-item">
-              <img [src]="image.url" [alt]="image.name" />
-              <div class="image-overlay">
-                <span class="badge" [class]="'badge-' + getImageTypeColor(image.type)">
-                  {{ getImageTypeLabel(image.type) }}
-                </span>
-                @if (canEdit) {
-                  <button
-                    class="btn-icon btn-danger"
-                    (click)="deleteReferenceImage($index)">
-                    <svg class="icon" viewBox="0 0 512 512">
-                      <path fill="currentColor" d="M296 64h-80a7.91 7.91 0 00-8 8v24h96V72a7.91 7.91 0 00-8-8z"/>
-                      <path fill="currentColor" d="M432 96h-96V72a40 40 0 00-40-40h-80a40 40 0 00-40 40v24H80a16 16 0 000 32h17l19 304.92c1.42 26.85 22 47.08 48 47.08h184c26.13 0 46.3-19.78 48-47l19-305h17a16 16 0 000-32zM192.57 416H192a16 16 0 01-16-15.43l-8-224a16 16 0 1132-1.14l8 224A16 16 0 01192.57 416zM272 400a16 16 0 01-32 0V176a16 16 0 0132 0zm32-304h-96V72a8 8 0 018-8h80a8 8 0 018 8zm32 304.57A16 16 0 01320 416h-.58A16 16 0 01304 399.43l8-224a16 16 0 1132 1.14z"/>
-                    </svg>
-                  </button>
-                }
-              </div>
-            </div>
-          }
-
-          @if (canEdit) {
-            <div class="upload-placeholder">
-              <input
-                type="file"
-                accept="image/*"
-                (change)="uploadReferenceImage($event)"
-                [disabled]="uploading"
-                hidden
-                #fileInput />
-              <button
-                class="btn btn-outline"
-                (click)="fileInput.click()"
-                [disabled]="uploading">
-                <svg class="icon" viewBox="0 0 512 512">
-                  <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 112v288m144-144H112"/>
-                </svg>
-                上传图片
-              </button>
-            </div>
+    <!-- 多空间切换器 -->
+    @if (isMultiSpaceProject) {
+      <div class="space-selector">
+        <div class="space-tabs">
+          @for (space of projectSpaces; track space.id) {
+            <button
+              class="space-tab"
+              [class.active]="activeSpaceId === space.id"
+              (click)="selectSpace(space.id)">
+              <span class="space-icon">{{ getSpaceIcon(space.type) }}</span>
+              <span>{{ getSpaceDisplayName(space) }}</span>
+              <span class="progress-indicator" [style.width.%]="calculateSpaceCompletion(space.id)"></span>
+            </button>
           }
         </div>
       </div>
-    </div>
+    }
 
-    <!-- 2. CAD文件 -->
-    <div class="card cad-files-card">
-      <div class="card-header">
-        <h3 class="card-title">
-          <svg class="icon" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M428 224H288a48 48 0 01-48-48V36a4 4 0 00-4-4h-92a64 64 0 00-64 64v320a64 64 0 0064 64h224a64 64 0 0064-64V228a4 4 0 00-4-4z"/>
-            <path fill="currentColor" d="M419.22 188.59L275.41 44.78a2 2 0 00-3.41 1.41V176a16 16 0 0016 16h129.81a2 2 0 001.41-3.41z"/>
-          </svg>
-          CAD文件
-        </h3>
-        <p class="card-subtitle">上传户型图或施工图纸</p>
+    <!-- 需求分段导航 -->
+    <div class="requirements-segment">
+      <div class="segment-buttons">
+        <button
+          class="segment-btn"
+          [class.active]="requirementsSegment === 'global'"
+          (click)="selectRequirementsSegment('global')">
+          全局需求
+        </button>
+        @if (isMultiSpaceProject) {
+          <button
+            class="segment-btn"
+            [class.active]="requirementsSegment === 'spaces'"
+            (click)="selectRequirementsSegment('spaces')">
+            空间需求
+          </button>
+          <button
+            class="segment-btn"
+            [class.active]="requirementsSegment === 'cross-space'"
+            (click)="selectRequirementsSegment('cross-space')">
+            跨空间协调
+          </button>
+        }
       </div>
-      <div class="card-content">
-        @if (cadFiles.length === 0) {
-          <div class="empty-state">
-            <svg class="icon-large" viewBox="0 0 512 512">
-              <path fill="currentColor" d="M428 224H288a48 48 0 01-48-48V36a4 4 0 00-4-4h-92a64 64 0 00-64 64v320a64 64 0 0064 64h224a64 64 0 0064-64V228a4 4 0 00-4-4z" opacity="0.4"/>
-              <path fill="currentColor" d="M419.22 188.59L275.41 44.78a2 2 0 00-3.41 1.41V176a16 16 0 0016 16h129.81a2 2 0 001.41-3.41z"/>
-            </svg>
-            <p>暂无CAD文件</p>
+    </div>
+
+    <!-- 全局需求 -->
+    @if (requirementsSegment === 'global') {
+      <div class="global-requirements">
+        <!-- 参考图片 -->
+        <div class="card reference-images-card">
+          <div class="card-header">
+            <h3 class="card-title">
+              <span class="icon">📷</span>
+              参考图片
+            </h3>
+            <p class="card-subtitle">上传风格、空间或材质参考图</p>
           </div>
-        } @else {
-          <div class="file-list">
-            @for (file of cadFiles; track $index) {
-              <div class="file-item">
-                <svg class="icon file-icon" viewBox="0 0 512 512">
-                  <path fill="currentColor" d="M416 221.25V416a48 48 0 01-48 48H144a48 48 0 01-48-48V96a48 48 0 0148-48h98.75a32 32 0 0122.62 9.37l141.26 141.26a32 32 0 019.37 22.62z"/>
-                  <path fill="currentColor" d="M256 56v120a32 32 0 0032 32h120" opacity="0.4"/>
-                </svg>
-                <div class="file-info">
-                  <h4>{{ file.name }}</h4>
-                  <p>{{ formatFileSize(file.size) }} · {{ file.uploadTime | date:'yyyy-MM-dd HH:mm' }}</p>
+          <div class="card-content">
+            <div class="images-grid">
+              @for (image of getFilteredReferenceImages(); track image.id) {
+                <div class="image-item">
+                  <img [src]="image.url" [alt]="image.name" />
+                  <div class="image-overlay">
+                    <span class="badge" [class]="getImageTypeBadgeClass(image.type)">
+                      {{ getImageTypeLabel(image.type) }}
+                    </span>
+                    @if (image.spaceId) {
+                      <span class="badge badge-outline">{{ getSpaceDisplayNameById(image.spaceId || '') }}</span>
+                    }
+                    @if (canEdit) {
+                      <button
+                        class="btn-icon btn-danger"
+                        (click)="deleteReferenceImage(image.id)">
+                        <ion-icon name="trash"></ion-icon>
+                      </button>
+                    }
+                  </div>
                 </div>
-                @if (canEdit) {
+              }
+
+              @if (canEdit) {
+                <div class="upload-placeholder">
+                  <input
+                    type="file"
+                    id="referenceFileInput"
+                    accept="image/*"
+                    multiple
+                    (change)="uploadReferenceImage($event, activeSpaceId)"
+                    [disabled]="uploading"
+                    hidden />
                   <button
-                    class="btn-icon btn-danger"
-                    (click)="deleteCAD($index)">
-                    <svg class="icon" viewBox="0 0 512 512">
-                      <path fill="currentColor" d="M296 64h-80a7.91 7.91 0 00-8 8v24h96V72a7.91 7.91 0 00-8-8z"/>
-                      <path fill="currentColor" d="M432 96h-96V72a40 40 0 00-40-40h-80a40 40 0 00-40 40v24H80a16 16 0 000 32h17l19 304.92c1.42 26.85 22 47.08 48 47.08h184c26.13 0 46.3-19.78 48-47l19-305h17a16 16 0 000-32zM192.57 416H192a16 16 0 01-16-15.43l-8-224a16 16 0 1132-1.14l8 224A16 16 0 01192.57 416zM272 400a16 16 0 01-32 0V176a16 16 0 0132 0zm32-304h-96V72a8 8 0 018-8h80a8 8 0 018 8zm32 304.57A16 16 0 01320 416h-.58A16 16 0 01304 399.43l8-224a16 16 0 1132 1.14z"/>
-                    </svg>
+                    class="btn btn-outline"
+                    (click)="triggerFileClick('referenceFileInput')"
+                    [disabled]="uploading">
+                    <ion-icon name="add"></ion-icon>
+                    上传图片
                   </button>
+                </div>
+              }
+            </div>
+          </div>
+        </div>
+
+        <!-- CAD文件 -->
+        <div class="card cad-files-card">
+          <div class="card-header">
+            <h3 class="card-title">
+              <span class="icon">📄</span>
+              CAD文件
+            </h3>
+            <p class="card-subtitle">上传户型图或施工图纸</p>
+          </div>
+          <div class="card-content">
+            @if (getFilteredCADFiles().length === 0) {
+              <div class="empty-state">
+                <ion-icon name="document-outline" class="icon-large"></ion-icon>
+                <p>暂无CAD文件</p>
+              </div>
+            } @else {
+              <div class="file-list">
+                @for (file of getFilteredCADFiles(); track file.id) {
+                  <div class="file-item">
+                    <ion-icon name="document-text" class="file-icon"></ion-icon>
+                    <div class="file-info">
+                      <h4>{{ file.name }}</h4>
+                      <p>{{ formatFileSize(file.size) }} · {{ file.uploadTime | date:'yyyy-MM-dd HH:mm' }}</p>
+                      @if (file.spaceId) {
+                        <span class="badge badge-outline">{{ getSpaceDisplayNameById(file.spaceId || '') }}</span>
+                      }
+                    </div>
+                    @if (canEdit) {
+                      <button
+                        class="btn-icon btn-danger"
+                        (click)="deleteCAD(file.id)">
+                        <ion-icon name="trash"></ion-icon>
+                      </button>
+                    }
+                  </div>
                 }
               </div>
             }
-          </div>
-        }
 
-        @if (canEdit) {
-          <input
-            type="file"
-            accept=".dwg,.dxf,.pdf"
-            (change)="uploadCAD($event)"
-            [disabled]="uploading"
-            hidden
-            #cadInput />
-          <button
-            class="btn btn-outline btn-block"
-            (click)="cadInput.click()"
-            [disabled]="uploading">
-            <svg class="icon" viewBox="0 0 512 512">
-              <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M320 367.79h76c55 0 100-29.21 100-83.6s-53-81.47-96-83.6c-8.89-85.06-71-136.8-144-136.8-69 0-113.44 45.79-128 91.2-60 5.7-112 43.88-112 106.4s54 106.4 120 106.4h56"/>
-              <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M320 255.79l-64-64-64 64m64 192.42V207.79"/>
-            </svg>
-            上传CAD文件
-          </button>
-        }
-      </div>
-    </div>
-
-    <!-- 3. 需求清单 -->
-    <div class="card requirements-card">
-      <div class="card-header">
-        <h3 class="card-title">
-          <svg class="icon" viewBox="0 0 512 512">
-            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M160 144h288m-288 96h288m-288 96h288"/>
-            <circle cx="80" cy="144" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
-            <circle cx="80" cy="240" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
-            <circle cx="80" cy="336" r="16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
-          </svg>
-          需求清单
-        </h3>
-      </div>
-      <div class="card-content">
-        <!-- 空间信息 -->
-        <div class="section">
-          <div class="section-header">
-            <h3>空间信息</h3>
             @if (canEdit) {
-              <button class="btn btn-outline btn-sm" (click)="addSpace()">
-                <svg class="icon" viewBox="0 0 512 512">
-                  <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 112v288m144-144H112"/>
-                </svg>
-                添加空间
+              <input
+                type="file"
+                accept=".dwg,.dxf,.pdf"
+                multiple
+                (change)="uploadCAD($event, activeSpaceId)"
+                [disabled]="uploading"
+                hidden
+                id="cadFileInput" />
+              <button
+                class="btn btn-outline btn-block"
+                (click)="triggerFileClick('cadFileInput')"
+                [disabled]="uploading">
+                <ion-icon name="cloud-upload"></ion-icon>
+                上传CAD文件
               </button>
             }
           </div>
+        </div>
 
-          @if (requirements.spaces.length === 0) {
-            <p class="empty-text">请添加至少一个空间</p>
-          } @else {
-            @for (space of requirements.spaces; track $index) {
-              <div class="space-item">
-                <div class="space-header">
-                  <span class="badge badge-primary">{{ $index + 1 }}</span>
-                  @if (canEdit) {
-                    <button
-                      class="btn-icon btn-danger"
-                      (click)="removeSpace($index)">
-                      <svg class="icon" viewBox="0 0 512 512">
-                        <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144m224 0L144 368"/>
-                      </svg>
-                    </button>
-                  }
-                </div>
+        <!-- 风格偏好 -->
+        <div class="card style-preferences-card">
+          <div class="card-header">
+            <h3 class="card-title">
+              <span class="icon">🎨</span>
+              风格偏好
+            </h3>
+          </div>
+          <div class="card-content">
+            <div class="form-group">
+              <label class="form-label">风格偏好 <span class="required">*</span></label>
+              <input
+                type="text"
+                class="form-input"
+                [(ngModel)]="globalRequirements.stylePreference"
+                [disabled]="!canEdit"
+                placeholder="如:现代简约、北欧、轻奢等" />
+            </div>
+
+            <!-- 色彩方案 -->
+            <div class="color-scheme">
+              <div class="form-group">
+                <label class="form-label">色彩氛围</label>
+                <select
+                  class="form-select"
+                  [(ngModel)]="globalRequirements.colorScheme.atmosphere"
+                  [disabled]="!canEdit">
+                  <option value="">请选择</option>
+                  <option value="温馨">温馨</option>
+                  <option value="高级">高级</option>
+                  <option value="简约">简约</option>
+                  <option value="时尚">时尚</option>
+                  <option value="北欧">北欧</option>
+                  <option value="中式">中式</option>
+                  <option value="欧式">欧式</option>
+                </select>
+              </div>
 
+              <div class="color-inputs">
                 <div class="form-group">
-                  <label class="form-label">空间名称 <span class="required">*</span></label>
+                  <label class="form-label">主色调</label>
                   <input
-                    type="text"
-                    class="form-input"
-                    [(ngModel)]="space.name"
-                    [disabled]="!canEdit"
-                    placeholder="如:客厅、主卧等" />
+                    type="color"
+                    class="form-color-input"
+                    [(ngModel)]="globalRequirements.colorScheme.primary"
+                    [disabled]="!canEdit" />
                 </div>
+                <div class="form-group">
+                  <label class="form-label">副色调</label>
+                  <input
+                    type="color"
+                    class="form-color-input"
+                    [(ngModel)]="globalRequirements.colorScheme.secondary"
+                    [disabled]="!canEdit" />
+                </div>
+                <div class="form-group">
+                  <label class="form-label">点缀色</label>
+                  <input
+                    type="color"
+                    class="form-color-input"
+                    [(ngModel)]="globalRequirements.colorScheme.accent"
+                    [disabled]="!canEdit" />
+                </div>
+              </div>
+            </div>
 
+            <!-- 预算范围 -->
+            <div class="budget-range">
+              <h4>预算范围(万元)</h4>
+              <div class="budget-inputs">
                 <div class="form-group">
-                  <label class="form-label">面积(㎡) <span class="required">*</span></label>
+                  <label class="form-label">最低</label>
                   <input
                     type="number"
                     class="form-input"
-                    [(ngModel)]="space.area"
+                    [(ngModel)]="globalRequirements.overallBudget.min"
                     [disabled]="!canEdit"
                     placeholder="0" />
                 </div>
-
+                <span class="separator">-</span>
                 <div class="form-group">
-                  <label class="form-label">空间描述</label>
-                  <textarea
-                    class="form-textarea"
-                    [(ngModel)]="space.description"
+                  <label class="form-label">最高</label>
+                  <input
+                    type="number"
+                    class="form-input"
+                    [(ngModel)]="globalRequirements.overallBudget.max"
                     [disabled]="!canEdit"
-                    rows="2"
-                    placeholder="描述空间用途、功能需求等"></textarea>
+                    placeholder="0" />
                 </div>
               </div>
-            }
-          }
-        </div>
-
-        <!-- 风格偏好 -->
-        <div class="section">
-          <div class="form-group">
-            <label class="form-label">风格偏好 <span class="required">*</span></label>
-            <input
-              type="text"
-              class="form-input"
-              [(ngModel)]="requirements.stylePreference"
-              [disabled]="!canEdit"
-              placeholder="如:现代简约、北欧、轻奢等" />
-          </div>
-        </div>
+            </div>
 
-        <!-- 色彩方案 -->
-        <div class="section">
-          <div class="section-header">
-            <h3>色彩方案</h3>
-            @if (canEdit) {
-              <button class="btn btn-outline btn-sm" (click)="openColorAnalysis()">
-                <svg class="icon" viewBox="0 0 512 512">
-                  <path fill="currentColor" d="M441 336.2l-.06-.05c-9.93-9.18-22.78-11.34-32.16-12.92l-.69-.12c-9.05-1.49-10.48-2.5-14.58-6.17-2.44-2.17-5.35-5.65-5.35-9.94s2.91-7.77 5.34-9.94l30.28-26.87c25.92-22.91 40.2-53.66 40.2-86.59s-14.25-63.68-40.2-86.6c-35.89-31.59-85-49-138.37-49C223.72 48 162 71.37 116 112.11c-43.87 38.77-68 90.71-68 146.24s24.16 107.47 68 146.23c21.75 19.24 47.49 34.18 76.52 44.42a266.17 266.17 0 0086.87 15h1.81c61 0 119.09-20.57 159.39-56.4 9.7-8.56 15.15-20.83 15.34-34.56.21-14.17-5.37-27.95-14.93-36.84zM112 208a32 32 0 1132 32 32 32 0 01-32-32zm40 135a32 32 0 1132-32 32 32 0 01-32 32zm40-199a32 32 0 1132 32 32 32 0 01-32-32zm64 271a48 48 0 1148-48 48 48 0 01-48 48zm72-239a32 32 0 1132-32 32 32 0 01-32 32zm32 39a32 32 0 1132 32 32 32 0 01-32-32zm68 88a32 32 0 1132-32 32 32 0 01-32 32z"/>
-                </svg>
-                色彩分析
-              </button>
-            }
-          </div>
+            <!-- 质量等级 -->
+            <div class="quality-level">
+              <label class="form-label">质量等级</label>
+              <div class="radio-group">
+                <label class="radio-item">
+                  <input
+                    type="radio"
+                    name="qualityLevel"
+                    value="standard"
+                    [(ngModel)]="globalRequirements.qualityLevel"
+                    [disabled]="!canEdit" />
+                  <span class="radio-label">{{ getQualityLevelName('standard') }}</span>
+                </label>
+                <label class="radio-item">
+                  <input
+                    type="radio"
+                    name="qualityLevel"
+                    value="premium"
+                    [(ngModel)]="globalRequirements.qualityLevel"
+                    [disabled]="!canEdit" />
+                  <span class="radio-label">{{ getQualityLevelName('premium') }}</span>
+                </label>
+                <label class="radio-item">
+                  <input
+                    type="radio"
+                    name="qualityLevel"
+                    value="luxury"
+                    [(ngModel)]="globalRequirements.qualityLevel"
+                    [disabled]="!canEdit" />
+                  <span class="radio-label">{{ getQualityLevelName('luxury') }}</span>
+                </label>
+              </div>
+            </div>
 
-          <div class="form-group">
-            <label class="form-label">色彩氛围</label>
-            <select
-              class="form-select"
-              [(ngModel)]="requirements.colorScheme.atmosphere"
-              [disabled]="!canEdit">
-              <option value="">请选择</option>
-              <option value="温馨">温馨</option>
-              <option value="高级">高级</option>
-              <option value="简约">简约</option>
-              <option value="时尚">时尚</option>
-            </select>
+            <!-- 特殊需求 -->
+            <div class="form-group">
+              <label class="form-label">特殊需求</label>
+              <textarea
+                class="form-textarea"
+                [(ngModel)]="globalRequirements.specialRequirements"
+                [disabled]="!canEdit"
+                rows="3"
+                placeholder="描述任何特殊需求或注意事项"></textarea>
+            </div>
           </div>
         </div>
+      </div>
+    }
 
-        <!-- 预算范围 -->
-        <div class="section">
-          <h3>预算范围(万元)</h3>
-          <div class="budget-range">
-            <div class="form-group">
-              <label class="form-label">最低</label>
-              <input
-                type="number"
-                class="form-input"
-                [(ngModel)]="requirements.budget.min"
-                [disabled]="!canEdit"
-                placeholder="0" />
+    <!-- 空间需求 -->
+    @if (requirementsSegment === 'spaces' && isMultiSpaceProject) {
+      <div class="space-requirements">
+        @for (space of projectSpaces; track space.id) {
+          <div class="card space-requirement-card" [class.active]="activeSpaceId === space.id">
+            <div class="card-header">
+              <h3 class="card-title">
+                <span class="space-icon">{{ getSpaceIcon(space.type) }}</span>
+                {{ getSpaceDisplayName(space) }}
+              </h3>
+              <span class="completion-badge">{{ calculateSpaceCompletion(space.id) }}%</span>
             </div>
-            <span class="separator">-</span>
-            <div class="form-group">
-              <label class="form-label">最高</label>
-              <input
-                type="number"
-                class="form-input"
-                [(ngModel)]="requirements.budget.max"
-                [disabled]="!canEdit"
-                placeholder="0" />
+            <div class="card-content">
+              <!-- 这里可以添加空间特定的需求字段 -->
+              <div class="form-group">
+                <label class="form-label">空间特殊要求</label>
+                <textarea
+                  class="form-textarea"
+                  [(ngModel)]="currentSpaceSpecificRequirements"
+                  [disabled]="!canEdit"
+                  rows="2"
+                  placeholder="描述该空间的特殊要求"></textarea>
+              </div>
+
+              <!-- 空间特定的参考图片 -->
+              <div class="space-reference-images">
+                <h4>空间参考图片</h4>
+                <div class="images-grid small">
+                  @for (image of getSpaceReferenceImages(space.id); track image.id) {
+                    <div class="image-item small">
+                      <img [src]="image.url" [alt]="image.name" />
+                      <div class="image-overlay">
+                        @if (canEdit) {
+                          <button
+                            class="btn-icon btn-danger"
+                            (click)="deleteReferenceImage(image.id)">
+                            <ion-icon name="trash"></ion-icon>
+                          </button>
+                        }
+                      </div>
+                    </div>
+                  }
+
+                  @if (canEdit) {
+                    <div class="upload-placeholder small">
+                      <input
+                        type="file"
+                        accept="image/*"
+                        multiple
+                        (change)="uploadReferenceImage($event, space.id)"
+                        [disabled]="uploading"
+                        hidden
+                        id="spaceFileInput_{{ space.id }}" />
+                      <button
+                        class="btn btn-outline btn-sm"
+                        (click)="triggerFileClick('spaceFileInput_' + space.id)"
+                        [disabled]="uploading">
+                        <ion-icon name="add"></ion-icon>
+                      </button>
+                    </div>
+                  }
+                </div>
+              </div>
             </div>
           </div>
-        </div>
+        }
+      </div>
+    }
 
-        <!-- 特殊需求 -->
-        <div class="section">
-          <div class="form-group">
-            <label class="form-label">特殊需求</label>
-            <textarea
-              class="form-textarea"
-              [(ngModel)]="requirements.specialRequirements"
-              [disabled]="!canEdit"
-              rows="3"
-              placeholder="描述任何特殊需求或注意事项"></textarea>
+    <!-- 跨空间协调需求 -->
+    @if (requirementsSegment === 'cross-space' && isMultiSpaceProject) {
+      <div class="cross-space-requirements">
+        <div class="card">
+          <div class="card-header">
+            <h3 class="card-title">
+              <ion-icon name="links"></ion-icon>
+              跨空间协调需求
+            </h3>
+            <button class="btn btn-outline btn-sm" (click)="createCrossSpaceRequirement()">
+              <ion-icon name="add"></ion-icon>
+              添加协调需求
+            </button>
+          </div>
+          <div class="card-content">
+            @if (crossSpaceRequirements.length === 0) {
+              <div class="empty-state">
+                <ion-icon name="links-outline" class="icon-large"></ion-icon>
+                <p>暂无跨空间协调需求</p>
+              </div>
+            } @else {
+              <div class="cross-space-list">
+                @for (requirement of crossSpaceRequirements; track requirement.id) {
+                  <div class="cross-space-item">
+                    <div class="item-header">
+                      <span class="badge badge-primary">{{ getCrossSpaceRequirementTypeName(requirement.type) }}</span>
+                      <button
+                        class="btn-icon btn-danger"
+                        (click)="deleteCrossSpaceRequirement(requirement.id)">
+                        <ion-icon name="trash"></ion-icon>
+                      </button>
+                    </div>
+                    <p class="description">{{ requirement.description }}</p>
+                    <div class="related-spaces">
+                      <span class="label">涉及空间:</span>
+                      <div class="space-tags">
+                        @for (spaceId of getRelatedSpaceIds(requirement); track spaceId) {
+                          <span class="badge badge-outline">
+                            {{ getSpaceDisplayNameById(spaceId) }}
+                          </span>
+                        }
+                      </div>
+                    </div>
+                  </div>
+                }
+              </div>
+            }
           </div>
         </div>
       </div>
-    </div>
+    }
 
-    <!-- 4. AI生成方案 -->
+    <!-- AI生成方案 -->
     <div class="card ai-solution-card">
       <div class="card-header">
         <h3 class="card-title">
-          <svg class="icon" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M208 512a24.84 24.84 0 01-23.34-16l-39.84-103.6a16.06 16.06 0 00-9.19-9.19L32 343.34a25 25 0 010-46.68l103.6-39.84a16.06 16.06 0 009.19-9.19L184.66 144a25 25 0 0146.68 0l39.84 103.6a16.06 16.06 0 009.19 9.19l103 39.63a25.49 25.49 0 0116.63 24.1 24.82 24.82 0 01-16 22.82l-103.6 39.84a16.06 16.06 0 00-9.19 9.19L231.34 496A24.84 24.84 0 01208 512zm66.85-254.84zm-4.56 16.08zm-.21-.12z"/>
-            <path fill="currentColor" d="M88 176a14.67 14.67 0 01-13.69-9.4l-16.86-43.84a7.28 7.28 0 00-4.21-4.21L9.4 101.69a14.67 14.67 0 010-27.38l43.84-16.86a7.31 7.31 0 004.21-4.21L74.16 9.4a14.67 14.67 0 0127.38 0l16.86 43.84a7.31 7.31 0 004.21 4.21l43.84 16.86a14.67 14.67 0 010 27.38l-43.84 16.86a7.28 7.28 0 00-4.21 4.21L101.69 166.6A14.67 14.67 0 0188 176zm312 208a16 16 0 01-14.93-10.26l-22.84-59.37a8 8 0 00-4.6-4.6l-59.37-22.84a16 16 0 010-29.86l59.37-22.84a8 8 0 004.6-4.6l22.67-58.95a16.45 16.45 0 0129.86 0l22.84 59.37a8 8 0 004.6 4.6l59.37 22.84a16 16 0 010 29.86l-59.37 22.84a8 8 0 00-4.6 4.6l-22.84 59.37A16 16 0 01400 384z" opacity="0.4"/>
-          </svg>
+          <ion-icon name="sparkles"></ion-icon>
           AI设计方案
         </h3>
-        <p class="card-subtitle">基于需求智能生成设计方案</p>
+        <div class="completion-indicator">
+          <span class="label">需求完成度</span>
+          <div class="progress-bar">
+            <div class="progress-fill" [style.width.%]="calculateRequirementsCompleteness()"></div>
+          </div>
+          <span class="progress-text">{{ calculateRequirementsCompleteness() }}%</span>
+        </div>
       </div>
       <div class="card-content">
         @if (!aiSolution) {
           <div class="empty-state">
-            <svg class="icon-large" viewBox="0 0 512 512">
-              <path fill="currentColor" d="M304 384v-24c0-29 31.54-56.43 52-76 28.84-27.57 44-64.61 44-108 0-80-63.73-144-144-144a143.6 143.6 0 00-144 144c0 41.84 15.81 81.39 44 108 20.35 19.21 52 46.7 52 76v24m16 96h64m-80-32h96m-128-32h160" opacity="0.4"/>
-            </svg>
+            <ion-icon name="sparkles-outline" class="icon-large"></ion-icon>
             <p>尚未生成AI方案</p>
             @if (canEdit) {
               <button
@@ -334,9 +465,7 @@
                   </div>
                   生成中...
                 } @else {
-                  <svg class="icon" viewBox="0 0 512 512">
-                    <path fill="currentColor" d="M208 512a24.84 24.84 0 01-23.34-16l-39.84-103.6a16.06 16.06 0 00-9.19-9.19L32 343.34a25 25 0 010-46.68l103.6-39.84a16.06 16.06 0 009.19-9.19L184.66 144a25 25 0 0146.68 0l39.84 103.6a16.06 16.06 0 009.19 9.19l103 39.63a25.49 25.49 0 0116.63 24.1 24.82 24.82 0 01-16 22.82l-103.6 39.84a16.06 16.06 0 00-9.19 9.19L231.34 496A24.84 24.84 0 01208 512z"/>
-                  </svg>
+                  <ion-icon name="sparkles"></ion-icon>
                   生成AI方案
                 }
               </button>
@@ -346,9 +475,7 @@
           <div class="ai-solution-content">
             <div class="solution-header">
               <span class="badge badge-success">
-                <svg class="icon" viewBox="0 0 512 512">
-                  <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M416 128L192 384l-96-96"/>
-                </svg>
+                <ion-icon name="checkmark"></ion-icon>
                 已生成
               </span>
               @if (canEdit) {
@@ -356,27 +483,43 @@
                   class="btn btn-outline btn-sm"
                   (click)="generateAISolution()"
                   [disabled]="generating">
-                  <svg class="icon" viewBox="0 0 512 512">
-                    <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M320 146s24.36-12-64-12a160 160 0 10160 160"/>
-                    <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 58l80 80-80 80"/>
-                  </svg>
+                  <ion-icon name="refresh"></ion-icon>
                   重新生成
                 </button>
               }
             </div>
 
+            <div class="solution-summary">
+              <p>{{ aiSolution.content }}</p>
+            </div>
+
             <!-- 空间方案 -->
             <div class="spaces-solution">
-              @for (space of aiSolution.spaces; track $index) {
+              @for (space of aiSolution.spaces; track space.id) {
                 <div class="space-solution-item">
-                  <h4>{{ space.name }}</h4>
+                  <div class="space-header">
+                    <span class="space-icon">{{ getSpaceIcon(space.type) }}</span>
+                    <h4>{{ space.name }}</h4>
+                    <span class="badge" [class]="getProcessBadgeClass(space.type)">{{ getSpaceTypeName(space.type) }}</span>
+                  </div>
                   <p class="style-desc">{{ space.styleDescription }}</p>
 
+                  <div class="solution-details">
+                    <div class="detail-item">
+                      <span class="label">预估造价:</span>
+                      <span class="value">¥{{ space.estimatedCost }}</span>
+                    </div>
+                    <div class="detail-item">
+                      <span class="label">工期:</span>
+                      <span class="value">{{ space.timeline }}</span>
+                    </div>
+                  </div>
+
                   <div class="color-palette">
                     <span class="label">色彩搭配:</span>
                     <div class="colors">
                       @for (color of space.colorPalette; track color) {
-                        <div class="color-swatch" [style.background-color]="color"></div>
+                        <div class="color-swatch" [style.background-color]="color" [title]="color"></div>
                       }
                     </div>
                   </div>
@@ -402,24 +545,48 @@
               }
             </div>
 
+            <!-- 跨空间协调方案 -->
+            @if (aiSolution.crossSpaceCoordination && isMultiSpaceProject) {
+              <div class="cross-space-coordination">
+                <h4>跨空间协调方案</h4>
+                <div class="coordination-items">
+                  <div class="coordination-item">
+                    <span class="icon">🎨</span>
+                    <div>
+                      <h5>风格统一</h5>
+                      <p>{{ aiSolution.crossSpaceCoordination.styleConsistency.description }}</p>
+                    </div>
+                  </div>
+                  <div class="coordination-item">
+                    <ion-icon name="git-network"></ion-icon>
+                    <div>
+                      <h5>功能流线</h5>
+                      <p>{{ aiSolution.crossSpaceCoordination.functionalFlow.description }}</p>
+                    </div>
+                  </div>
+                  <div class="coordination-item">
+                    <ion-icon name="time"></ion-icon>
+                    <div>
+                      <h5>时间协调</h5>
+                      <p>{{ aiSolution.crossSpaceCoordination.timelineCoordination.strategy }}</p>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            }
+
             <!-- 预算与时间线 -->
             <div class="summary">
               <div class="summary-item">
-                <svg class="icon" viewBox="0 0 512 512">
-                  <path fill="currentColor" d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z" opacity="0.4"/>
-                  <path fill="currentColor" d="M310.4 140.6c-17.8 0-32.4 14.2-32.4 31.7v151.4c0 17.5 14.6 31.7 32.4 31.7M201.6 140.6c17.8 0 32.4 14.2 32.4 31.7v151.4c0 17.5-14.6 31.7-32.4 31.7"/>
-                </svg>
+                <ion-icon name="cash"></ion-icon>
                 <div>
-                  <p class="label">预估造价</p>
-                  <h3>¥{{ aiSolution.estimatedCost.toLocaleString() }}</h3>
+                  <p class="label">预估总造价</p>
+                  <h3>¥{{ aiSolution.estimatedCost }}</h3>
                 </div>
               </div>
 
               <div class="summary-item">
-                <svg class="icon" viewBox="0 0 512 512">
-                  <path fill="currentColor" d="M256 64C150 64 64 150 64 256s86 192 192 192 192-86 192-192S362 64 256 64z" opacity="0.4"/>
-                  <path fill="currentColor" d="M256 128v144h96"/>
-                </svg>
+                <ion-icon name="time"></ion-icon>
                 <div>
                   <p class="label">项目周期</p>
                   <p>{{ aiSolution.timeline }}</p>
@@ -431,16 +598,14 @@
       </div>
     </div>
 
-    <!-- 5. 操作按钮 -->
+    <!-- 操作按钮 -->
     @if (canEdit) {
       <div class="action-buttons">
         <button
           class="btn btn-outline"
           (click)="saveDraft()"
           [disabled]="saving">
-          <svg class="icon" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M380.93 57.37A32 32 0 00358.3 48H94.22A46.21 46.21 0 0048 94.22v323.56A46.21 46.21 0 0094.22 464h323.56A46.36 46.36 0 00464 417.78V153.7a32 32 0 00-9.37-22.63zM256 416a64 64 0 1164-64 63.92 63.92 0 01-64 64zm48-224H112a16 16 0 01-16-16v-64a16 16 0 0116-16h192a16 16 0 0116 16v64a16 16 0 01-16 16z"/>
-          </svg>
+          <ion-icon name="save"></ion-icon>
           保存草稿
         </button>
 
@@ -448,13 +613,10 @@
           class="btn btn-primary"
           (click)="submitRequirements()"
           [disabled]="saving || !aiSolution">
-          <svg class="icon" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z" opacity="0.4"/>
-            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M352 176L217.6 336 160 272"/>
-          </svg>
+          <ion-icon name="checkmark"></ion-icon>
           确认需求
         </button>
       </div>
     }
   </div>
-}
+}

+ 482 - 108
src/modules/project/pages/project-detail/stages/stage-requirements.component.scss

@@ -1,4 +1,4 @@
-// 确认需求阶段样式 - 纯 div+scss 实现
+// 确认需求阶段样式 - 多空间支持
 
 // CSS 变量定义
 :host {
@@ -77,6 +77,115 @@
 .stage-requirements-container {
   padding: 0 12px 80px;
 
+  // 多空间切换器
+  .space-selector {
+    margin-bottom: 16px;
+
+    .space-tabs {
+      display: flex;
+      overflow-x: auto;
+      gap: 8px;
+      padding: 4px;
+      background: var(--light-color);
+      border-radius: 12px;
+
+      &::-webkit-scrollbar {
+        height: 4px;
+      }
+
+      &::-webkit-scrollbar-track {
+        background: transparent;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        background: var(--medium-color);
+        border-radius: 2px;
+      }
+
+      .space-tab {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        padding: 12px 16px;
+        border: none;
+        background: transparent;
+        border-radius: 8px;
+        font-size: 13px;
+        font-weight: 500;
+        color: var(--medium-color);
+        cursor: pointer;
+        transition: all 0.3s;
+        white-space: nowrap;
+        position: relative;
+        min-width: 120px;
+        justify-content: center;
+
+        .icon, .space-icon {
+          font-size: 18px;
+        }
+
+        .progress-indicator {
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          height: 2px;
+          background: var(--success-color);
+          border-radius: 1px;
+          transition: width 0.3s;
+        }
+
+        &.active {
+          background: var(--primary-color);
+          color: white;
+          box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.3);
+        }
+
+        &:hover:not(.active) {
+          background: rgba(var(--primary-rgb), 0.1);
+          color: var(--primary-color);
+        }
+      }
+    }
+  }
+
+  // 需求分段导航
+  .requirements-segment {
+    margin-bottom: 16px;
+
+    .segment-buttons {
+      display: flex;
+      gap: 4px;
+      background: var(--light-color);
+      border-radius: 8px;
+      padding: 4px;
+
+      .segment-btn {
+        flex: 1;
+        padding: 12px 16px;
+        border: none;
+        background: transparent;
+        border-radius: 6px;
+        font-size: 13px;
+        font-weight: 500;
+        color: var(--medium-color);
+        cursor: pointer;
+        transition: all 0.3s;
+        white-space: nowrap;
+
+        &.active {
+          background: var(--primary-color);
+          color: white;
+          box-shadow: 0 2px 4px rgba(var(--primary-rgb), 0.3);
+        }
+
+        &:hover:not(.active) {
+          background: rgba(var(--primary-rgb), 0.1);
+          color: var(--primary-color);
+        }
+      }
+    }
+  }
+
   // 通用卡片样式
   .card {
     background: white;
@@ -88,6 +197,9 @@
     .card-header {
       padding: 16px;
       border-bottom: 1px solid var(--light-shade);
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
 
       .card-title {
         display: flex;
@@ -98,11 +210,36 @@
         font-weight: 600;
         color: var(--dark-color);
 
-        .icon {
-          width: 20px;
-          height: 20px;
+        .icon, .space-icon {
+          font-size: 20px;
           color: var(--primary-color);
         }
+
+        .completion-indicator {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          font-size: 12px;
+
+          .progress-bar {
+            width: 60px;
+            height: 6px;
+            background: var(--light-shade);
+            border-radius: 3px;
+            overflow: hidden;
+
+            .progress-fill {
+              height: 100%;
+              background: var(--success-color);
+              transition: width 0.3s;
+            }
+          }
+
+          .progress-text {
+            font-weight: 600;
+            color: var(--success-color);
+          }
+        }
       }
 
       .card-subtitle {
@@ -115,6 +252,11 @@
     .card-content {
       padding: 16px;
     }
+
+    &.active {
+      border: 2px solid var(--primary-color);
+      box-shadow: 0 4px 16px rgba(var(--primary-rgb), 0.2);
+    }
   }
 
   // 必填标记
@@ -133,8 +275,7 @@
     text-align: center;
 
     .icon-large {
-      width: 64px;
-      height: 64px;
+      font-size: 64px;
       color: var(--medium-color);
       margin-bottom: 16px;
       opacity: 0.5;
@@ -179,9 +320,10 @@
           bottom: 0;
           background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), transparent);
           display: flex;
+          flex-direction: column;
           justify-content: space-between;
           padding: 8px;
-          opacity: 1;
+          opacity: 0;
           transition: opacity 0.3s;
 
           .badge {
@@ -200,6 +342,14 @@
         &:hover .image-overlay {
           opacity: 1;
         }
+
+        &.small {
+          aspect-ratio: 1.2;
+
+          .image-overlay {
+            padding: 6px;
+          }
+        }
       }
 
       .upload-placeholder {
@@ -216,6 +366,10 @@
           border-color: var(--primary-color);
           background-color: rgba(var(--primary-rgb), 0.05);
         }
+
+        &.small {
+          aspect-ratio: 1.2;
+        }
       }
     }
   }
@@ -241,8 +395,7 @@
       }
 
       .file-icon {
-        width: 40px;
-        height: 40px;
+        font-size: 40px;
         color: var(--primary-color);
         flex-shrink: 0;
       }
@@ -274,76 +427,151 @@
     }
   }
 
-  // 需求清单卡片
-  .requirements-card {
-    .section {
+  // 风格偏好卡片
+  .style-preferences-card {
+    .color-scheme {
       margin-bottom: 24px;
-      padding-bottom: 24px;
-      border-bottom: 1px solid var(--light-shade);
 
-      &:last-child {
-        border-bottom: none;
-        margin-bottom: 0;
-        padding-bottom: 0;
-      }
+      .color-inputs {
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 12px;
+        margin-top: 12px;
 
-      .section-header {
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        margin-bottom: 16px;
+        .form-color-input {
+          width: 100%;
+          height: 48px;
+          border: 1px solid var(--light-shade);
+          border-radius: 8px;
+          cursor: pointer;
+          padding: 4px;
 
-        h3 {
-          margin: 0;
-          font-size: 15px;
-          font-weight: 600;
-          color: var(--dark-color);
+          &:disabled {
+            cursor: not-allowed;
+            opacity: 0.5;
+          }
         }
       }
+    }
+
+    .budget-range {
+      margin-bottom: 24px;
 
-      h3 {
+      h4 {
         margin: 0 0 12px;
-        font-size: 15px;
+        font-size: 14px;
         font-weight: 600;
         color: var(--dark-color);
       }
 
-      .empty-text {
-        color: var(--medium-color);
-        font-size: 13px;
-        font-style: italic;
-        margin: 0;
+      .budget-inputs {
+        display: grid;
+        grid-template-columns: 1fr auto 1fr;
+        align-items: flex-start;
+        gap: 12px;
+
+        .separator {
+          font-size: 20px;
+          font-weight: 600;
+          color: var(--medium-color);
+          padding-top: 32px;
+        }
+      }
+    }
+
+    .quality-level {
+      margin-bottom: 24px;
+
+      .radio-group {
+        display: flex;
+        gap: 16px;
+        margin-top: 8px;
+
+        .radio-item {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          cursor: pointer;
+
+          input[type="radio"] {
+            margin: 0;
+          }
+
+          .radio-label {
+            font-size: 14px;
+            color: var(--dark-color);
+          }
+        }
+      }
+    }
+  }
+
+  // 空间需求
+  .space-requirements {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+
+    .space-requirement-card {
+      .completion-badge {
+        background: var(--success-color);
+        color: white;
+        padding: 4px 8px;
+        border-radius: 12px;
+        font-size: 12px;
+        font-weight: 600;
+      }
+
+      .space-reference-images {
+        h4 {
+          margin: 16px 0 8px;
+          font-size: 14px;
+          font-weight: 600;
+          color: var(--dark-color);
+        }
       }
+    }
+  }
 
-      .space-item {
+  // 跨空间需求
+  .cross-space-requirements {
+    .cross-space-list {
+      .cross-space-item {
         padding: 16px;
         background-color: var(--light-color);
         border-radius: 8px;
         margin-bottom: 12px;
 
-        &:last-child {
-          margin-bottom: 0;
-        }
-
-        .space-header {
+        .item-header {
           display: flex;
           justify-content: space-between;
           align-items: center;
-          margin-bottom: 12px;
+          margin-bottom: 8px;
         }
-      }
 
-      .budget-range {
-        display: grid;
-        grid-template-columns: 1fr auto 1fr;
-        align-items: flex-start;
-        gap: 12px;
+        .description {
+          margin: 0 0 12px;
+          font-size: 14px;
+          line-height: 1.5;
+          color: var(--dark-color);
+        }
 
-        .separator {
-          font-size: 20px;
-          font-weight: 600;
-          color: var(--medium-color);
-          padding-top: 32px;
+        .related-spaces {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+
+          .label {
+            font-size: 12px;
+            color: var(--medium-color);
+            font-weight: 500;
+          }
+
+          .space-tags {
+            display: flex;
+            gap: 6px;
+            flex-wrap: wrap;
+          }
         }
       }
     }
@@ -366,13 +594,27 @@
           gap: 4px;
           padding: 6px 12px;
 
-          .icon {
-            width: 16px;
-            height: 16px;
+          .icon, .space-icon {
+            font-size: 16px;
           }
         }
       }
 
+      .solution-summary {
+        margin-bottom: 24px;
+        padding: 16px;
+        background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.05), rgba(12, 209, 232, 0.05));
+        border-radius: 8px;
+        border-left: 4px solid var(--primary-color);
+
+        p {
+          margin: 0;
+          font-size: 14px;
+          line-height: 1.6;
+          color: var(--dark-color);
+        }
+      }
+
       .spaces-solution {
         .space-solution-item {
           padding: 16px;
@@ -384,11 +626,23 @@
             margin-bottom: 20px;
           }
 
-          h4 {
-            margin: 0 0 8px;
-            font-size: 16px;
-            font-weight: 600;
-            color: var(--dark-color);
+          .space-header {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            margin-bottom: 8px;
+
+            .icon, .space-icon {
+              font-size: 20px;
+              color: var(--primary-color);
+            }
+
+            h4 {
+              margin: 0;
+              font-size: 16px;
+              font-weight: 600;
+              color: var(--dark-color);
+            }
           }
 
           .style-desc {
@@ -398,6 +652,31 @@
             color: var(--medium-color);
           }
 
+          .solution-details {
+            display: grid;
+            grid-template-columns: repeat(2, 1fr);
+            gap: 12px;
+            margin-bottom: 16px;
+
+            .detail-item {
+              display: flex;
+              align-items: center;
+              gap: 8px;
+
+              .label {
+                font-size: 12px;
+                color: var(--medium-color);
+                font-weight: 500;
+              }
+
+              .value {
+                font-size: 14px;
+                color: var(--dark-color);
+                font-weight: 600;
+              }
+            }
+          }
+
           .color-palette,
           .materials,
           .furniture {
@@ -428,6 +707,7 @@
                 border-radius: 4px;
                 border: 1px solid rgba(0, 0, 0, 0.1);
                 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+                cursor: pointer;
               }
             }
 
@@ -440,6 +720,62 @@
         }
       }
 
+      .cross-space-coordination {
+        margin-bottom: 24px;
+        padding: 16px;
+        background: linear-gradient(135deg, rgba(var(--secondary-rgb), 0.05), rgba(112, 68, 255, 0.05));
+        border-radius: 8px;
+        border-left: 4px solid var(--secondary-color);
+
+        h4 {
+          margin: 0 0 16px;
+          font-size: 15px;
+          font-weight: 600;
+          color: var(--dark-color);
+          display: flex;
+          align-items: center;
+          gap: 8px;
+
+          .icon, .space-icon {
+            font-size: 18px;
+            color: var(--secondary-color);
+          }
+        }
+
+        .coordination-items {
+          display: flex;
+          flex-direction: column;
+          gap: 12px;
+
+          .coordination-item {
+            display: flex;
+            align-items: flex-start;
+            gap: 12px;
+
+            .icon, .space-icon {
+              font-size: 20px;
+              color: var(--secondary-color);
+              flex-shrink: 0;
+              margin-top: 2px;
+            }
+
+            h5 {
+              margin: 0 0 4px;
+              font-size: 14px;
+              font-weight: 600;
+              color: var(--dark-color);
+            }
+
+            p {
+              margin: 0;
+              font-size: 13px;
+              line-height: 1.5;
+              color: var(--medium-color);
+            }
+          }
+        }
+      }
+
       .summary {
         display: grid;
         grid-template-columns: repeat(2, 1fr);
@@ -453,9 +789,8 @@
           background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.1), rgba(12, 209, 232, 0.1));
           border-radius: 8px;
 
-          > .icon {
-            width: 32px;
-            height: 32px;
+          .icon, .space-icon {
+            font-size: 32px;
             color: var(--primary-color);
             flex-shrink: 0;
           }
@@ -494,13 +829,6 @@
   }
 }
 
-// 通用图标样式
-.icon {
-  width: 20px;
-  height: 20px;
-  flex-shrink: 0;
-}
-
 // Badge 组件
 .badge {
   display: inline-block;
@@ -544,6 +872,12 @@
     background: var(--medium-color);
     color: white;
   }
+
+  &.badge-outline {
+    background: transparent;
+    border: 1px solid var(--medium-color);
+    color: var(--medium-color);
+  }
 }
 
 // 按钮样式
@@ -562,9 +896,8 @@
   outline: none;
   white-space: nowrap;
 
-  .icon {
-    width: 20px;
-    height: 20px;
+  .icon, .space-icon {
+    font-size: 20px;
   }
 
   &.btn-primary {
@@ -601,9 +934,8 @@
     padding: 8px 16px;
     font-size: 12px;
 
-    .icon {
-      width: 16px;
-      height: 16px;
+    .icon, .space-icon {
+      font-size: 16px;
     }
   }
 
@@ -632,9 +964,8 @@
   transition: all 0.2s;
   padding: 8px;
 
-  .icon {
-    width: 24px;
-    height: 24px;
+  .icon, .space-icon {
+    font-size: 24px;
   }
 
   &.btn-danger {
@@ -735,6 +1066,12 @@
       }
     }
 
+    .style-preferences-card {
+      .color-inputs {
+        grid-template-columns: repeat(4, 1fr);
+      }
+    }
+
     .ai-solution-card {
       .ai-solution-content {
         .summary {
@@ -762,12 +1099,30 @@
   .stage-requirements-container {
     padding: 0 8px 70px;
 
+    .space-selector {
+      .space-tabs {
+        .space-tab {
+          min-width: 100px;
+          padding: 10px 12px;
+          font-size: 12px;
+
+          .icon, .space-icon {
+            font-size: 16px;
+          }
+        }
+      }
+    }
+
     .card {
       .card-header {
         padding: 12px;
 
         .card-title {
           font-size: 15px;
+
+          .icon, .space-icon {
+            font-size: 18px;
+          }
         }
 
         .card-subtitle {
@@ -796,27 +1151,31 @@
             .btn-icon {
               width: 28px;
               height: 28px;
+
+              .icon, .space-icon {
+                font-size: 20px;
+              }
             }
           }
         }
       }
     }
 
-    .requirements-card {
-      .section {
-        margin-bottom: 20px;
-        padding-bottom: 20px;
-
-        .space-item {
-          padding: 12px;
-        }
+    .style-preferences-card {
+      .color-inputs {
+        grid-template-columns: repeat(2, 1fr);
+        gap: 8px;
+      }
 
-        .budget-range {
+      .quality-level {
+        .radio-group {
+          flex-direction: column;
           gap: 8px;
 
-          .separator {
-            font-size: 18px;
-            padding-top: 28px;
+          .radio-item {
+            .radio-label {
+              font-size: 13px;
+            }
           }
         }
       }
@@ -828,8 +1187,15 @@
           .space-solution-item {
             padding: 12px;
 
-            h4 {
-              font-size: 15px;
+            .space-header {
+              h4 {
+                font-size: 15px;
+              }
+            }
+
+            .solution-details {
+              grid-template-columns: 1fr;
+              gap: 8px;
             }
 
             .color-palette,
@@ -846,15 +1212,26 @@
           }
         }
 
+        .cross-space-coordination {
+          padding: 12px;
+
+          .coordination-items {
+            .coordination-item {
+              .icon, .space-icon {
+                font-size: 18px;
+              }
+            }
+          }
+        }
+
         .summary {
           grid-template-columns: 1fr;
 
           .summary-item {
             padding: 12px;
 
-            > .icon {
-              width: 28px;
-              height: 28px;
+            .icon, .space-icon {
+              font-size: 28px;
             }
 
             h3 {
@@ -872,9 +1249,8 @@
         padding: 10px 16px;
         font-size: 13px;
 
-        .icon {
-          width: 18px;
-          height: 18px;
+        .icon, .space-icon {
+          font-size: 18px;
         }
       }
     }
@@ -898,9 +1274,8 @@
       padding: 10px 20px;
       font-size: 13px;
 
-      .icon {
-        width: 18px;
-        height: 18px;
+      .icon, .space-icon {
+        font-size: 18px;
       }
     }
 
@@ -908,10 +1283,9 @@
       width: 36px;
       height: 36px;
 
-      .icon {
-        width: 20px;
-        height: 20px;
+      .icon, .space-icon {
+        font-size: 20px;
       }
     }
   }
-}
+}

+ 632 - 277
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -1,97 +1,122 @@
-import { Component, OnInit, Input } from '@angular/core';
+import { Component, OnInit, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
-import { ProjectUploadService } from '../../../services/upload.service';
-import { ProjectAIService } from '../../../services/ai.service';
-import { WxworkSDKService } from '../../../services/wxwork-sdk.service';
-
-const Parse = FmodeParse.with('nova');
+import { IonIcon } from '@ionic/angular/standalone';
 
 /**
- * 确认需求阶段组件
- *
- * 功能:
- * 1. 参考图片上传与管理
- * 2. 色彩分析弹窗
- * 3. CAD文件上传
- * 4. 需求确认清单
- * 5. AI方案生成
+ * 确认需求阶段组件 - 多空间支持
  */
 @Component({
   selector: 'app-stage-requirements',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [CommonModule, FormsModule, ReactiveFormsModule, IonIcon],
   templateUrl: './stage-requirements.component.html',
-  styleUrls: ['./stage-requirements.component.scss']
+  styleUrls: ['./stage-requirements.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class StageRequirementsComponent implements OnInit {
-  @Input() project: FmodeObject | null = null;
-  @Input() customer: FmodeObject | null = null;
-  @Input() currentUser: FmodeObject | null = null;
+  @Input() project: any = null;
+  @Input() customer: any = null;
+  @Input() currentUser: any = null;
   @Input() canEdit: boolean = false;
 
   // 路由参数
   cid: string = '';
   projectId: string = '';
 
-  // 服务注入
-  wxwork: WxworkSDKService | null = null;
+  // 多空间管理
+  projectSpaces: any[] = [];
+  isMultiSpaceProject: boolean = false;
+  activeSpaceId: string = '';
+  selectedSpaceIds: string[] = [];
+
+  // 需求分段管理
+  requirementsSegment: string = 'global'; // global | spaces | cross-space
+
+  // 全局需求
+  globalRequirements = {
+    stylePreference: '',
+    colorScheme: {
+      primary: '',
+      secondary: '',
+      accent: '',
+      atmosphere: ''
+    },
+    overallBudget: {
+      min: 0,
+      max: 0
+    },
+    timeline: '',
+    qualityLevel: 'standard', // standard | premium | luxury
+    specialRequirements: '',
+    environmentRequirements: {
+      lighting: '',
+      ventilation: '',
+      noise: '',
+      temperature: ''
+    }
+  };
+
+  // 空间需求数据
+  spaceRequirements: any[] = [];
 
-  // 参考图片
+  // 跨空间需求
+  crossSpaceRequirements: any[] = [];
+
+  // 参考图片(支持按空间分类)
   referenceImages: Array<{
+    id: string;
     url: string;
     name: string;
     type: string; // style | space | material
     uploadTime: Date;
     description?: string;
+    spaceId?: string;
+    tags: string[];
   }> = [];
 
   // CAD文件
   cadFiles: Array<{
+    id: string;
     url: string;
     name: string;
     uploadTime: Date;
     size: number;
+    spaceId?: string;
   }> = [];
 
-  // 需求清单
-  requirements = {
-    spaces: [] as Array<{
-      name: string;
-      area: number;
-      description: string;
-      features: string[];
-    }>,
-    stylePreference: '',
-    colorScheme: {
-      primary: '',
-      secondary: '',
-      accent: '',
-      atmosphere: '' // 温馨 | 高级 | 简约 | 时尚
-    },
-    budget: {
-      min: 0,
-      max: 0
-    },
-    specialRequirements: '',
-    deadline: ''
-  };
-
   // AI生成的方案
   aiSolution: {
     generated: boolean;
     content: string;
     spaces: Array<{
+      id: string;
       name: string;
+      type: string;
       styleDescription: string;
       colorPalette: string[];
       materials: string[];
       furnitureRecommendations: string[];
+      estimatedCost?: number;
+      timeline?: string;
     }>;
     estimatedCost: number;
     timeline: string;
+    crossSpaceCoordination?: {
+      styleConsistency: {
+        description: string;
+        keyElements: string[];
+      };
+      functionalFlow: {
+        description: string;
+        considerations: string[];
+      };
+      timelineCoordination: {
+        description: string;
+        strategy: string;
+      };
+    };
   } | null = null;
 
   // 加载状态
@@ -100,13 +125,9 @@ export class StageRequirementsComponent implements OnInit {
   generating: boolean = false;
   saving: boolean = false;
 
-  // 使用懒加载方式注入服务,避免循环依赖
-  private uploadService?: ProjectUploadService;
-  private aiService?: ProjectAIService;
-  private wxworkService?: WxworkSDKService;
-
   constructor(
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private cdr: ChangeDetectorRef
   ) {}
 
   async ngOnInit() {
@@ -127,65 +148,47 @@ export class StageRequirementsComponent implements OnInit {
     try {
       this.loading = true;
 
-      // 如果没有传入project,从路由参数加载
-      if (!this.project && this.projectId) {
-        const query = new Parse.Query('Project');
-        query.include('customer', 'assignee');
-        this.project = await query.get(this.projectId);
-        this.customer = this.project.get('customer');
-      }
-
-      // 如果没有传入currentUser,加载当前用户
-      if (!this.currentUser && this.cid) {
-        // 动态导入WxworkSDK避免循环依赖
-        const { WxworkSDK } = await import('fmode-ng/core');
-        const wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
-        this.currentUser = await wxwork.getCurrentUser();
-
-        const role = this.currentUser?.get('roleName') || '';
-        this.canEdit = ['客服', '组员', '组长', '管理员'].includes(role);
+      // 模拟加载项目空间数据
+      this.projectSpaces = [
+        { id: '1', name: '客厅', type: 'living_room', area: 25, priority: 5, status: 'not_started', complexity: 'medium', order: 0 },
+        { id: '2', name: '主卧', type: 'bedroom', area: 18, priority: 4, status: 'not_started', complexity: 'medium', order: 1 },
+        { id: '3', name: '厨房', type: 'kitchen', area: 12, priority: 5, status: 'not_started', complexity: 'high', order: 2 }
+      ];
+      this.isMultiSpaceProject = this.projectSpaces.length > 1;
+
+      // 如果有空间,默认选中第一个
+      if (this.projectSpaces.length > 0 && !this.activeSpaceId) {
+        this.activeSpaceId = this.projectSpaces[0].id;
       }
 
-      // 加载已保存的需求数据
-      if (this.project) {
-        const data = this.project.get('data') || {};
-
-        if (data.requirements) {
-          this.requirements = data.requirements;
-        }
-
-        if (data.referenceImages) {
-          this.referenceImages = data.referenceImages;
-        }
-
-        if (data.cadFiles) {
-          this.cadFiles = data.cadFiles;
-        }
-
-        if (data.aiSolution) {
-          this.aiSolution = data.aiSolution;
+      // 模拟加载需求数据
+      this.globalRequirements = {
+        stylePreference: '现代简约',
+        colorScheme: {
+          primary: '#ffffff',
+          secondary: '#f5f5f5',
+          accent: '#3880ff',
+          atmosphere: '温馨'
+        },
+        overallBudget: { min: 15, max: 25 },
+        timeline: '45天',
+        qualityLevel: 'premium',
+        specialRequirements: '需要充足的储物空间',
+        environmentRequirements: {
+          lighting: '明亮自然',
+          ventilation: '良好通风',
+          noise: '隔音处理',
+          temperature: '恒温控制'
         }
+      };
 
-        // 从客户画像加载偏好
-        if (this.customer) {
-          const customerData = this.customer.get('data') || {};
-          const preferences = customerData.preferences || {};
-
-          if (!this.requirements.stylePreference && preferences.style) {
-            this.requirements.stylePreference = preferences.style.join(', ');
-          }
+      // 模拟加载参考图片
+      this.referenceImages = [
+        { id: '1', url: 'https://via.placeholder.com/300x300/3880ff/ffffff?text=客厅', name: '客厅风格参考', type: 'style', uploadTime: new Date(), spaceId: '1', tags: ['现代', '简约'] },
+        { id: '2', url: 'https://via.placeholder.com/300x300/2dd36f/ffffff?text=主卧', name: '主卧风格参考', type: 'style', uploadTime: new Date(), spaceId: '2', tags: ['温馨', '舒适'] }
+      ];
 
-          if (!this.requirements.colorScheme.atmosphere && preferences.colorAtmosphere) {
-            this.requirements.colorScheme.atmosphere = preferences.colorAtmosphere;
-          }
-
-          if (!this.requirements.budget.min && preferences.budget) {
-            const [min, max] = this.parseBudgetRange(preferences.budget);
-            this.requirements.budget.min = min;
-            this.requirements.budget.max = max;
-          }
-        }
-      }
+      this.cdr.markForCheck();
 
     } catch (err) {
       console.error('加载失败:', err);
@@ -194,54 +197,89 @@ export class StageRequirementsComponent implements OnInit {
     }
   }
 
+  // ===== 多空间需求管理方法 =====
+
   /**
-   * 解析预算范围
+   * 切换需求分段
    */
-  parseBudgetRange(budgetStr: string): [number, number] {
-    const match = budgetStr.match(/(\d+)-(\d+)/);
-    if (match) {
-      return [parseInt(match[1]) * 10000, parseInt(match[2]) * 10000];
-    }
-    return [0, 0];
+  onRequirementsSegmentChange(event: any): void {
+    this.requirementsSegment = event.detail.value;
+    this.cdr.markForCheck();
   }
 
   /**
-   * 上传参考图片
+   * 选择需求分段
    */
-  async uploadReferenceImage(event: any) {
-    const file = event.target.files[0];
-    if (!file) return;
+  selectRequirementsSegment(segment: string): void {
+    this.requirementsSegment = segment;
+    this.cdr.markForCheck();
+  }
 
-    // 简单的文件类型验证
-    if (!file.type.startsWith('image/')) {
-      alert('请上传图片文件');
-      return;
-    }
+  /**
+   * 选择空间
+   */
+  selectSpace(spaceId: string): void {
+    this.activeSpaceId = spaceId;
+    this.cdr.markForCheck();
+  }
 
-    // 验证文件大小 (10MB)
-    if (file.size > 10 * 1024 * 1024) {
-      alert('图片大小不能超过10MB');
-      return;
+  /**
+   * 切换空间选择状态
+   */
+  toggleSpaceSelection(spaceId: string): void {
+    const index = this.selectedSpaceIds.indexOf(spaceId);
+    if (index > -1) {
+      this.selectedSpaceIds.splice(index, 1);
+    } else {
+      this.selectedSpaceIds.push(spaceId);
     }
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 上传参考图片
+   */
+  async uploadReferenceImage(event: any, spaceId?: string): Promise<void> {
+    const files = event.target.files;
+    if (!files || files.length === 0) return;
 
     try {
       this.uploading = true;
 
-      //
-      let url = await this.uploadService?.uploadFile(file)
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+
+        // 简单的文件类型验证
+        if (!file.type.startsWith('image/')) {
+          console.warn(`文件 ${file.name} 不是图片格式,跳过`);
+          continue;
+        }
+
+        // 验证文件大小 (10MB)
+        if (file.size > 10 * 1024 * 1024) {
+          console.warn(`文件 ${file.name} 超过10MB限制,跳过`);
+          continue;
+        }
 
-      url&&this.referenceImages.push({
-        url: url,
-        name: file.name,
-        type: 'style',
-        uploadTime: new Date()
-      });
+        // 模拟文件上传
+        const uploadedFile = {
+          id: `img_${Date.now()}_${i}`,
+          url: URL.createObjectURL(file),
+          name: file.name,
+          type: 'style',
+          uploadTime: new Date(),
+          spaceId: spaceId || this.activeSpaceId,
+          tags: []
+        };
+
+        // 添加到参考图片列表
+        this.referenceImages.push(uploadedFile);
+      }
 
-      await this.saveDraft();
+      this.cdr.markForCheck();
 
-    } catch (error: any) {
+    } catch (error) {
       console.error('上传失败:', error);
-      alert('上传失败: ' + (error?.message || '未知错误'));
     } finally {
       this.uploading = false;
     }
@@ -250,49 +288,62 @@ export class StageRequirementsComponent implements OnInit {
   /**
    * 删除参考图片
    */
-  async deleteReferenceImage(index: number) {
-    this.referenceImages.splice(index, 1);
-    await this.saveDraft();
+  async deleteReferenceImage(imageId: string): Promise<void> {
+    try {
+      // 从列表中移除
+      this.referenceImages = this.referenceImages.filter(img => img.id !== imageId);
+      this.cdr.markForCheck();
+
+    } catch (error) {
+      console.error('删除参考图片失败:', error);
+    }
   }
 
   /**
    * 上传CAD文件
    */
-  async uploadCAD(event: any) {
-    const file = event.target.files[0];
-    if (!file) return;
-
-    // 验证文件类型
-    const allowedExtensions = ['.dwg', '.dxf', '.pdf'];
-    const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
-    if (!allowedExtensions.includes(fileExtension)) {
-      alert('请上传CAD文件(.dwg/.dxf)或PDF文件');
-      return;
-    }
-
-    // 验证文件大小 (50MB)
-    if (file.size > 50 * 1024 * 1024) {
-      alert('文件大小不能超过50MB');
-      return;
-    }
+  async uploadCAD(event: any, spaceId?: string): Promise<void> {
+    const files = event.target.files;
+    if (!files || files.length === 0) return;
 
     try {
       this.uploading = true;
 
-      let url = await this.uploadService?.uploadFile(file)
+      for (let i = 0; i < files.length; i++) {
+        const file = files[i];
+
+        // 验证文件类型
+        const allowedExtensions = ['.dwg', '.dxf', '.pdf'];
+        const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+        if (!allowedExtensions.includes(fileExtension)) {
+          console.warn(`文件 ${file.name} 不是支持的CAD格式,跳过`);
+          continue;
+        }
+
+        // 验证文件大小 (50MB)
+        if (file.size > 50 * 1024 * 1024) {
+          console.warn(`文件 ${file.name} 超过50MB限制,跳过`);
+          continue;
+        }
 
-      url&&this.cadFiles.push({
-        url: url,
-        name: file.name,
-        uploadTime: new Date(),
-        size: file.size
-      });
+        // 模拟文件上传
+        const uploadedFile = {
+          id: `cad_${Date.now()}_${i}`,
+          url: URL.createObjectURL(file),
+          name: file.name,
+          uploadTime: new Date(),
+          size: file.size,
+          spaceId: spaceId || this.activeSpaceId
+        };
+
+        // 添加到CAD文件列表
+        this.cadFiles.push(uploadedFile);
+      }
 
-      await this.saveDraft();
+      this.cdr.markForCheck();
 
-    } catch (error: any) {
+    } catch (error) {
       console.error('上传失败:', error);
-      alert('上传失败: ' + (error?.message || '未知错误'));
     } finally {
       this.uploading = false;
     }
@@ -301,181 +352,309 @@ export class StageRequirementsComponent implements OnInit {
   /**
    * 删除CAD文件
    */
-  async deleteCAD(index: number) {
-    this.cadFiles.splice(index, 1);
-    await this.saveDraft();
-  }
-
-  /**
-   * 添加空间
-   */
-  addSpace() {
-    this.requirements.spaces.push({
-      name: '',
-      area: 0,
-      description: '',
-      features: []
-    });
-  }
-
-  /**
-   * 删除空间
-   */
-  removeSpace(index: number) {
-    this.requirements.spaces.splice(index, 1);
-  }
+  async deleteCAD(fileId: string): Promise<void> {
+    try {
+      // 从列表中移除
+      this.cadFiles = this.cadFiles.filter(file => file.id !== fileId);
+      this.cdr.markForCheck();
 
-  /**
-   * 打开色彩分析弹窗
-   */
-  async openColorAnalysis() {
-    // TODO: 实现色彩分析弹窗组件
-    alert('色彩分析功能开发中...');
+    } catch (error) {
+      console.error('删除CAD文件失败:', error);
+    }
   }
 
   /**
    * 生成AI方案
    */
-  async generateAISolution() {
+  async generateAISolution(): Promise<void> {
     if (!this.project || !this.canEdit) return;
 
-    // 验证必填项
-    if (this.requirements.spaces.length === 0) {
-      alert('请至少添加一个空间');
-      return;
-    }
-
-    if (!this.requirements.stylePreference) {
-      alert('请填写风格偏好');
-      return;
-    }
-
     try {
       this.generating = true;
 
-      // 收集参考图片URL
-      const imageUrls = this.referenceImages.map(img => img.url);
-
-      // TODO: 调用AI服务生成方案
-      // 暂时使用mock数据
+      // 模拟AI方案生成
       this.aiSolution = {
         generated: true,
-        content: '基于您的需求,我们为您设计了以下方案...',
-        spaces: this.requirements.spaces.map(space => ({
+        content: `基于您的${this.globalRequirements.stylePreference}风格需求,我们为您设计了以下方案...`,
+        spaces: this.projectSpaces.map(space => ({
+          id: space.id,
           name: space.name,
-          styleDescription: `${this.requirements.stylePreference}风格设计`,
-          colorPalette: ['#FFFFFF', '#F5F5DC', '#8B4513'],
+          type: space.type,
+          styleDescription: `${this.globalRequirements.stylePreference}风格${space.name}设计`,
+          colorPalette: [this.globalRequirements.colorScheme.primary, this.globalRequirements.colorScheme.secondary, this.globalRequirements.colorScheme.accent],
           materials: ['实木', '大理石', '布艺'],
-          furnitureRecommendations: ['沙发', '茶几', '电视柜']
+          furnitureRecommendations: this.getFurnitureRecommendations(space.type),
+          estimatedCost: this.calculateSpaceEstimatedCost(space),
+          timeline: this.calculateSpaceTimeline(space)
         })),
-        estimatedCost: this.requirements.budget.max || 100000,
-        timeline: '预计30-45个工作日'
+        estimatedCost: this.globalRequirements.overallBudget.max || this.calculateTotalEstimatedCost(),
+        timeline: this.calculateTotalTimeline(),
+        crossSpaceCoordination: this.generateCrossSpaceCoordination()
       };
 
-      // 保存到项目数据
-      await this.saveDraft();
-
-      alert('AI方案生成成功');
+      this.cdr.markForCheck();
 
-    } catch (error: any) {
+    } catch (error) {
       console.error('生成失败:', error);
-      alert('生成失败: ' + (error?.message || '未知错误'));
     } finally {
       this.generating = false;
     }
   }
 
+  /**
+   * 获取家具推荐
+   */
+  private getFurnitureRecommendations(spaceType: string): string[] {
+    const recommendations: Record<string, string[]> = {
+      'living_room': ['沙发', '茶几', '电视柜', '书架', '装饰柜'],
+      'bedroom': ['床', '衣柜', '床头柜', '梳妆台', '椅子'],
+      'kitchen': ['橱柜', '冰箱', '灶具', '抽油烟机', '洗碗机'],
+      'bathroom': ['浴室柜', '马桶', '淋浴房', '花洒', '镜子'],
+      'dining_room': ['餐桌', '餐椅', '餐边柜', '酒柜', '装饰品'],
+      'study': ['书桌', '书椅', '书架', '台灯', '电脑桌']
+    };
+    return recommendations[spaceType] || ['基础家具'];
+  }
+
+  /**
+   * 计算空间估算成本
+   */
+  private calculateSpaceEstimatedCost(space: any): number {
+    const baseCostPerSqm: Record<string, number> = {
+      'living_room': 1500,
+      'bedroom': 1200,
+      'kitchen': 2000,
+      'bathroom': 1800,
+      'dining_room': 1300,
+      'study': 1100
+    };
+    const baseCost = (baseCostPerSqm[space.type] || 1000) * (space.area || 10);
+    const qualityMultiplier: Record<string, number> = {
+      'standard': 1.0,
+      'premium': 1.5,
+      'luxury': 2.0
+    };
+    return baseCost * (qualityMultiplier[this.globalRequirements.qualityLevel] || 1.0);
+  }
+
+  /**
+   * 计算总估算成本
+   */
+  private calculateTotalEstimatedCost(): number {
+    return this.projectSpaces.reduce((total, space) => {
+      return total + this.calculateSpaceEstimatedCost(space);
+    }, 0);
+  }
+
+  /**
+   * 计算空间工期
+   */
+  private calculateSpaceTimeline(space: any): string {
+    const baseDays: Record<string, number> = {
+      'living_room': 15,
+      'bedroom': 12,
+      'kitchen': 20,
+      'bathroom': 18,
+      'dining_room': 10,
+      'study': 8
+    };
+    return `${baseDays[space.type] || 10}-15个工作日`;
+  }
+
+  /**
+   * 计算总工期
+   */
+  private calculateTotalTimeline(): string {
+    const totalDays = this.projectSpaces.reduce((total, space) => {
+      const baseDays: Record<string, number> = {
+        'living_room': 15,
+        'bedroom': 12,
+        'kitchen': 20,
+        'bathroom': 18,
+        'dining_room': 10,
+        'study': 8
+      };
+      return total + (baseDays[space.type] || 10);
+    }, 0);
+    return `预计${Math.ceil(totalDays * 0.7)}-${totalDays}个工作日(考虑并行施工)`;
+  }
+
+  /**
+   * 生成跨空间协调方案
+   */
+  private generateCrossSpaceCoordination(): any {
+    return {
+      styleConsistency: {
+        description: '确保各空间风格统一协调',
+        keyElements: ['色彩搭配', '材质选择', '设计元素']
+      },
+      functionalFlow: {
+        description: '优化空间之间的功能流线',
+        considerations: ['动线规划', '采光通风', '噪音控制']
+      },
+      timelineCoordination: {
+        description: '协调各空间施工时间',
+        strategy: '并行施工,关键节点协调'
+      }
+    };
+  }
 
   /**
    * 保存草稿
    */
-  async saveDraft() {
+  async saveDraft(): Promise<void> {
     if (!this.project || !this.canEdit) return;
 
     try {
       this.saving = true;
 
-      const data = this.project.get('data') || {};
-      data.requirements = this.requirements;
-      data.referenceImages = this.referenceImages;
-      data.cadFiles = this.cadFiles;
-
-      if (this.aiSolution) {
-        data.aiSolution = this.aiSolution;
-      }
-
-      this.project.set('data', data);
-      await this.project.save();
+      // 模拟保存逻辑
+      console.log('保存草稿');
 
     } catch (err) {
       console.error('保存失败:', err);
-      alert('保存失败');
     } finally {
       this.saving = false;
+      this.cdr.markForCheck();
     }
   }
 
   /**
    * 提交确认
    */
-  async submitRequirements() {
+  async submitRequirements(): Promise<void> {
     if (!this.project || !this.canEdit) return;
 
-    // 验证必填项
-    if (this.requirements.spaces.length === 0) {
-      alert('请至少添加一个空间');
-      return;
-    }
+    try {
+      this.saving = true;
 
-    if (!this.requirements.stylePreference) {
-      alert('请填写风格偏好');
-      return;
-    }
+      // 模拟提交逻辑
+      console.log('提交需求确认');
 
-    if (!this.aiSolution) {
-      alert('请先生成AI方案');
-      return;
+    } catch (err) {
+      console.error('提交失败:', err);
+    } finally {
+      this.saving = false;
+      this.cdr.markForCheck();
     }
+  }
 
-    try {
-      this.saving = true;
+  // ===== 工具方法 =====
 
-      // 保存数据
-      await this.saveDraft();
+  /**
+   * 获取空间图标
+   */
+  getSpaceIcon(spaceType: string): string {
+    const iconMap: Record<string, string> = {
+      'living_room': 'home',
+      'bedroom': 'bed',
+      'kitchen': 'restaurant',
+      'bathroom': 'water',
+      'dining_room': 'restaurant',
+      'study': 'book',
+      'balcony': 'sunny',
+      'corridor': 'walk',
+      'storage': 'archive',
+      'entrance': 'log-in',
+      'other': 'grid'
+    };
+    return iconMap[spaceType] || 'grid';
+  }
 
-      // 更新项目状态
-      this.project.set('currentStage', '建模');
-      this.project.set('status', '进行中');
+  /**
+   * 获取空间类型名称
+   */
+  getSpaceTypeName(spaceType: string): string {
+    const nameMap: Record<string, string> = {
+      'living_room': '客厅',
+      'bedroom': '卧室',
+      'kitchen': '厨房',
+      'bathroom': '卫生间',
+      'dining_room': '餐厅',
+      'study': '书房',
+      'balcony': '阳台',
+      'corridor': '走廊',
+      'storage': '储物间',
+      'entrance': '玄关',
+      'other': '其他'
+    };
+    return nameMap[spaceType] || '其他';
+  }
 
-      await this.project.save();
+  /**
+   * 获取空间显示名称
+   */
+  getSpaceDisplayName(space: any): string {
+    return space.name || this.getSpaceTypeName(space.type);
+  }
 
-      alert('需求确认完成,进入交付执行阶段');
+  /**
+   * 获取当前空间的需求
+   */
+  getCurrentSpaceRequirement(): any {
+    if (!this.activeSpaceId) return null;
+    return this.spaceRequirements.find(req => req.spaceId === this.activeSpaceId) || {
+      spaceId: this.activeSpaceId,
+      colorRequirement: {},
+      spaceStructureRequirement: {},
+      materialRequirement: {},
+      lightingRequirement: {},
+      specificRequirements: ''
+    };
+  }
 
-    } catch (err) {
-      console.error('提交失败:', err);
-      alert('提交失败');
-    } finally {
-      this.saving = false;
+  /**
+   * 获取当前空间特殊需求的 getter/setter
+   */
+  get currentSpaceSpecificRequirements(): string {
+    const requirement = this.getCurrentSpaceRequirement();
+    return requirement?.specificRequirements || '';
+  }
+
+  set currentSpaceSpecificRequirements(value: string) {
+    const requirement = this.getCurrentSpaceRequirement();
+    if (requirement) {
+      requirement.specificRequirements = value;
     }
   }
 
   /**
-   * 格式化文件大小
+   * 获取当前空间的评价
    */
-  formatFileSize(bytes: number): string {
-    if (bytes < 1024) return bytes + ' B';
-    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
-    return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
+  getCurrentSpaceFeedback(): any {
+    return null;
+  }
+
+  /**
+   * 过滤参考图片
+   */
+  getFilteredReferenceImages(): typeof this.referenceImages {
+    if (!this.isMultiSpaceProject || !this.activeSpaceId) {
+      return this.referenceImages;
+    }
+    return this.referenceImages.filter(img => !img.spaceId || img.spaceId === this.activeSpaceId);
+  }
+
+  /**
+   * 过滤CAD文件
+   */
+  getFilteredCADFiles(): typeof this.cadFiles {
+    if (!this.isMultiSpaceProject || !this.activeSpaceId) {
+      return this.cadFiles;
+    }
+    return this.cadFiles.filter(file => !file.spaceId || file.spaceId === this.activeSpaceId);
   }
 
   /**
    * 获取图片类型标签
    */
   getImageTypeLabel(type: string): string {
-    const map: any = {
+    const map: Record<string, string> = {
       'style': '风格参考',
       'space': '空间布局',
-      'material': '材质参考'
+      'material': '材质参考',
+      'color': '色彩参考',
+      'layout': '布局参考'
     };
     return map[type] || '其他';
   }
@@ -484,11 +663,187 @@ export class StageRequirementsComponent implements OnInit {
    * 获取图片类型颜色
    */
   getImageTypeColor(type: string): string {
-    const map: any = {
+    const map: Record<string, string> = {
       'style': 'primary',
       'space': 'secondary',
-      'material': 'tertiary'
+      'material': 'tertiary',
+      'color': 'success',
+      'layout': 'warning'
     };
     return map[type] || 'medium';
   }
-}
+
+  /**
+   * 获取空间的参考图片
+   */
+  getSpaceReferenceImages(spaceId?: string): any[] {
+    if (!spaceId) return this.referenceImages;
+    return this.referenceImages.filter(img => img.spaceId === spaceId);
+  }
+
+  /**
+   * 格式化文件大小
+   */
+  formatFileSize(bytes: number): string {
+    if (bytes === 0) return '0 B';
+    const k = 1024;
+    const sizes = ['B', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+  }
+
+  /**
+   * 获取质量等级名称
+   */
+  getQualityLevelName(level: string): string {
+    const levelMap: Record<string, string> = {
+      'standard': '标准',
+      'premium': '高级',
+      'luxury': '豪华'
+    };
+    return levelMap[level] || '标准';
+  }
+
+  /**
+   * 获取氛围名称
+   */
+  getAtmosphereName(atmosphere: string): string {
+    const atmosphereMap: Record<string, string> = {
+      'warm': '温馨',
+      'luxury': '高级',
+      'minimal': '简约',
+      'fashion': '时尚',
+      'nordic': '北欧',
+      'chinese': '中式',
+      'european': '欧式'
+    };
+    return atmosphereMap[atmosphere] || atmosphere;
+  }
+
+  /**
+   * 获取跨空间需求类型名称
+   */
+  getCrossSpaceRequirementTypeName(type: string): string {
+    const typeMap: Record<string, string> = {
+      'style': '风格统一',
+      'color': '色彩协调',
+      'material': '材质搭配',
+      'lighting': '照明衔接',
+      'functional': '功能关联',
+      'structural': '结构连接'
+    };
+    return typeMap[type] || type;
+  }
+
+  /**
+   * 计算需求完成度
+   */
+  calculateRequirementsCompleteness(): number {
+    let completedItems = 0;
+    let totalItems = 0;
+
+    // 全局需求检查
+    if (this.globalRequirements.stylePreference) completedItems++;
+    totalItems++;
+    if (this.globalRequirements.colorScheme.primary) completedItems++;
+    totalItems++;
+    if (this.globalRequirements.overallBudget.max > 0) completedItems++;
+    totalItems++;
+    if (this.globalRequirements.timeline) completedItems++;
+    totalItems++;
+
+    // 空间需求检查
+    for (const space of this.projectSpaces) {
+      const spaceFeedback = this.spaceRequirements.find(req => req.spaceId === space.id);
+      if (spaceFeedback) {
+        completedItems++;
+      }
+      totalItems++;
+    }
+
+    return totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0;
+  }
+
+  /**
+   * 计算空间完成度
+   */
+  calculateSpaceCompletion(_spaceId: string): number {
+    // 简化计算,实际应该基于空间的具体需求完成情况
+    return Math.floor(Math.random() * 100);
+  }
+
+  /**
+   * 创建跨空间需求
+   */
+  async createCrossSpaceRequirement(requirement?: any): Promise<void> {
+    console.log('创建跨空间需求:', requirement || {});
+  }
+
+  /**
+   * 删除跨空间需求
+   */
+  async deleteCrossSpaceRequirement(requirementId: string): Promise<void> {
+    console.log('删除跨空间需求:', requirementId);
+  }
+
+  /**
+   * 获取工序颜色
+   */
+  getProcessColor(spaceType: string): string {
+    const colorMap: Record<string, string> = {
+      'living_room': 'primary',
+      'bedroom': 'secondary',
+      'kitchen': 'tertiary',
+      'bathroom': 'success',
+      'dining_room': 'warning',
+      'study': 'medium'
+    };
+    return colorMap[spaceType] || 'primary';
+  }
+
+  /**
+   * 获取图片类型徽章类名
+   */
+  getImageTypeBadgeClass(imageType: string): string {
+    const colorClass = this.getImageTypeColor(imageType);
+    return `badge-${colorClass}`;
+  }
+
+  /**
+   * 获取工序徽章类名
+   */
+  getProcessBadgeClass(spaceType: string): string {
+    const colorClass = this.getProcessColor(spaceType);
+    return `badge-${colorClass}`;
+  }
+
+  /**
+   * 获取关联空间ID列表
+   */
+  getRelatedSpaceIds(requirement: any): string[] {
+    if (!requirement) return [];
+    const ids = [requirement.primarySpaceId];
+    if (requirement.relatedSpaceIds && Array.isArray(requirement.relatedSpaceIds)) {
+      ids.push(...requirement.relatedSpaceIds);
+    }
+    return ids.filter(id => id); // 过滤掉空值
+  }
+
+  /**
+   * 根据空间ID获取空间显示名称(用于模板)
+   */
+  getSpaceDisplayNameById(spaceId: string): string {
+    const space = this.projectSpaces.find(s => s.id === spaceId);
+    return this.getSpaceDisplayName(space);
+  }
+
+  /**
+   * 触发文件选择器点击
+   */
+  triggerFileClick(inputId: string): void {
+    const element = document.getElementById(inputId) as HTMLInputElement;
+    if (element) {
+      element.click();
+    }
+  }
+}

+ 495 - 0
src/modules/project/services/multi-space.service.ts

@@ -0,0 +1,495 @@
+import { Injectable } from '@angular/core';
+import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
+
+const Parse = FmodeParse.with('nova');
+
+export interface ProjectSpace {
+  id: string;
+  name: string;
+  type: string;
+  area?: number;
+  priority: number;
+  status: string;
+  complexity: string;
+  metadata?: any;
+  estimatedBudget?: number;
+  estimatedDuration?: number;
+  order: number;
+}
+
+export interface SpaceProgress {
+  spaceId: string;
+  stage: string;
+  progress: number;
+  status: string;
+  timeline?: any[];
+  blockers?: string[];
+  estimatedCompletion?: Date;
+  actualCompletion?: Date;
+}
+
+export interface SpaceRequirement {
+  spaceId: string;
+  spaceName: string;
+  spaceType: string;
+  colorRequirement: any;
+  spaceStructureRequirement: any;
+  materialRequirement: any;
+  lightingRequirement: any;
+  specificRequirements: any;
+  referenceImages?: string[];
+  referenceFiles?: any[];
+}
+
+export interface CrossSpaceRequirement {
+  id: string;
+  type: string;
+  description: string;
+  primarySpaceId: string;
+  relatedSpaceIds: string[];
+  requirements: any;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class MultiSpaceService {
+
+  constructor() {}
+
+  /**
+   * 创建项目空间
+   */
+  async createSpace(projectId: string, spaceData: Partial<ProjectSpace>): Promise<ProjectSpace> {
+    try {
+      const ProjectSpace = Parse.Object.extend('ProjectSpace');
+      const space = new ProjectSpace();
+
+      // 获取项目
+      const projectQuery = new Parse.Query('Project');
+      const project = await projectQuery.get(projectId);
+
+      // 设置空间字段
+      space.set('project', project);
+      space.set('name', spaceData.name || '');
+      space.set('type', spaceData.type || 'other');
+      space.set('area', spaceData.area || 0);
+      space.set('priority', spaceData.priority || 5);
+      space.set('status', spaceData.status || 'not_started');
+      space.set('complexity', spaceData.complexity || 'medium');
+      space.set('metadata', spaceData.metadata || {});
+      space.set('estimatedBudget', spaceData.estimatedBudget || 0);
+      space.set('estimatedDuration', spaceData.estimatedDuration || 0);
+      space.set('order', spaceData.order || 0);
+
+      const savedSpace = await space.save();
+      return this.parseSpaceData(savedSpace);
+
+    } catch (error) {
+      console.error('创建空间失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取项目空间列表
+   */
+  async getProjectSpaces(projectId: string): Promise<ProjectSpace[]> {
+    try {
+      const query = new Parse.Query('ProjectSpace');
+      query.equalTo('project', {
+        __type: 'Pointer',
+        className: 'Project',
+        objectId: projectId
+      });
+      query.ascending('order');
+      query.equalTo('isDeleted', false);
+
+      const results = await query.find();
+      return results.map(space => this.parseSpaceData(space));
+
+    } catch (error) {
+      console.error('获取项目空间失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 更新空间信息
+   */
+  async updateSpace(spaceId: string, updateData: Partial<ProjectSpace>): Promise<ProjectSpace> {
+    try {
+      const query = new Parse.Query('ProjectSpace');
+      const space = await query.get(spaceId);
+
+      // 更新字段
+      if (updateData.name !== undefined) space.set('name', updateData.name);
+      if (updateData.type !== undefined) space.set('type', updateData.type);
+      if (updateData.area !== undefined) space.set('area', updateData.area);
+      if (updateData.priority !== undefined) space.set('priority', updateData.priority);
+      if (updateData.status !== undefined) space.set('status', updateData.status);
+      if (updateData.complexity !== undefined) space.set('complexity', updateData.complexity);
+      if (updateData.metadata !== undefined) space.set('metadata', updateData.metadata);
+      if (updateData.estimatedBudget !== undefined) space.set('estimatedBudget', updateData.estimatedBudget);
+      if (updateData.estimatedDuration !== undefined) space.set('estimatedDuration', updateData.estimatedDuration);
+      if (updateData.order !== undefined) space.set('order', updateData.order);
+
+      const savedSpace = await space.save();
+      return this.parseSpaceData(savedSpace);
+
+    } catch (error) {
+      console.error('更新空间失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 删除空间
+   */
+  async deleteSpace(spaceId: string): Promise<void> {
+    try {
+      const query = new Parse.Query('ProjectSpace');
+      const space = await query.get(spaceId);
+      space.set('isDeleted', true);
+      await space.save();
+
+    } catch (error) {
+      console.error('删除空间失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 更新空间进度
+   */
+  async updateSpaceProgress(progressData: SpaceProgress): Promise<void> {
+    try {
+      // 查找现有进度记录
+      const query = new Parse.Query('SpaceProgress');
+      query.equalTo('spaceId', {
+        __type: 'Pointer',
+        className: 'ProjectSpace',
+        objectId: progressData.spaceId
+      });
+      query.equalTo('stage', progressData.stage);
+
+      let progressRecord = await query.first();
+
+      if (!progressRecord) {
+        // 创建新记录
+        const SpaceProgress = Parse.Object.extend('SpaceProgress');
+        progressRecord = new SpaceProgress();
+        progressRecord.set('spaceId', {
+          __type: 'Pointer',
+          className: 'ProjectSpace',
+          objectId: progressData.spaceId
+        });
+      }
+
+      // 更新进度数据
+      progressRecord.set('stage', progressData.stage);
+      progressRecord.set('progress', progressData.progress);
+      progressRecord.set('status', progressData.status);
+      progressRecord.set('timeline', progressData.timeline || []);
+      progressRecord.set('blockers', progressData.blockers || []);
+      progressRecord.set('estimatedCompletion', progressData.estimatedCompletion);
+      progressRecord.set('actualCompletion', progressData.actualCompletion);
+
+      await progressRecord.save();
+
+    } catch (error) {
+      console.error('更新空间进度失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取空间进度
+   */
+  async getSpaceProgress(spaceId: string, stage?: string): Promise<SpaceProgress[]> {
+    try {
+      const query = new Parse.Query('SpaceProgress');
+      query.equalTo('spaceId', {
+        __type: 'Pointer',
+        className: 'ProjectSpace',
+        objectId: spaceId
+      });
+
+      if (stage) {
+        query.equalTo('stage', stage);
+      }
+
+      const results = await query.find();
+      return results.map(record => ({
+        spaceId: record.get('spaceId')?.objectId,
+        stage: record.get('stage'),
+        progress: record.get('progress'),
+        status: record.get('status'),
+        timeline: record.get('timeline'),
+        blockers: record.get('blockers'),
+        estimatedCompletion: record.get('estimatedCompletion'),
+        actualCompletion: record.get('actualCompletion')
+      }));
+
+    } catch (error) {
+      console.error('获取空间进度失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 创建空间依赖关系
+   */
+  async createSpaceDependency(
+    projectId: string,
+    fromSpaceId: string,
+    toSpaceId: string,
+    dependencyType: string,
+    description: string
+  ): Promise<void> {
+    try {
+      const SpaceDependency = Parse.Object.extend('SpaceDependency');
+      const dependency = new SpaceDependency();
+
+      // 获取项目
+      const projectQuery = new Parse.Query('Project');
+      const project = await projectQuery.get(projectId);
+
+      dependency.set('project', project);
+      dependency.set('fromSpace', {
+        __type: 'Pointer',
+        className: 'ProjectSpace',
+        objectId: fromSpaceId
+      });
+      dependency.set('toSpace', {
+        __type: 'Pointer',
+        className: 'ProjectSpace',
+        objectId: toSpaceId
+      });
+      dependency.set('type', dependencyType);
+      dependency.set('description', description);
+      dependency.set('status', 'pending');
+      dependency.set('confidence', 0.8);
+
+      await dependency.save();
+
+    } catch (error) {
+      console.error('创建空间依赖失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取空间依赖关系
+   */
+  async getSpaceDependencies(projectId: string): Promise<any[]> {
+    try {
+      const query = new Parse.Query('SpaceDependency');
+      query.equalTo('project', {
+        __type: 'Pointer',
+        className: 'Project',
+        objectId: projectId
+      });
+      query.include('fromSpace', 'toSpace');
+
+      const results = await query.find();
+      return results.map(record => ({
+        id: record.id,
+        fromSpace: record.get('fromSpace'),
+        toSpace: record.get('toSpace'),
+        type: record.get('type'),
+        description: record.get('description'),
+        status: record.get('status'),
+        confidence: record.get('confidence')
+      }));
+
+    } catch (error) {
+      console.error('获取空间依赖失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 保存空间需求
+   */
+  async saveSpaceRequirements(
+    projectId: string,
+    spaceId: string,
+    requirements: SpaceRequirement
+  ): Promise<void> {
+    try {
+      // 查找多空间需求记录
+      const query = new Parse.Query('MultiSpaceRequirement');
+      query.equalTo('project', {
+        __type: 'Pointer',
+        className: 'Project',
+        objectId: projectId
+      });
+
+      let multiSpaceReq = await query.first();
+
+      if (!multiSpaceReq) {
+        // 创建多空间需求记录
+        const MultiSpaceRequirement = Parse.Object.extend('MultiSpaceRequirement');
+        multiSpaceReq = new MultiSpaceRequirement();
+        multiSpaceReq.set('project', {
+          __type: 'Pointer',
+          className: 'Project',
+          objectId: projectId
+        });
+        multiSpaceReq.set('requirementId', `req_${Date.now()}`);
+        multiSpaceReq.set('globalRequirements', {});
+        multiSpaceReq.set('spaceRequirements', []);
+        multiSpaceReq.set('crossSpaceRequirements', []);
+        multiSpaceReq.set('status', 'draft');
+        multiSpaceReq.set('completenessCheck', {});
+        multiSpaceReq.set('createdAt', new Date());
+        multiSpaceReq.set('updatedAt', new Date());
+
+        await multiSpaceReq.save();
+      }
+
+      // 创建空间需求记录
+      const SpaceRequirement = Parse.Object.extend('SpaceRequirement');
+      const spaceRequirement = new SpaceRequirement();
+
+      spaceRequirement.set('multiSpaceReqId', multiSpaceReq);
+      spaceRequirement.set('spaceId', {
+        __type: 'Pointer',
+        className: 'ProjectSpace',
+        objectId: spaceId
+      });
+      spaceRequirement.set('spaceName', requirements.spaceName);
+      spaceRequirement.set('spaceType', requirements.spaceType);
+      spaceRequirement.set('colorRequirement', requirements.colorRequirement);
+      spaceRequirement.set('spaceStructureRequirement', requirements.spaceStructureRequirement);
+      spaceRequirement.set('materialRequirement', requirements.materialRequirement);
+      spaceRequirement.set('lightingRequirement', requirements.lightingRequirement);
+      spaceRequirement.set('specificRequirements', requirements.specificRequirements);
+
+      await spaceRequirement.save();
+
+    } catch (error) {
+      console.error('保存空间需求失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取空间需求
+   */
+  async getSpaceRequirements(projectId: string, spaceId?: string): Promise<SpaceRequirement[]> {
+    try {
+      // 获取多空间需求记录
+      const multiSpaceQuery = new Parse.Query('MultiSpaceRequirement');
+      multiSpaceQuery.equalTo('project', {
+        __type: 'Pointer',
+        className: 'Project',
+        objectId: projectId
+      });
+
+      const multiSpaceReq = await multiSpaceQuery.first();
+      if (!multiSpaceReq) return [];
+
+      // 获取空间需求记录
+      const query = new Parse.Query('SpaceRequirement');
+      query.equalTo('multiSpaceReqId', multiSpaceReq);
+
+      if (spaceId) {
+        query.equalTo('spaceId', {
+          __type: 'Pointer',
+          className: 'ProjectSpace',
+          objectId: spaceId
+        });
+      }
+
+      const results = await query.find();
+      return results.map(record => ({
+        spaceId: record.get('spaceId')?.objectId,
+        spaceName: record.get('spaceName'),
+        spaceType: record.get('spaceType'),
+        colorRequirement: record.get('colorRequirement'),
+        spaceStructureRequirement: record.get('spaceStructureRequirement'),
+        materialRequirement: record.get('materialRequirement'),
+        lightingRequirement: record.get('lightingRequirement'),
+        specificRequirements: record.get('specificRequirements'),
+        referenceImages: record.get('referenceImages') || [],
+        referenceFiles: record.get('referenceFiles') || []
+      }));
+
+    } catch (error) {
+      console.error('获取空间需求失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 计算空间完成进度
+   */
+  calculateSpaceProgress(spaceId: string, allStages: string[]): number {
+    // 这里需要从缓存或数据库中获取各阶段进度
+    // 暂时返回模拟数据
+    return Math.floor(Math.random() * 100);
+  }
+
+  /**
+   * 获取空间类型图标
+   */
+  getSpaceIcon(spaceType: string): string {
+    const iconMap: Record<string, string> = {
+      'living_room': 'living-room',
+      'bedroom': 'bedroom',
+      'kitchen': 'kitchen',
+      'bathroom': 'bathroom',
+      'dining_room': 'dining-room',
+      'study': 'study',
+      'balcony': 'balcony',
+      'corridor': 'corridor',
+      'storage': 'storage',
+      'entrance': 'entrance',
+      'other': 'room'
+    };
+
+    return iconMap[spaceType] || 'room';
+  }
+
+  /**
+   * 获取空间类型名称
+   */
+  getSpaceTypeName(spaceType: string): string {
+    const nameMap: Record<string, string> = {
+      'living_room': '客厅',
+      'bedroom': '卧室',
+      'kitchen': '厨房',
+      'bathroom': '卫生间',
+      'dining_room': '餐厅',
+      'study': '书房',
+      'balcony': '阳台',
+      'corridor': '走廊',
+      'storage': '储物间',
+      'entrance': '玄关',
+      'other': '其他'
+    };
+
+    return nameMap[spaceType] || '其他';
+  }
+
+  /**
+   * 解析空间数据
+   */
+  private parseSpaceData(space: any): ProjectSpace {
+    return {
+      id: space.id,
+      name: space.get('name'),
+      type: space.get('type'),
+      area: space.get('area'),
+      priority: space.get('priority'),
+      status: space.get('status'),
+      complexity: space.get('complexity'),
+      metadata: space.get('metadata'),
+      estimatedBudget: space.get('estimatedBudget'),
+      estimatedDuration: space.get('estimatedDuration'),
+      order: space.get('order')
+    };
+  }
+}

+ 338 - 0
src/modules/project/services/project-file.service.ts

@@ -0,0 +1,338 @@
+import { Injectable } from '@angular/core';
+import { NovaStorage, NovaFile } from 'fmode-ng/core';
+import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
+
+const Parse = FmodeParse.with('nova');
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ProjectFileService {
+
+  /**
+   * 上传项目文件并保存到ProjectFile表
+   * @param file 要上传的文件
+   * @param projectId 项目ID
+   * @param fileType 文件类型
+   * @param spaceId 空间ID(可选)
+   * @param stage 项目阶段(可选)
+   * @param additionalMetadata 额外元数据(可选)
+   * @param onProgress 上传进度回调
+   * @returns 上传后的NovaFile对象
+   */
+  async uploadProjectFile(
+    file: File,
+    projectId: string,
+    fileType: string,
+    spaceId?: string,
+    stage?: string,
+    additionalMetadata?: any,
+    onProgress?: (progress: number) => void
+  ): Promise<NovaFile> {
+    try {
+      // 获取公司ID
+      const cid = localStorage.getItem('company');
+      if (!cid) {
+        throw new Error('公司ID未找到');
+      }
+
+      // 初始化存储
+      const storage = await NovaStorage.withCid(cid);
+
+      // 构建prefixKey
+      let prefixKey = `project/${projectId}`;
+      if (spaceId) {
+        prefixKey += `/space/${spaceId}`;
+      }
+      if (stage) {
+        prefixKey += `/stage/${stage}`;
+      }
+
+      // 上传文件
+      const uploadedFile: NovaFile = await storage.upload(file, {
+        prefixKey,
+        onProgress: (progress) => {
+          if (onProgress) {
+            onProgress(progress.total.percent);
+          }
+        }
+      });
+
+      // 保存到Attachment表
+      await this.saveToAttachmentTable(uploadedFile, projectId, fileType, spaceId, stage, additionalMetadata);
+
+      return uploadedFile;
+
+    } catch (error) {
+      console.error('项目文件上传失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 保存文件信息到Attachment表
+   */
+  private async saveToAttachmentTable(
+    file: NovaFile,
+    projectId: string,
+    fileType: string,
+    spaceId?: string,
+    stage?: string,
+    additionalMetadata?: any
+  ): Promise<FmodeObject> {
+    const Attachment = Parse.Object.extend('Attachment');
+    const attachment = new Attachment();
+
+    // 设置基本字段
+    attachment.set('size', file.size);
+    attachment.set('url', file.url);
+    attachment.set('name', file.name);
+    attachment.set('mime', file.type);
+    attachment.set('md5', file.md5);
+    attachment.set('metadata', {
+      ...file.metadata,
+      projectId,
+      fileType,
+      spaceId,
+      stage,
+      ...additionalMetadata
+    });
+
+    // 设置关联关系
+    const cid = localStorage.getItem('company');
+    if (cid) {
+      const companyQuery = new Parse.Query('Company');
+      companyQuery.equalTo('corpId', cid);
+      const company = await companyQuery.first();
+      if (company) {
+        attachment.set('company', company);
+      }
+    }
+
+    // 设置当前用户
+    const currentUser = Parse.User.current();
+    if (currentUser) {
+      attachment.set('user', currentUser);
+    }
+
+    const savedAttachment = await attachment.save();
+    return savedAttachment;
+  }
+
+  /**
+   * 保存到ProjectFile表
+   */
+  async saveToProjectFile(
+    attachment: FmodeObject,
+    projectId: string,
+    fileType: string,
+    spaceId?: string,
+    stage?: string
+  ): Promise<FmodeObject> {
+    const ProjectFile = Parse.Object.extend('ProjectFile');
+    const projectFile = new ProjectFile();
+
+    // 获取项目
+    const projectQuery = new Parse.Query('Project');
+    const project = await projectQuery.get(projectId);
+
+    // 设置字段
+    projectFile.set('project', project);
+    projectFile.set('attach', attachment);
+    projectFile.set('fileType', fileType);
+    projectFile.set('fileUrl', attachment.get('url'));
+    projectFile.set('fileName', attachment.get('name'));
+    projectFile.set('fileSize', attachment.get('size'));
+
+    if (stage) {
+      projectFile.set('stage', stage);
+    }
+
+    // 设置扩展数据
+    const data = {
+      spaceId,
+      uploadedAt: new Date(),
+      fileType,
+      metadata: attachment.get('metadata')
+    };
+    projectFile.set('data', data);
+
+    // 设置上传者
+    const currentUser = Parse.User.current();
+    if (currentUser) {
+      projectFile.set('uploadedBy', currentUser);
+    }
+
+    const savedProjectFile = await projectFile.save();
+    return savedProjectFile;
+  }
+
+  /**
+   * 删除项目文件
+   */
+  async deleteProjectFile(projectFileId: string): Promise<void> {
+    try {
+      // 删除ProjectFile记录
+      const ProjectFile = Parse.Object.extend('ProjectFile');
+      const query = new Parse.Query(ProjectFile);
+      const projectFile = await query.get(projectFileId);
+
+      // 删除Attachment记录
+      const attachment = projectFile.get('attach');
+      if (attachment) {
+        await attachment.destroy();
+      }
+
+      // 删除ProjectFile记录
+      await projectFile.destroy();
+
+    } catch (error) {
+      console.error('删除项目文件失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取项目文件列表
+   */
+  async getProjectFiles(
+    projectId: string,
+    filters?: {
+      fileType?: string;
+      spaceId?: string;
+      stage?: string;
+    }
+  ): Promise<FmodeObject[]> {
+    try {
+      const ProjectFile = Parse.Object.extend('ProjectFile');
+      const query = new Parse.Query(ProjectFile);
+
+      // 关联项目查询
+      const Project = Parse.Object.extend('Project');
+      const projectQuery = new Parse.Query(Project);
+      projectQuery.equalTo('objectId', projectId);
+
+      query.matchesQuery('project', projectQuery);
+      query.include('attach', 'uploadedBy');
+      query.descending('createdAt');
+
+      // 应用过滤器
+      if (filters?.fileType) {
+        query.equalTo('fileType', filters.fileType);
+      }
+      if (filters?.stage) {
+        query.equalTo('stage', filters.stage);
+      }
+
+      const results = await query.find();
+
+      // 如果有空间ID过滤,从data中筛选
+      if (filters?.spaceId) {
+        return results.filter(result => {
+          const data = result.get('data');
+          return data?.spaceId === filters.spaceId;
+        });
+      }
+
+      return results;
+
+    } catch (error) {
+      console.error('获取项目文件列表失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 批量上传文件
+   */
+  async uploadMultipleFiles(
+    files: File[],
+    projectId: string,
+    fileType: string,
+    spaceId?: string,
+    stage?: string,
+    onProgress?: (fileIndex: number, progress: number) => void
+  ): Promise<NovaFile[]> {
+    const results: NovaFile[] = [];
+
+    for (let i = 0; i < files.length; i++) {
+      const file = files[i];
+
+      try {
+        const uploadedFile = await this.uploadProjectFile(
+          file,
+          projectId,
+          fileType,
+          spaceId,
+          stage,
+          undefined,
+          (progress) => {
+            if (onProgress) {
+              onProgress(i, progress);
+            }
+          }
+        );
+        results.push(uploadedFile);
+      } catch (error) {
+        console.error(`文件 ${file.name} 上传失败:`, error);
+        // 继续上传其他文件
+      }
+    }
+
+    return results;
+  }
+
+  /**
+   * 验证文件
+   */
+  validateFile(file: File, maxSize: number = 50 * 1024 * 1024, allowedTypes?: string[]): boolean {
+    // 检查文件大小
+    if (file.size > maxSize) {
+      return false;
+    }
+
+    // 检查文件类型
+    if (allowedTypes && !allowedTypes.includes(file.type)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * 获取文件类型标签
+   */
+  getFileTypeLabel(fileType: string): string {
+    const typeMap: Record<string, string> = {
+      'image/jpeg': '图片',
+      'image/png': '图片',
+      'image/gif': '图片',
+      'image/webp': '图片',
+      'video/mp4': '视频',
+      'video/mov': '视频',
+      'video/avi': '视频',
+      'application/pdf': 'PDF',
+      'application/msword': 'Word',
+      'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word',
+      'application/vnd.ms-excel': 'Excel',
+      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel',
+      'application/vnd.ms-powerpoint': 'PPT',
+      'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPT'
+    };
+
+    return typeMap[fileType] || '其他';
+  }
+
+  /**
+   * 格式化文件大小
+   */
+  formatFileSize(bytes: number): string {
+    if (bytes === 0) return '0 B';
+
+    const k = 1024;
+    const sizes = ['B', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+  }
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio