文件:src/modules/project/services/product-space.service.ts
/**
* 获取项目的所有空间产品
* @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 之间的百分比
}
订单分配阶段:
// 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) { ... }
交付执行阶段:
// 1. 组件初始化时
ngOnInit() {
await this.syncProductsWithQuotation();
}
// 2. 项目数据变化时
ngOnChanges(changes: SimpleChanges) {
if (changes['project']) {
await this.syncProductsWithQuotation();
}
}
设计师分配弹窗:
// 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();
}
}
}
设计师分配结果结构:
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 表问题:报价修改后未同步Product表
解决方案:
// 在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'
}
);
}
}
}
问题:无法检测空间数据不一致
解决方案:
// 添加同步检查方法
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
};
}
问题:空间分配结果未保存到数据库
解决方案:
// 在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));
}
┌─────────────────────────────────────────────────────────────────┐
│ 项目空间数据流 │
└─────────────────────────────────────────────────────────────────┘
订单分配阶段
├─ 输入:项目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[]
└─ 问题:未保存到数据库 ⚠️
| 数据项 | 存储表 | 字段路径 | 数据类型 | 备注 |
|---|---|---|---|---|
| 空间名称 | 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[] |
// 查询空间
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
// 加载空间
loadProjectSpaces(): Promise<void>
// 从报价创建空间
createSpacesFromQuotation(quotationSpaces: any[]): Promise<void>
// 推断空间类型
inferSpaceType(spaceName: string): string
// 计算空间预算
calculateSpaceRate(spaceData: any): number
// 从空间生成报价
regenerateQuotationFromSpaces(): Promise<void>
// 更新空间占比
updateSpaceBreakdown(): void
// 同步报价中的空间到Product表
syncProductsWithQuotation(): Promise<void>
// 加载真实空间数据
loadRealProjectSpaces(): Promise<void>
// 获取设计师分配的空间
getDesignerSpaces(designerId: string): SpaceScene[]
// 获取设计师空间文本
getDesignerSpacesText(designerId: string): string
// 检查空间是否被选中
isSpaceSelected(designerId: string, spaceId: string): boolean
// 切换空间选择
toggleSpaceSelection(designerId: string, spaceId: string): void
原因:
解决:
// 在保存报价时调用
await this.syncQuotationToProducts();
// 或在交付执行时自动同步
await this.syncProductsWithQuotation();
方案:
// 定期验证
const validation = await this.validateSpaceSync();
if (!validation.isConsistent) {
console.warn('空间数据不一致:', validation);
// 自动修复
await this.syncQuotationToProducts();
}
方案:
// 在确认分配时保存
async onConfirmAssignment(result: DesignerAssignmentResult) {
// 保存空间分配
await this.saveSpaceAssignments(this.projectId, result.spaceAssignments);
}
src/modules/project/services/product-space.service.tssrc/modules/project/pages/project-detail/stages/stage-order.component.tssrc/modules/project/pages/project-detail/stages/stage-delivery.component.tssrc/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts