本报告分析了订单分配阶段、报价明细、交付执行板块以及设计师分配弹窗中的空间数量同步机制。
Product 表(对应 ProductSpace 服务)ProductSpaceService.getProjectProductSpaces() 统一查询表名:Product(通过 ProductSpaceService 访问)
核心字段:
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 表
查询方法:
// ProductSpaceService
async getProjectProductSpaces(projectId: string): Promise<ProductSpace[]>
表名:Project
空间相关字段:
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格式)
文件:src/modules/project/pages/project-detail/stages/stage-order.component.ts
空间加载流程:
// 第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 |
数据同步逻辑:
文件:src/modules/project/components/quotation-editor.component.ts
空间数量来源:
// 从项目的Product表获取空间数量
this.projectSpaces = await this.productSpaceService
.getProjectProductSpaces(this.projectId);
// 空间数量 = this.projectSpaces.length
const spaceCount = this.projectSpaces.length;
报价空间结构:
quotation.spaces: Array<{
name: string; // 空间名称
spaceId?: string; // 关联的Product ID
processes: {...}; // 工序明细
subtotal: number; // 小计
}>
// 空间数量 = quotation.spaces.length
同步机制:
spaceId 字段关联 Product 表的 IDspaceId 建立报价与Product的关联文件:src/modules/project/pages/project-detail/stages/stage-delivery.component.ts
空间加载流程:
// 第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;
});
}
}
关键特性:
文件:src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts
空间加载流程:
// 第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();
}
}
空间分配数据结构:
// 第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() |
切换空间选择 | (需查看完整代码) |
订单分配阶段 (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[]>
| 阶段 | 数据来源 | 字段 | 空间数量 | 同步方式 |
|---|---|---|---|---|
| 订单分配 | Product表 | projectSpaces[] |
length |
自动创建缺失空间 |
| 报价明细 | Project.data | quotation.spaces[] |
length |
通过spaceId关联 |
| 交付执行 | Product表 | projectProducts[] |
length |
自动创建缺失空间 |
| 设计师分配 | Product表 | spaceScenes[] |
length |
直接查询Product |
✅ 已实现的同步:
spaceId 字段关联syncProductsWithQuotation() 自动同步loadRealProjectSpaces() 加载⚠️ 潜在的不一致风险:
报价修改后未同步Product表
Product表删除后未更新报价
空间名称修改后的同步
| 文件路径 | 功能 | 关键方法 |
|---|---|---|
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() |
ProductSpaceService.getProjectProductSpaces() 统一查询空间