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