Explorar el Código

feat:数据显示修复

徐福静0235668 hace 1 semana
padre
commit
04499b4021

+ 466 - 0
SPACE_SYNC_ANALYSIS_PART1.md

@@ -0,0 +1,466 @@
+# 项目空间数量同步分析报告 - 第一部分
+
+## 📋 执行摘要
+
+本报告分析了订单分配阶段、报价明细、交付执行板块以及设计师分配弹窗中的**空间数量同步机制**。
+
+### 核心发现:
+- ✅ **空间数据存储**:使用 `Product` 表(对应 `ProductSpace` 服务)
+- ✅ **同步机制**:通过 `ProductSpaceService.getProjectProductSpaces()` 统一查询
+- ✅ **数据字段**:Project.data 中存储报价信息,Product 表存储空间产品信息
+- ⚠️ **同步状态**:**部分同步**,存在潜在的数据不一致风险
+
+---
+
+## 1. 空间数据存储字段
+
+### 1.1 Product 表(ProductSpace)
+
+**表名**:`Product`(通过 `ProductSpaceService` 访问)
+
+**核心字段**:
+```typescript
+interface ProductSpace {
+  id: string;                    // 产品ID
+  name: string;                  // 空间名称(如"客厅"、"卧室")
+  type: string;                  // 空间类型(living_room, bedroom, kitchen等)
+  area?: number;                 // 面积(㎡)
+  priority?: number;             // 优先级
+  status: string;                // 状态(not_started, in_progress, completed等)
+  complexity?: string;           // 复杂度(low, medium, high)
+  estimatedBudget?: number;      // 预算
+  order?: number;                // 排序顺序
+  metadata?: {
+    description?: string;        // 描述
+  };
+  project: Pointer;              // 关联的项目指针
+  company: Pointer;              // 关联的公司指针
+}
+```
+
+**存储位置**:Parse Server 的 `Product` 表
+
+**查询方法**:
+```typescript
+// ProductSpaceService
+async getProjectProductSpaces(projectId: string): Promise<ProductSpace[]>
+```
+
+### 1.2 Project 表(项目数据)
+
+**表名**:`Project`
+
+**空间相关字段**:
+```typescript
+interface ProjectData {
+  // 报价信息(存储在 project.data.quotation)
+  quotation?: {
+    spaces: Array<{
+      name: string;              // 空间名称
+      spaceId?: string;          // 关联的Product ID
+      processes: {
+        modeling: { enabled, price, quantity };
+        softDecor: { enabled, price, quantity };
+        rendering: { enabled, price, quantity };
+        postProcess: { enabled, price, quantity };
+      };
+      subtotal: number;          // 小计
+    }>;
+    total: number;               // 总价
+    spaceBreakdown: Array<{
+      spaceName: string;
+      spaceId: string;
+      amount: number;
+      percentage: number;
+    }>;
+  };
+  
+  // 特殊需求(存储在 project.data.spaceSpecialRequirements)
+  spaceSpecialRequirements?: {
+    [spaceId: string]: {
+      content: string;
+      updatedAt: Date;
+      updatedBy: string;
+    };
+  };
+}
+```
+
+**存储位置**:Parse Server 的 `Project` 表的 `data` 字段(JSON格式)
+
+---
+
+## 2. 空间数量同步流程
+
+### 2.1 订单分配阶段(stage-order.component.ts)
+
+**文件**:`src/modules/project/pages/project-detail/stages/stage-order.component.ts`
+
+**空间加载流程**:
+
+```typescript
+// 第695-758行:loadProjectSpaces()
+async loadProjectSpaces(): Promise<void> {
+  // 1. 从ProductSpace表加载空间数据
+  this.projectSpaces = await this.productSpaceService.getProjectProductSpaces(
+    this.project.id
+  );
+
+  // 2. 如果没有空间数据,但从项目数据中有报价信息,则转换创建默认空间
+  if (this.projectSpaces.length === 0) {
+    const data = this.project.get('data') || {};
+    if (data.quotation?.spaces) {
+      await this.createSpacesFromQuotation(data.quotation.spaces);
+    }
+  }
+
+  // 3. ⭐ 同步缺失空间:当已有部分 ProductSpace 时,补齐与报价明细一致
+  if (this.projectSpaces.length > 0) {
+    const quotationSpaces = Array.isArray(data.quotation?.spaces) 
+      ? data.quotation.spaces 
+      : [];
+    
+    // 找出报价中有但Product表中没有的空间
+    const missing = quotationSpaces.filter((s: any) => {
+      const n = (s?.name || '').trim().toLowerCase();
+      return n && !existingNames.has(n);
+    });
+    
+    // 为缺失的空间创建Product记录
+    if (missing.length > 0) {
+      for (const spaceData of missing) {
+        await this.productSpaceService.createProductSpace(
+          this.project!.id,
+          {
+            name: spaceData.name,
+            type: this.inferSpaceType(spaceData.name),
+            status: 'not_started',
+            complexity: 'medium',
+            order: this.projectSpaces.length
+          }
+        );
+      }
+      // 重新加载补齐后的空间列表
+      this.projectSpaces = await this.productSpaceService
+        .getProjectProductSpaces(this.project.id);
+    }
+  }
+}
+```
+
+**关键方法**:
+
+| 方法名 | 功能 | 行号 |
+|--------|------|------|
+| `loadProjectSpaces()` | 加载项目空间 | 695-758 |
+| `createSpacesFromQuotation()` | 从报价创建空间 | 763-784 |
+| `inferSpaceType()` | 推断空间类型 | 789-797 |
+| `calculateSpaceRate()` | 计算空间预算 | 802-811 |
+| `regenerateQuotationFromSpaces()` | 从空间重新生成报价 | 950-996 |
+| `updateSpaceBreakdown()` | 更新空间占比 | 1001-1008 |
+
+**数据同步逻辑**:
+1. 优先从 Product 表加载空间
+2. 如果 Product 表为空,从 Project.data.quotation 创建
+3. 如果两者都有,检查缺失的空间并自动创建
+
+### 2.2 报价明细(quotation-editor.component.ts)
+
+**文件**:`src/modules/project/components/quotation-editor.component.ts`
+
+**空间数量来源**:
+```typescript
+// 从项目的Product表获取空间数量
+this.projectSpaces = await this.productSpaceService
+  .getProjectProductSpaces(this.projectId);
+
+// 空间数量 = this.projectSpaces.length
+const spaceCount = this.projectSpaces.length;
+```
+
+**报价空间结构**:
+```typescript
+quotation.spaces: Array<{
+  name: string;           // 空间名称
+  spaceId?: string;       // 关联的Product ID
+  processes: {...};       // 工序明细
+  subtotal: number;       // 小计
+}>
+
+// 空间数量 = quotation.spaces.length
+```
+
+**同步机制**:
+- 报价中的 `spaceId` 字段关联 Product 表的 ID
+- 通过 `spaceId` 建立报价与Product的关联
+
+### 2.3 交付执行板块(stage-delivery.component.ts)
+
+**文件**:`src/modules/project/pages/project-detail/stages/stage-delivery.component.ts`
+
+**空间加载流程**:
+
+```typescript
+// 第145-188行:syncProductsWithQuotation()
+private async syncProductsWithQuotation(): Promise<void> {
+  if (!this.project) return;
+  
+  const data = this.project.get('data') || {};
+  const quotationSpaces: any[] = Array.isArray(data.quotation?.spaces) 
+    ? data.quotation.spaces 
+    : [];
+  
+  if (quotationSpaces.length === 0) return;
+
+  // 检查哪些空间在Product表中缺失
+  const existingById = new Set(this.projectProducts.map(p => p.id));
+  const existingByName = new Set(
+    this.projectProducts.map(p => (p.name || '').trim().toLowerCase())
+  );
+
+  const missing = quotationSpaces.filter(s => {
+    const n = (s?.name || '').trim().toLowerCase();
+    const pid = s?.productId || '';
+    if (pid && existingById.has(pid)) return false;
+    if (n && existingByName.has(n)) return false;
+    return true;
+  });
+
+  // 为缺失的空间创建Product记录
+  if (missing.length > 0) {
+    for (const s of missing) {
+      await this.productSpaceService.createProductSpace(
+        this.project!.id,
+        {
+          name: s?.name || '未命名空间',
+          type: 'other',
+          priority: 5,
+          status: 'not_started',
+          complexity: 'medium',
+          order: this.projectProducts.length
+        }
+      );
+    }
+    
+    // 重新加载并去重
+    this.projectProducts = await this.productSpaceService
+      .getProjectProductSpaces(this.project.id);
+    
+    this.projectProducts = this.projectProducts.filter((p, idx, arr) => {
+      const key = (p.name || '').trim().toLowerCase();
+      return arr.findIndex(x => (x.name || '').trim().toLowerCase() === key) === idx;
+    });
+  }
+}
+```
+
+**关键特性**:
+- 自动去重(按名称)
+- 同步报价中的空间到Product表
+- 支持按ID或名称匹配
+
+### 2.4 设计师分配弹窗(designer-team-assignment-modal.component.ts)
+
+**文件**:`src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts`
+
+**空间加载流程**:
+
+```typescript
+// 第84-88行:输入属性
+@Input() spaceScenes: SpaceScene[] = [];           // 空间场景列表
+@Input() enableSpaceAssignment: boolean = false;   // 是否启用空间分配
+@Input() projectId: string = '';                   // 项目ID
+@Input() loadRealData: boolean = true;             // 是否加载真实数据
+@Input() loadRealSpaces: boolean = true;           // 是否自动加载真实空间数据
+
+// 第296-298行:ngOnInit()
+if (this.loadRealSpaces && this.projectId) {
+  await this.loadRealProjectSpaces();
+}
+
+// 第951-987行:loadRealProjectSpaces()
+async loadRealProjectSpaces() {
+  if (!this.projectId) {
+    console.warn('未提供projectId,无法加载空间数据');
+    return;
+  }
+
+  try {
+    this.loadingSpaces = true;
+    this.spaceLoadError = '';
+
+    // 使用ProductSpaceService查询项目的所有空间产品
+    this.parseProducts = await this.productSpaceService
+      .getProjectProductSpaces(this.projectId);
+
+    if (this.parseProducts.length === 0) {
+      console.warn('未找到项目空间数据');
+      this.spaceLoadError = '未找到项目空间数据';
+      return;
+    }
+
+    // 转换为SpaceScene格式
+    this.spaceScenes = this.parseProducts.map(product => ({
+      id: product.id,
+      name: product.name,
+      area: product.area,
+      description: this.getProductDescription(product)
+    }));
+
+    console.log('成功加载项目空间数据:', this.spaceScenes);
+  } catch (err) {
+    console.error('加载项目空间数据失败:', err);
+    this.spaceLoadError = '加载项目空间数据失败';
+  } finally {
+    this.loadingSpaces = false;
+    this.cdr.markForCheck();
+  }
+}
+```
+
+**空间分配数据结构**:
+
+```typescript
+// 第53-57行:DesignerSpaceAssignment
+export interface DesignerSpaceAssignment {
+  designerId: string;
+  designerName: string;
+  spaceIds: string[];  // 分配的空间ID列表
+}
+
+// 第1311-1315行:getDesignerSpacesText()
+getDesignerSpacesText(designerId: string): string {
+  const spaces = this.getDesignerSpaces(designerId);
+  if (spaces.length === 0) return '未分配空间';
+  return spaces.map(s => s.name).join(', ');
+}
+
+// 第1305-1308行:getDesignerSpaces()
+getDesignerSpaces(designerId: string): SpaceScene[] {
+  const spaceIds = this.designerSpaceMap.get(designerId) || [];
+  return this.spaceScenes.filter(space => spaceIds.includes(space.id));
+}
+```
+
+**关键方法**:
+
+| 方法名 | 功能 | 行号 |
+|--------|------|------|
+| `loadRealProjectSpaces()` | 加载真实空间数据 | 951-987 |
+| `getDesignerSpaces()` | 获取设计师分配的空间 | 1305-1308 |
+| `getDesignerSpacesText()` | 获取空间名称文本 | 1311-1315 |
+| `isSpaceSelected()` | 检查空间是否被选中 | 1299-1302 |
+| `toggleSpaceSelection()` | 切换空间选择 | (需查看完整代码) |
+
+---
+
+## 3. 数据同步现状分析
+
+### 3.1 同步机制总结
+
+```
+订单分配阶段 (stage-order)
+    ↓
+    ├─ 从Product表加载空间
+    │  └─ productSpaceService.getProjectProductSpaces()
+    │
+    ├─ 从Project.data.quotation同步缺失空间
+    │  └─ 自动创建Product记录
+    │
+    └─ 生成报价 (quotation.spaces)
+       └─ 每个空间关联spaceId
+
+报价明细 (quotation-editor)
+    ↓
+    ├─ 读取quotation.spaces
+    │  └─ 空间数量 = quotation.spaces.length
+    │
+    └─ 通过spaceId关联Product表
+
+交付执行 (stage-delivery)
+    ↓
+    ├─ 从Product表加载空间
+    │  └─ productSpaceService.getProjectProductSpaces()
+    │
+    ├─ 同步报价中的缺失空间
+    │  └─ 自动创建Product记录
+    │
+    └─ 去重处理
+       └─ 按名称去重
+
+设计师分配 (designer-team-assignment-modal)
+    ↓
+    ├─ 从Product表加载空间
+    │  └─ productSpaceService.getProjectProductSpaces()
+    │
+    ├─ 转换为SpaceScene格式
+    │  └─ { id, name, area, description }
+    │
+    └─ 分配空间给设计师
+       └─ designerSpaceMap: Map<designerId, spaceIds[]>
+```
+
+### 3.2 同步数据字段对应关系
+
+| 阶段 | 数据来源 | 字段 | 空间数量 | 同步方式 |
+|------|---------|------|---------|---------|
+| 订单分配 | Product表 | `projectSpaces[]` | `length` | 自动创建缺失空间 |
+| 报价明细 | Project.data | `quotation.spaces[]` | `length` | 通过spaceId关联 |
+| 交付执行 | Product表 | `projectProducts[]` | `length` | 自动创建缺失空间 |
+| 设计师分配 | Product表 | `spaceScenes[]` | `length` | 直接查询Product |
+
+### 3.3 同步一致性检查
+
+**✅ 已实现的同步**:
+1. **Product表 ↔ 报价明细**:通过 `spaceId` 字段关联
+2. **Product表 ↔ 交付执行**:通过 `syncProductsWithQuotation()` 自动同步
+3. **Product表 ↔ 设计师分配**:通过 `loadRealProjectSpaces()` 加载
+
+**⚠️ 潜在的不一致风险**:
+1. **报价修改后未同步Product表**
+   - 问题:在报价编辑器中添加/删除空间,不会自动更新Product表
+   - 影响:交付执行和设计师分配看到的空间数量可能不一致
+   - 解决方案:需要在报价保存时同步Product表
+
+2. **Product表删除后未更新报价**
+   - 问题:直接删除Product记录,报价中仍保留该空间
+   - 影响:报价中的spaceId可能指向不存在的Product
+   - 解决方案:需要级联删除或软删除处理
+
+3. **空间名称修改后的同步**
+   - 问题:修改Product的name,报价中的name可能不同步
+   - 影响:显示的空间名称可能不一致
+   - 解决方案:需要定期同步或使用spaceId作为唯一标识
+
+---
+
+## 4. 关键代码文件清单
+
+| 文件路径 | 功能 | 关键方法 |
+|---------|------|---------|
+| `src/modules/project/services/product-space.service.ts` | 空间产品服务 | `getProjectProductSpaces()`, `createProductSpace()`, `updateProductSpace()`, `deleteProductSpace()` |
+| `src/modules/project/pages/project-detail/stages/stage-order.component.ts` | 订单分配阶段 | `loadProjectSpaces()`, `createSpacesFromQuotation()`, `regenerateQuotationFromSpaces()` |
+| `src/modules/project/components/quotation-editor.component.ts` | 报价编辑器 | (需查看完整代码) |
+| `src/modules/project/pages/project-detail/stages/stage-delivery.component.ts` | 交付执行阶段 | `syncProductsWithQuotation()` |
+| `src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts` | 设计师分配弹窗 | `loadRealProjectSpaces()`, `getDesignerSpaces()`, `getDesignerSpacesText()` |
+
+---
+
+## 5. 总结
+
+### 核心同步机制:
+1. **Product表** 是空间数据的唯一真实来源
+2. **Project.data.quotation** 存储报价信息,包含空间明细
+3. 各阶段通过 `ProductSpaceService.getProjectProductSpaces()` 统一查询空间
+4. 自动同步机制确保Product表与报价数据基本一致
+5. 设计师分配弹窗从Product表加载空间,支持空间分配功能
+
+### 实现方式:
+- ✅ 订单分配:自动创建缺失的Product记录
+- ✅ 交付执行:自动同步报价中的空间到Product表
+- ✅ 设计师分配:从Product表加载空间,支持多设计师分配
+
+### 建议改进:
+- 添加报价保存时的Product同步
+- 实现空间数据一致性验证
+- 完善设计师空间分配的持久化存储

+ 517 - 0
SPACE_SYNC_ANALYSIS_PART2.md

@@ -0,0 +1,517 @@
+# 项目空间数量同步分析报告 - 第二部分
+
+## 4. 实现细节
+
+### 4.1 ProductSpaceService 核心方法
+
+**文件**:`src/modules/project/services/product-space.service.ts`
+
+```typescript
+/**
+ * 获取项目的所有空间产品
+ * @param projectId 项目ID
+ * @returns ProductSpace数组
+ */
+async getProjectProductSpaces(projectId: string): Promise<ProductSpace[]> {
+  const query = new Parse.Query('Product');
+  query.equalTo('project', projectId);
+  query.notEqualTo('isDeleted', true);
+  query.ascending('order');
+  return await query.find();
+}
+
+/**
+ * 创建项目空间产品
+ * @param projectId 项目ID
+ * @param spaceData 空间数据
+ * @returns 创建的ProductSpace
+ */
+async createProductSpace(
+  projectId: string,
+  spaceData: Partial<ProductSpace>
+): Promise<ProductSpace> {
+  const Product = Parse.Object.extend('Product');
+  const product = new Product();
+  
+  product.set('name', spaceData.name);
+  product.set('type', spaceData.type);
+  product.set('project', projectId);
+  product.set('company', localStorage.getItem('company'));
+  product.set('status', spaceData.status || 'not_started');
+  product.set('complexity', spaceData.complexity || 'medium');
+  product.set('order', spaceData.order || 0);
+  
+  return await product.save();
+}
+
+/**
+ * 更新项目空间产品
+ * @param spaceId 空间ID
+ * @param updates 更新数据
+ */
+async updateProductSpace(
+  spaceId: string,
+  updates: Partial<ProductSpace>
+): Promise<void> {
+  const query = new Parse.Query('Product');
+  const product = await query.get(spaceId);
+  
+  Object.keys(updates).forEach(key => {
+    product.set(key, (updates as any)[key]);
+  });
+  
+  await product.save();
+}
+
+/**
+ * 删除项目空间产品(软删除)
+ * @param spaceId 空间ID
+ */
+async deleteProductSpace(spaceId: string): Promise<void> {
+  const query = new Parse.Query('Product');
+  const product = await query.get(spaceId);
+  product.set('isDeleted', true);
+  await product.save();
+}
+
+/**
+ * 计算空间进度
+ * @param spaceId 空间ID
+ * @param processTypes 工序类型列表
+ * @returns 进度百分比
+ */
+calculateProductProgress(spaceId: string, processTypes: string[]): number {
+  // 计算该空间的完成进度
+  // 基于关联的ProjectDeliverable记录
+  // 返回 0-100 之间的百分比
+}
+```
+
+### 4.2 数据同步触发点
+
+**订单分配阶段**:
+```typescript
+// 1. 组件初始化时
+ngOnInit() {
+  await this.loadProjectSpaces();
+}
+
+// 2. 项目类型改变时
+onProjectTypeChange() {
+  this.quotation.spaces = [];
+  this.quotation.total = 0;
+}
+
+// 3. 空间模式改变时
+async onProjectSpaceModeChange() {
+  this.isMultiSpaceProject = this.projectInfo.spaceType === 'multi';
+  await this.regenerateQuotationFromSpaces();
+}
+
+// 4. 添加/编辑/删除空间时
+async addSpace() { ... }
+async editSpace(spaceId: string) { ... }
+async deleteSpace(spaceId: string) { ... }
+```
+
+**交付执行阶段**:
+```typescript
+// 1. 组件初始化时
+ngOnInit() {
+  await this.syncProductsWithQuotation();
+}
+
+// 2. 项目数据变化时
+ngOnChanges(changes: SimpleChanges) {
+  if (changes['project']) {
+    await this.syncProductsWithQuotation();
+  }
+}
+```
+
+**设计师分配弹窗**:
+```typescript
+// 1. 弹窗打开时
+async ngOnInit() {
+  if (this.loadRealSpaces && this.projectId) {
+    await this.loadRealProjectSpaces();
+  }
+}
+
+// 2. 输入属性变化时
+async ngOnChanges(changes: SimpleChanges) {
+  if (changes['visible'] || changes['isVisible']) {
+    if (currentVisible && !previousVisible && this.loadRealData) {
+      await this.enrichMembersWithProjectAssignments();
+    }
+  }
+}
+```
+
+### 4.3 空间分配数据持久化
+
+**设计师分配结果结构**:
+```typescript
+export interface DesignerAssignmentResult {
+  selectedDesigners: Designer[];           // 选中的设计师
+  primaryTeamId: string;                   // 主要项目组ID
+  crossTeamCollaborators: Designer[];      // 跨组合作设计师
+  quotationAssignments: any[];             // 报价分配
+  spaceAssignments: DesignerSpaceAssignment[]; // ⭐ 空间分配
+  projectLeader?: Designer;                // 项目负责人
+}
+
+// 空间分配数据
+spaceAssignments: [
+  {
+    designerId: "designer-1",
+    designerName: "张设计师",
+    spaceIds: ["space-1", "space-2", "space-3"]
+  },
+  {
+    designerId: "designer-2",
+    designerName: "李设计师",
+    spaceIds: ["space-4", "space-5"]
+  }
+]
+```
+
+**保存位置**:
+- 临时存储:组件内存中的 `designerSpaceMap: Map<designerId, spaceIds[]>`
+- 持久化:需要通过 `confirm` 事件传递给父组件,由父组件保存到 `ProjectTeam` 或 `Project` 表
+
+---
+
+## 5. 建议的改进方案
+
+### 5.1 确保数据一致性
+
+**问题**:报价修改后未同步Product表
+
+**解决方案**:
+```typescript
+// 在quotation-editor中添加同步方法
+async saveQuotation(): Promise<void> {
+  // 1. 保存报价数据
+  await this.saveQuotationData();
+  
+  // 2. ⭐ 同步Product表
+  await this.syncQuotationToProducts();
+}
+
+private async syncQuotationToProducts(): Promise<void> {
+  const quotationSpaces = this.quotation.spaces;
+  const existingProducts = await this.productSpaceService
+    .getProjectProductSpaces(this.projectId);
+  
+  // 找出需要创建的空间
+  const existingNames = new Set(
+    existingProducts.map(p => (p.name || '').trim().toLowerCase())
+  );
+  
+  for (const space of quotationSpaces) {
+    const spaceName = (space.name || '').trim().toLowerCase();
+    if (!existingNames.has(spaceName)) {
+      await this.productSpaceService.createProductSpace(
+        this.projectId,
+        {
+          name: space.name,
+          type: 'other',
+          status: 'not_started',
+          complexity: 'medium'
+        }
+      );
+    }
+  }
+}
+```
+
+### 5.2 添加空间同步验证
+
+**问题**:无法检测空间数据不一致
+
+**解决方案**:
+```typescript
+// 添加同步检查方法
+async validateSpaceSync(): Promise<{
+  isConsistent: boolean;
+  productCount: number;
+  quotationCount: number;
+  missingInProduct: string[];
+  missingInQuotation: string[];
+}> {
+  const products = await this.productSpaceService
+    .getProjectProductSpaces(this.projectId);
+  
+  const quotationSpaces = this.project.get('data')?.quotation?.spaces || [];
+  
+  const productNames = new Set(
+    products.map(p => (p.name || '').trim().toLowerCase())
+  );
+  
+  const quotationNames = new Set(
+    quotationSpaces.map(s => (s.name || '').trim().toLowerCase())
+  );
+  
+  const missingInProduct = quotationSpaces
+    .filter(s => !productNames.has((s.name || '').trim().toLowerCase()))
+    .map(s => s.name);
+  
+  const missingInQuotation = products
+    .filter(p => !quotationNames.has((p.name || '').trim().toLowerCase()))
+    .map(p => p.name);
+  
+  return {
+    isConsistent: missingInProduct.length === 0 && missingInQuotation.length === 0,
+    productCount: products.length,
+    quotationCount: quotationSpaces.length,
+    missingInProduct,
+    missingInQuotation
+  };
+}
+```
+
+### 5.3 改进设计师空间分配的持久化
+
+**问题**:空间分配结果未保存到数据库
+
+**解决方案**:
+```typescript
+// 在ProjectTeam表中添加空间分配字段
+interface ProjectTeamData {
+  // ... 其他字段
+  spaceAssignments?: {
+    [designerId: string]: string[];  // 设计师ID -> 空间ID列表
+  };
+}
+
+// 保存空间分配
+async saveSpaceAssignments(
+  projectId: string,
+  assignments: DesignerSpaceAssignment[]
+): Promise<void> {
+  const query = new Parse.Query('Project');
+  const project = await query.get(projectId);
+  
+  const data = project.get('data') || {};
+  data.designerSpaceAssignments = {};
+  
+  for (const assignment of assignments) {
+    data.designerSpaceAssignments[assignment.designerId] = assignment.spaceIds;
+  }
+  
+  project.set('data', data);
+  await project.save();
+}
+
+// 加载空间分配
+async loadSpaceAssignments(projectId: string): Promise<Map<string, string[]>> {
+  const query = new Parse.Query('Project');
+  const project = await query.get(projectId);
+  
+  const data = project.get('data') || {};
+  const assignments = data.designerSpaceAssignments || {};
+  
+  return new Map(Object.entries(assignments));
+}
+```
+
+---
+
+## 6. 完整的数据流图
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                       项目空间数据流                              │
+└─────────────────────────────────────────────────────────────────┘
+
+订单分配阶段
+├─ 输入:项目ID
+├─ 加载Product表 → projectSpaces[]
+├─ 同步报价数据 → 创建缺失的Product
+└─ 输出:quotation.spaces[]
+   └─ 每个空间包含 spaceId(指向Product.id)
+
+        ↓ 保存到 Project.data.quotation
+
+报价明细
+├─ 读取:Project.data.quotation.spaces[]
+├─ 显示:空间列表和价格明细
+└─ 修改:编辑空间信息
+   └─ 问题:修改后未同步Product表 ⚠️
+
+        ↓ 项目进入交付执行阶段
+
+交付执行
+├─ 加载Product表 → projectProducts[]
+├─ 同步报价中的缺失空间 → 创建Product
+├─ 去重处理 → 按名称去重
+└─ 显示:交付物列表
+
+        ↓ 设计师分配
+
+设计师分配弹窗
+├─ 加载Product表 → spaceScenes[]
+├─ 转换格式 → SpaceScene[]
+├─ 分配空间给设计师 → designerSpaceMap
+└─ 输出:DesignerAssignmentResult
+   └─ spaceAssignments: DesignerSpaceAssignment[]
+      └─ 问题:未保存到数据库 ⚠️
+```
+
+---
+
+## 7. 字段存储位置总结表
+
+| 数据项 | 存储表 | 字段路径 | 数据类型 | 备注 |
+|--------|--------|---------|---------|------|
+| 空间名称 | Product | name | string | 如"客厅"、"卧室" |
+| 空间类型 | Product | type | string | living_room, bedroom等 |
+| 空间面积 | Product | area | number | 单位:㎡ |
+| 空间状态 | Product | status | string | not_started, in_progress等 |
+| 空间复杂度 | Product | complexity | string | low, medium, high |
+| 空间预算 | Product | estimatedBudget | number | 预算金额 |
+| 空间排序 | Product | order | number | 排序顺序 |
+| 项目关联 | Product | project | Pointer | 指向Project表 |
+| 公司关联 | Product | company | Pointer | 指向Company表 |
+| 报价信息 | Project | data.quotation | Object | 包含spaces数组 |
+| 报价空间 | Project | data.quotation.spaces[] | Array | 每个空间包含spaceId |
+| 特殊需求 | Project | data.spaceSpecialRequirements | Object | 按spaceId存储 |
+| 空间分配 | Project | data.designerSpaceAssignments | Object | 设计师ID -> 空间ID[] |
+
+---
+
+## 8. 关键函数速查表
+
+### 8.1 ProductSpaceService
+
+```typescript
+// 查询空间
+getProjectProductSpaces(projectId: string): Promise<ProductSpace[]>
+
+// 创建空间
+createProductSpace(projectId: string, spaceData: Partial<ProductSpace>): Promise<ProductSpace>
+
+// 更新空间
+updateProductSpace(spaceId: string, updates: Partial<ProductSpace>): Promise<void>
+
+// 删除空间
+deleteProductSpace(spaceId: string): Promise<void>
+
+// 计算进度
+calculateProductProgress(spaceId: string, processTypes: string[]): number
+```
+
+### 8.2 stage-order.component.ts
+
+```typescript
+// 加载空间
+loadProjectSpaces(): Promise<void>
+
+// 从报价创建空间
+createSpacesFromQuotation(quotationSpaces: any[]): Promise<void>
+
+// 推断空间类型
+inferSpaceType(spaceName: string): string
+
+// 计算空间预算
+calculateSpaceRate(spaceData: any): number
+
+// 从空间生成报价
+regenerateQuotationFromSpaces(): Promise<void>
+
+// 更新空间占比
+updateSpaceBreakdown(): void
+```
+
+### 8.3 stage-delivery.component.ts
+
+```typescript
+// 同步报价中的空间到Product表
+syncProductsWithQuotation(): Promise<void>
+```
+
+### 8.4 designer-team-assignment-modal.component.ts
+
+```typescript
+// 加载真实空间数据
+loadRealProjectSpaces(): Promise<void>
+
+// 获取设计师分配的空间
+getDesignerSpaces(designerId: string): SpaceScene[]
+
+// 获取设计师空间文本
+getDesignerSpacesText(designerId: string): string
+
+// 检查空间是否被选中
+isSpaceSelected(designerId: string, spaceId: string): boolean
+
+// 切换空间选择
+toggleSpaceSelection(designerId: string, spaceId: string): void
+```
+
+---
+
+## 9. 常见问题排查
+
+### Q1: 为什么设计师分配弹窗显示的空间数量与报价不一致?
+
+**原因**:
+1. 报价中添加了新空间,但未同步到Product表
+2. Product表中的空间被删除,但报价未更新
+
+**解决**:
+```typescript
+// 在保存报价时调用
+await this.syncQuotationToProducts();
+
+// 或在交付执行时自动同步
+await this.syncProductsWithQuotation();
+```
+
+### Q2: 如何确保空间数据的一致性?
+
+**方案**:
+```typescript
+// 定期验证
+const validation = await this.validateSpaceSync();
+if (!validation.isConsistent) {
+  console.warn('空间数据不一致:', validation);
+  // 自动修复
+  await this.syncQuotationToProducts();
+}
+```
+
+### Q3: 设计师空间分配如何持久化?
+
+**方案**:
+```typescript
+// 在确认分配时保存
+async onConfirmAssignment(result: DesignerAssignmentResult) {
+  // 保存空间分配
+  await this.saveSpaceAssignments(this.projectId, result.spaceAssignments);
+}
+```
+
+---
+
+## 10. 总结
+
+### 核心要点:
+1. ✅ **Product表**是空间数据的唯一真实来源
+2. ✅ **Project.data.quotation**存储报价信息
+3. ✅ 各阶段通过**ProductSpaceService**统一查询空间
+4. ⚠️ **报价修改**后需要同步Product表
+5. ⚠️ **设计师分配**结果需要持久化存储
+
+### 改进优先级:
+1. **高**:添加报价保存时的Product同步
+2. **高**:实现设计师空间分配的持久化
+3. **中**:添加空间数据一致性验证
+4. **中**:完善错误处理和日志
+
+### 相关文件:
+- `src/modules/project/services/product-space.service.ts`
+- `src/modules/project/pages/project-detail/stages/stage-order.component.ts`
+- `src/modules/project/pages/project-detail/stages/stage-delivery.component.ts`
+- `src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts`

+ 348 - 0
SPACE_SYNC_QUICK_REFERENCE.md

@@ -0,0 +1,348 @@
+# 空间数量同步 - 快速参考指南
+
+## 📍 快速导航
+
+### 四个阶段的空间数据来源
+
+| 阶段 | 数据来源 | 查询方法 | 空间数量获取 |
+|------|---------|---------|------------|
+| 🏷️ 订单分配 | Product表 | `productSpaceService.getProjectProductSpaces(projectId)` | `projectSpaces.length` |
+| 💰 报价明细 | Project.data | `project.get('data').quotation.spaces` | `quotation.spaces.length` |
+| 📦 交付执行 | Product表 | `productSpaceService.getProjectProductSpaces(projectId)` | `projectProducts.length` |
+| 👥 设计师分配 | Product表 | `productSpaceService.getProjectProductSpaces(projectId)` | `spaceScenes.length` |
+
+---
+
+## 🔑 关键字段存储位置
+
+### Product 表(Parse Server)
+```
+Product
+├─ id: string                    // 空间ID
+├─ name: string                  // 空间名称
+├─ type: string                  // 空间类型
+├─ area: number                  // 面积
+├─ status: string                // 状态
+├─ complexity: string            // 复杂度
+├─ estimatedBudget: number       // 预算
+├─ order: number                 // 排序
+├─ project: Pointer              // 关联项目
+├─ company: Pointer              // 关联公司
+└─ isDeleted: boolean            // 软删除标记
+```
+
+### Project 表 → data 字段
+```
+Project.data
+├─ quotation: {
+│  ├─ spaces: [{
+│  │  ├─ name: string            // 空间名称
+│  │  ├─ spaceId: string         // 关联Product ID
+│  │  ├─ processes: {...}        // 工序明细
+│  │  └─ subtotal: number        // 小计
+│  │}]
+│  ├─ total: number              // 总价
+│  └─ spaceBreakdown: [{...}]    // 空间占比
+├─ spaceSpecialRequirements: {
+│  ├─ [spaceId]: {
+│  │  ├─ content: string         // 特殊需求内容
+│  │  ├─ updatedAt: Date         // 更新时间
+│  │  └─ updatedBy: string       // 更新人
+│  └─ }
+└─ designerSpaceAssignments: {
+   ├─ [designerId]: string[]     // 设计师分配的空间ID列表
+   └─ }
+```
+
+---
+
+## 🔄 同步流程速查
+
+### 1️⃣ 订单分配阶段
+```typescript
+// 文件:stage-order.component.ts
+// 行号:695-758
+
+// 加载空间
+this.projectSpaces = await this.productSpaceService
+  .getProjectProductSpaces(this.project.id);
+
+// 自动同步缺失空间
+if (this.projectSpaces.length > 0) {
+  const missing = quotationSpaces.filter(s => !existingNames.has(s.name));
+  for (const spaceData of missing) {
+    await this.productSpaceService.createProductSpace(
+      this.project.id,
+      { name: spaceData.name, type: this.inferSpaceType(spaceData.name) }
+    );
+  }
+}
+
+// 生成报价
+await this.regenerateQuotationFromSpaces();
+```
+
+### 2️⃣ 报价明细
+```typescript
+// 文件:quotation-editor.component.ts
+
+// 读取报价空间
+const quotationSpaces = this.project.get('data').quotation.spaces;
+
+// 空间数量
+const spaceCount = quotationSpaces.length;
+
+// 通过spaceId关联Product
+quotationSpaces.forEach(space => {
+  const productId = space.spaceId;  // 指向Product.id
+});
+```
+
+### 3️⃣ 交付执行阶段
+```typescript
+// 文件:stage-delivery.component.ts
+// 行号:145-188
+
+// 自动同步报价中的缺失空间
+private async syncProductsWithQuotation(): Promise<void> {
+  const quotationSpaces = this.project.get('data').quotation.spaces;
+  const existingProducts = await this.productSpaceService
+    .getProjectProductSpaces(this.project.id);
+  
+  // 找出缺失的空间并创建
+  const missing = quotationSpaces.filter(s => 
+    !existingProducts.some(p => p.name === s.name)
+  );
+  
+  for (const s of missing) {
+    await this.productSpaceService.createProductSpace(
+      this.project.id,
+      { name: s.name, type: 'other' }
+    );
+  }
+}
+```
+
+### 4️⃣ 设计师分配弹窗
+```typescript
+// 文件:designer-team-assignment-modal.component.ts
+// 行号:951-987
+
+// 加载真实空间数据
+async loadRealProjectSpaces() {
+  this.parseProducts = await this.productSpaceService
+    .getProjectProductSpaces(this.projectId);
+  
+  // 转换为SpaceScene格式
+  this.spaceScenes = this.parseProducts.map(product => ({
+    id: product.id,
+    name: product.name,
+    area: product.area,
+    description: this.getProductDescription(product)
+  }));
+}
+
+// 获取设计师分配的空间
+getDesignerSpacesText(designerId: string): string {
+  const spaces = this.getDesignerSpaces(designerId);
+  return spaces.map(s => s.name).join(', ');
+}
+```
+
+---
+
+## ⚠️ 常见问题速查
+
+### 问题1:空间数量不一致
+**症状**:报价显示5个空间,交付执行显示3个空间
+
+**原因**:
+- 报价中添加了新空间,但未同步到Product表
+- 或Product表中的空间被删除
+
+**解决**:
+```typescript
+// 在报价保存时调用
+await this.syncQuotationToProducts();
+
+// 或在交付执行时调用
+await this.syncProductsWithQuotation();
+```
+
+### 问题2:设计师分配弹窗显示"未找到项目空间数据"
+**症状**:弹窗打开时显示空间加载错误
+
+**原因**:
+- Product表中没有该项目的空间记录
+- projectId参数错误
+
+**解决**:
+```typescript
+// 检查projectId是否正确
+console.log('projectId:', this.projectId);
+
+// 确保Product表中有数据
+const spaces = await this.productSpaceService
+  .getProjectProductSpaces(this.projectId);
+console.log('spaces:', spaces);
+
+// 如果为空,从报价创建
+if (spaces.length === 0) {
+  await this.createSpacesFromQuotation(
+    this.project.get('data').quotation.spaces
+  );
+}
+```
+
+### 问题3:设计师空间分配未保存
+**症状**:分配空间后刷新页面,分配信息消失
+
+**原因**:
+- 空间分配结果未持久化到数据库
+- 只保存在组件内存中
+
+**解决**:
+```typescript
+// 在确认分配时保存
+async onConfirmAssignment(result: DesignerAssignmentResult) {
+  // 保存空间分配到Project.data
+  const data = this.project.get('data') || {};
+  data.designerSpaceAssignments = {};
+  
+  for (const assignment of result.spaceAssignments) {
+    data.designerSpaceAssignments[assignment.designerId] = assignment.spaceIds;
+  }
+  
+  this.project.set('data', data);
+  await this.project.save();
+}
+```
+
+---
+
+## 🛠️ 核心方法一览
+
+### ProductSpaceService
+```typescript
+// 查询项目空间
+getProjectProductSpaces(projectId: string): Promise<ProductSpace[]>
+
+// 创建空间
+createProductSpace(projectId: string, spaceData: Partial<ProductSpace>): Promise<ProductSpace>
+
+// 更新空间
+updateProductSpace(spaceId: string, updates: Partial<ProductSpace>): Promise<void>
+
+// 删除空间(软删除)
+deleteProductSpace(spaceId: string): Promise<void>
+
+// 计算空间进度
+calculateProductProgress(spaceId: string, processTypes: string[]): number
+```
+
+### stage-order.component.ts
+```typescript
+// 加载空间
+loadProjectSpaces(): Promise<void>
+
+// 从报价创建空间
+createSpacesFromQuotation(quotationSpaces: any[]): Promise<void>
+
+// 推断空间类型
+inferSpaceType(spaceName: string): string
+
+// 计算空间预算
+calculateSpaceRate(spaceData: any): number
+
+// 从空间生成报价
+regenerateQuotationFromSpaces(): Promise<void>
+
+// 更新空间占比
+updateSpaceBreakdown(): void
+```
+
+### stage-delivery.component.ts
+```typescript
+// 同步报价中的空间到Product表
+syncProductsWithQuotation(): Promise<void>
+```
+
+### designer-team-assignment-modal.component.ts
+```typescript
+// 加载真实空间数据
+loadRealProjectSpaces(): Promise<void>
+
+// 获取设计师分配的空间
+getDesignerSpaces(designerId: string): SpaceScene[]
+
+// 获取设计师空间文本
+getDesignerSpacesText(designerId: string): string
+
+// 检查空间是否被选中
+isSpaceSelected(designerId: string, spaceId: string): boolean
+
+// 切换空间选择
+toggleSpaceSelection(designerId: string, spaceId: string): void
+```
+
+---
+
+## 📊 数据一致性检查清单
+
+- [ ] Product表中的空间数量 = quotation.spaces.length?
+- [ ] 每个quotation.space都有对应的spaceId?
+- [ ] spaceId是否指向有效的Product记录?
+- [ ] 设计师空间分配是否已保存到Project.data?
+- [ ] 删除空间时是否同时更新了报价?
+- [ ] 修改空间名称时是否同时更新了报价?
+
+---
+
+## 📝 实现检查清单
+
+### 必须实现
+- [x] Product表存储空间数据
+- [x] 订单分配自动创建缺失空间
+- [x] 交付执行自动同步空间
+- [x] 设计师分配加载空间
+
+### 需要改进
+- [ ] 报价保存时同步Product表
+- [ ] 设计师空间分配持久化
+- [ ] 空间数据一致性验证
+- [ ] 删除空间时的级联处理
+
+---
+
+## 🔗 相关文件快速链接
+
+| 功能 | 文件路径 | 关键行号 |
+|------|---------|---------|
+| 空间服务 | `src/modules/project/services/product-space.service.ts` | - |
+| 订单分配 | `src/modules/project/pages/project-detail/stages/stage-order.component.ts` | 695-1008 |
+| 交付执行 | `src/modules/project/pages/project-detail/stages/stage-delivery.component.ts` | 145-188 |
+| 设计师分配 | `src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts` | 84-88, 296-298, 951-987, 1305-1315 |
+
+---
+
+## 💡 最佳实践
+
+1. **始终通过ProductSpaceService查询空间**
+   - 不要直接查询Product表
+   - 使用统一的服务方法
+
+2. **在保存报价时同步Product表**
+   - 确保Product表与报价数据一致
+   - 防止数据不同步
+
+3. **使用spaceId关联Product和报价**
+   - 不要依赖空间名称作为唯一标识
+   - 名称可能会改变
+
+4. **设计师空间分配要持久化**
+   - 保存到Project.data.designerSpaceAssignments
+   - 支持跨会话恢复
+
+5. **定期验证数据一致性**
+   - 实现validateSpaceSync()方法
+   - 自动修复不一致的数据

+ 327 - 28
src/app/pages/admin/dashboard/dashboard.ts

@@ -349,56 +349,347 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
         objectId: this.company?.id || 'cDL6R1hgSi'
       };
 
-      // 从Project表的data.pricing字段计算总收入
-      const projectQuery = new Parse.Query('Project');
-      projectQuery.equalTo('company', companyPointer);
-      projectQuery.notEqualTo('isDeleted', true);
-      projectQuery.limit(1000); // 限制查询数量
-      
-      const projects = await projectQuery.find();
-      
+      // 🔥 修复:从ProjectPayment表获取已收到的尾款(真实收入)
+      // 而不是从Project.data.pricing获取报价金额
       let totalRevenue = 0;
-      projects.forEach(project => {
-        const data = project.get('data') || {};
-        const pricing = data.pricing || {};
+      
+      try {
+        // 查询所有ProjectPayment记录(已收到的支付)
+        const paymentQuery = new Parse.Query('ProjectPayment');
+        paymentQuery.equalTo('company', companyPointer);
+        paymentQuery.notEqualTo('isDeleted', true);
+        paymentQuery.limit(1000);
         
-        // 尝试多种可能的金额字段
-        const totalAmount = pricing.totalAmount || 
-                           pricing.total || 
-                           pricing.finalPrice || 
-                           pricing.quotedPrice || 
-                           0;
+        const payments = await paymentQuery.find();
         
-        if (totalAmount > 0) {
-          totalRevenue += totalAmount;
-          console.log(`  项目 ${project.get('title')}: ¥${totalAmount.toLocaleString()}`);
+        console.log(`📊 找到 ${payments.length} 条支付记录`);
+        
+        // 统计所有已收到的支付金额
+        payments.forEach(payment => {
+          const amount = payment.get('amount') || 0;
+          if (amount > 0) {
+            totalRevenue += amount;
+            const projectId = payment.get('project')?.id || '未知项目';
+            console.log(`  支付记录 ${projectId}: ¥${amount.toLocaleString()}`);
+          }
+        });
+        
+        console.log(`✅ 收入统计: 已收到尾款 ¥${totalRevenue.toLocaleString()} (来自${payments.length}条支付记录)`);
+      } catch (paymentError) {
+        console.warn('⚠️ ProjectPayment查询失败,尝试从ProjectFile(payment_voucher)获取:', paymentError);
+        
+        // 降级方案:从ProjectFile表的payment_voucher类型获取
+        try {
+          const fileQuery = new Parse.Query('ProjectFile');
+          fileQuery.equalTo('company', companyPointer);
+          fileQuery.equalTo('fileType', 'payment_voucher');
+          fileQuery.notEqualTo('isDeleted', true);
+          fileQuery.limit(1000);
+          
+          const voucherFiles = await fileQuery.find();
+          
+          voucherFiles.forEach(file => {
+            const data = file.get('data') || {};
+            const aiAnalysis = data.aiAnalysis || {};
+            const amount = aiAnalysis.amount || 0;
+            
+            if (amount > 0) {
+              totalRevenue += amount;
+            }
+          });
+          
+          console.log(`✅ 收入统计(降级): 已收到尾款 ¥${totalRevenue.toLocaleString()} (来自${voucherFiles.length}张凭证)`);
+        } catch (fallbackError) {
+          console.error('❌ 降级方案也失败:', fallbackError);
+          // 继续使用totalRevenue = 0
         }
-      });
+      }
 
       this.stats.totalRevenue.set(totalRevenue);
-      console.log(`✅ 收入统计: 总收入 ¥${totalRevenue.toLocaleString()} (来自${projects.length}个项目)`);
     } catch (error) {
       console.error('❌ 收入统计加载失败:', error);
       throw error;
     }
   }
 
-  // 加载最近活动日志
+  // 加载最近活动日志 + 项目紧急事件 + 进展动态
   private async loadRecentActivities(forceLimit?: number): Promise<void> {
     try {
       this.loadingActivities.set(true);
       const limit = forceLimit || (this.showAllActivities() ? 50 : 10);
+      
+      // 1️⃣ 加载活动日志
       const activities = await this.activityLogService.getRecentActivities(limit);
-      this.recentActivities.set(activities);
-      console.log(`✅ 活动日志加载成功: ${activities.length}条`);
+      
+      // 2️⃣ 加载项目紧急事件
+      const urgentEvents = await this.loadProjectUrgentEvents();
+      
+      // 3️⃣ 加载项目进展动态
+      const progressUpdates = await this.loadProjectProgressUpdates();
+      
+      // 合并所有活动,按时间排序
+      const allActivities = [
+        ...activities,
+        ...urgentEvents,
+        ...progressUpdates
+      ].sort((a, b) => {
+        const timeA = new Date(a.timestamp || a.createdAt || 0).getTime();
+        const timeB = new Date(b.timestamp || b.createdAt || 0).getTime();
+        return timeB - timeA; // 最新的在前
+      }).slice(0, limit);
+      
+      this.recentActivities.set(allActivities);
+      console.log(`✅ 活动加载成功: ${activities.length}条日志 + ${urgentEvents.length}个紧急事件 + ${progressUpdates.length}个进展 = 共${allActivities.length}条`);
     } catch (error) {
-      console.error('❌ 活动日志加载失败:', error);
+      console.error('❌ 活动加载失败:', error);
       this.recentActivities.set([]);
     } finally {
       this.loadingActivities.set(false);
     }
   }
 
+  /**
+   * 加载项目紧急事件
+   */
+  private async loadProjectUrgentEvents(): Promise<any[]> {
+    try {
+      const companyPointer = {
+        __type: 'Pointer',
+        className: 'Company',
+        objectId: this.company?.id || 'cDL6R1hgSi'
+      };
+
+      // 查询所有进行中的项目
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.equalTo('company', companyPointer);
+      projectQuery.notEqualTo('isDeleted', true);
+      projectQuery.notEqualTo('status', '已完成');
+      projectQuery.notEqualTo('status', '已取消');
+      projectQuery.limit(100);
+      
+      const projects = await projectQuery.find();
+      console.log(`📊 [紧急事件] 查询到 ${projects.length} 个项目`);
+      
+      const urgentEvents: any[] = [];
+      
+      const now = new Date();
+      const oneDayMs = 24 * 60 * 60 * 1000;
+      
+      for (const project of projects) {
+        const title = project.get('title') || '未命名项目';
+        const data = project.get('data') || {};
+        const currentStage = project.get('currentStage') || project.get('stage') || '订单分配';
+        
+        console.log(`📋 检查项目 "${title}" 的截止时间:`, {
+          smallImageDeadline: data.smallImageDeadline || '无',
+          deliveryDeadline: data.deliveryDeadline || '无',
+          stageDeadline: data.stageDeadline || '无'
+        });
+        
+        // 检查各类截止时间
+        
+        // 1️⃣ 小图对图截止
+        if (data.smallImageDeadline) {
+          const deadline = new Date(data.smallImageDeadline);
+          const daysLeft = (deadline.getTime() - now.getTime()) / oneDayMs;
+          
+          if (daysLeft < 1 && daysLeft >= 0) {
+            urgentEvents.push({
+              id: `urgent-${project.id}-smallimg`,
+              type: 'urgent_event',
+              module: 'project',
+              actionType: 'deadline_approaching',
+              entityName: title,
+              description: '小图对图截止在即',
+              severity: daysLeft < 0 ? 'critical' : 'high',
+              timestamp: new Date(),
+              createdAt: new Date(),
+              icon: '⚠️',
+              projectId: project.id,
+              deadline: deadline
+            });
+          } else if (daysLeft < 0) {
+            urgentEvents.push({
+              id: `urgent-${project.id}-smallimg-overdue`,
+              type: 'urgent_event',
+              module: 'project',
+              actionType: 'deadline_overdue',
+              entityName: title,
+              description: `小图对图已逾期 ${Math.abs(Math.floor(daysLeft))} 天`,
+              severity: 'critical',
+              timestamp: new Date(),
+              createdAt: new Date(),
+              icon: '🔴',
+              projectId: project.id,
+              deadline: deadline
+            });
+          }
+        }
+        
+        // 2️⃣ 交付截止
+        if (data.deliveryDeadline) {
+          const deadline = new Date(data.deliveryDeadline);
+          const daysLeft = (deadline.getTime() - now.getTime()) / oneDayMs;
+          
+          if (daysLeft < 1 && daysLeft >= 0) {
+            urgentEvents.push({
+              id: `urgent-${project.id}-delivery`,
+              type: 'urgent_event',
+              module: 'project',
+              actionType: 'deadline_approaching',
+              entityName: title,
+              description: '项目交付截止在即',
+              severity: daysLeft < 0 ? 'critical' : 'high',
+              timestamp: new Date(),
+              createdAt: new Date(),
+              icon: '📦',
+              projectId: project.id,
+              deadline: deadline
+            });
+          } else if (daysLeft < 0) {
+            urgentEvents.push({
+              id: `urgent-${project.id}-delivery-overdue`,
+              type: 'urgent_event',
+              module: 'project',
+              actionType: 'deadline_overdue',
+              entityName: title,
+              description: `项目交付已逾期 ${Math.abs(Math.floor(daysLeft))} 天`,
+              severity: 'critical',
+              timestamp: new Date(),
+              createdAt: new Date(),
+              icon: '🔴',
+              projectId: project.id,
+              deadline: deadline
+            });
+          }
+        }
+        
+        // 3️⃣ 阶段截止
+        if (data.stageDeadline) {
+          const deadline = new Date(data.stageDeadline);
+          const daysLeft = (deadline.getTime() - now.getTime()) / oneDayMs;
+          
+          if (daysLeft < 1 && daysLeft >= 0) {
+            urgentEvents.push({
+              id: `urgent-${project.id}-stage`,
+              type: 'urgent_event',
+              module: 'project',
+              actionType: 'deadline_approaching',
+              entityName: title,
+              description: `${currentStage}阶段截止在即`,
+              severity: daysLeft < 0 ? 'critical' : 'high',
+              timestamp: new Date(),
+              createdAt: new Date(),
+              icon: '⏰',
+              projectId: project.id,
+              deadline: deadline
+            });
+          } else if (daysLeft < 0) {
+            urgentEvents.push({
+              id: `urgent-${project.id}-stage-overdue`,
+              type: 'urgent_event',
+              module: 'project',
+              actionType: 'deadline_overdue',
+              entityName: title,
+              description: `${currentStage}阶段已逾期 ${Math.abs(Math.floor(daysLeft))} 天`,
+              severity: 'critical',
+              timestamp: new Date(),
+              createdAt: new Date(),
+              icon: '🔴',
+              projectId: project.id,
+              deadline: deadline
+            });
+          }
+        }
+      }
+      
+      console.log(`📌 找到 ${urgentEvents.length} 个紧急事件`);
+      return urgentEvents;
+    } catch (error) {
+      console.error('❌ 加载紧急事件失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 加载项目进展动态
+   */
+  private async loadProjectProgressUpdates(): Promise<any[]> {
+    try {
+      const companyPointer = {
+        __type: 'Pointer',
+        className: 'Company',
+        objectId: this.company?.id || 'cDL6R1hgSi'
+      };
+
+      // 查询最近更新的项目
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.equalTo('company', companyPointer);
+      projectQuery.notEqualTo('isDeleted', true);
+      projectQuery.descending('updatedAt');
+      projectQuery.limit(20);
+      
+      const projects = await projectQuery.find();
+      const progressUpdates: any[] = [];
+      
+      // 阶段映射
+      const stageLabels: Record<string, string> = {
+        '订单分配': '📋 订单分配',
+        '确认需求': '🎯 确认需求',
+        '交付执行': '🚀 交付执行',
+        '售后归档': '✅ 售后归档'
+      };
+      
+      for (const project of projects) {
+        const title = project.get('title') || '未命名项目';
+        const currentStage = project.get('currentStage') || project.get('stage') || '订单分配';
+        const status = project.get('status') || '待分配';
+        const updatedAt = project.get('updatedAt');
+        
+        // 计算距离现在的时间
+        const now = new Date();
+        const diffMs = now.getTime() - new Date(updatedAt).getTime();
+        const diffHours = Math.floor(diffMs / (60 * 60 * 1000));
+        const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
+        
+        let timeStr = '';
+        if (diffHours < 1) {
+          timeStr = '刚刚';
+        } else if (diffHours < 24) {
+          timeStr = `${diffHours}小时前`;
+        } else {
+          timeStr = `${diffDays}天前`;
+        }
+        
+        // 只显示最近24小时内的更新
+        if (diffHours <= 24) {
+          progressUpdates.push({
+            id: `progress-${project.id}`,
+            type: 'progress_update',
+            module: 'project',
+            actionType: 'stage_update',
+            entityName: title,
+            description: `进入${stageLabels[currentStage] || currentStage}阶段`,
+            severity: 'medium',
+            timestamp: updatedAt,
+            createdAt: updatedAt,
+            icon: stageLabels[currentStage]?.split(' ')[0] || '📊',
+            projectId: project.id,
+            stage: currentStage,
+            status: status,
+            timeStr: timeStr
+          });
+        }
+      }
+      
+      console.log(`📈 找到 ${progressUpdates.length} 个进展动态`);
+      return progressUpdates;
+    } catch (error) {
+      console.error('❌ 加载进展动态失败:', error);
+      return [];
+    }
+  }
+
   // 查看全部活动
   async viewAllActivities(): Promise<void> {
     this.showAllActivities.set(true);
@@ -931,7 +1222,11 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
       'complete': 'complete',
       'assign': 'assign',
       'upload': 'upload',
-      'approve': 'approve'
+      'approve': 'approve',
+      // 🔥 新增:紧急事件和进展动态
+      'deadline_approaching': 'warning',
+      'deadline_overdue': 'critical',
+      'stage_update': 'info'
     };
     return actionMap[activity.actionType] || 'default';
   }
@@ -952,7 +1247,11 @@ export class AdminDashboard implements OnInit, AfterViewInit, OnDestroy {
       'upload': '上传',
       'download': '下载',
       'share': '分享',
-      'comment': '评论'
+      'comment': '评论',
+      // 🔥 新增:紧急事件和进展动态
+      'deadline_approaching': '截止在即',
+      'deadline_overdue': '已逾期',
+      'stage_update': '阶段更新'
     };
     return labelMap[actionType] || actionType;
   }

+ 12 - 4
src/app/pages/admin/services/project.service.ts

@@ -255,13 +255,21 @@ export class ProjectService {
     json.customerName = customerName || '未知客户';
     json.customerId = customerId;
 
-    // 🔥 处理负责人信息 - 优先显示项目组长(department.leader
+    // 🔥 处理负责人信息 - 优先显示第一个标记为星号的员工(从ProjectTeam表
     let assigneeName = '';
     let assigneeId = '';
     let assigneeRole = '';
 
-    // 方案1: 优先使用 department.leader(组长)
-    if (json.department && typeof json.department === 'object') {
+    // 方案1: 从 data.primaryDesigner 获取(第一个标记为星号的员工)
+    const data = json.data || {};
+    if (data.primaryDesigner && typeof data.primaryDesigner === 'object') {
+      assigneeName = data.primaryDesigner.name || '';
+      assigneeId = data.primaryDesigner.id || '';
+      assigneeRole = '主设计师';
+      console.log(`👤 [ProjectService] 项目 "${json.title}" 负责人(主设计师): ${assigneeName}`);
+    }
+    // 方案2: 如果没有主设计师,使用 department.leader(组长)
+    else if (json.department && typeof json.department === 'object') {
       const leader = json.department.leader;
       if (leader && typeof leader === 'object') {
         assigneeName = leader.name || '';
@@ -275,7 +283,7 @@ export class ProjectService {
       console.log(`ℹ️ [ProjectService] 项目 "${json.title}" 没有关联项目组`);
     }
 
-    // 方案2: 如果没有项目组或组长,使用 assignee(直接分配的负责人)
+    // 方案3: 如果没有项目组或组长,使用 assignee(直接分配的负责人)
     if (!assigneeName && json.assignee && typeof json.assignee === 'object') {
       assigneeName = json.assignee.name || '';
       assigneeId = json.assignee.objectId;