现象:不同项目的报价空间计算方式不一致
经过代码审查,我发现了以下关键问题导致不同项目行为不一致:
报价编辑器 (quotation-editor.component.ts):
// ✅ 有广播事件
private broadcastProductUpdate(): void {
const event = new CustomEvent('product-spaces-updated', {
detail: { projectId, productCount, timestamp }
});
document.dispatchEvent(event);
}
确认需求阶段 (stage-requirements.component.ts):
// ✅ 有监听事件
private setupProductUpdateListener(): void {
this.productUpdateListener = (event: any) => {
if (detail.projectId === this.projectId) {
this.loadData(); // 重新加载数据
}
};
document.addEventListener('product-spaces-updated', this.productUpdateListener);
}
交付执行阶段 (stage-delivery.component.ts):
// ✅ 有监听事件
private setupProductUpdateListener(): void {
this.productUpdateListener = (event: any) => {
if (detail.projectId === this.projectId) {
this.loadData(); // 重新加载数据
}
};
document.addEventListener('product-spaces-updated', this.productUpdateListener);
}
订单分配阶段 (stage-order.component.ts):
// ❌ 没有监听事件!只有广播,没有接收
// 这导致在订单分配阶段,报价编辑器的变更不会触发订单分配页面刷新
当用户在订单分配阶段操作报价编辑器时:
product-spaces-updated 事件 ✅旧项目(在修复之前创建的):
project.data = {
quotation: {
spaces: [...], // 只有这个
total: 300
}
// ❌ 没有 unifiedSpaces
}
新项目(修复之后创建的):
project.data = {
quotation: {
spaces: [...],
total: 300
},
unifiedSpaces: [...] // ✅ 有这个
}
unifiedSpaces 读取,数据一致 ✅quotation.spaces 读取 ✅unifiedSpaces 读取(空数组),回退到 Product表查询 ⚠️某些旧项目的Product表存在重复记录:
Product表:
- 餐厅 (ID: abc123) ← 保留
- 餐厅 (ID: def456) ← 重复
- 主卧 (ID: ghi789)
报价编辑器加载时:
// ✅ 已有去重逻辑
loadProjectProducts() {
const seen = new Set<string>();
for (const p of results) {
const key = productName.trim().toLowerCase();
if (!seen.has(key)) {
seen.add(key);
this.products.push(p); // 只保留第一个
} else {
console.log('⚠️ 跳过重复空间'); // 跳过重复的
}
}
}
问题:
当前 syncUnifiedSpaces() 的调用位置:
loadProjectDataFromProject() - 加载项目数据后saveQuotationToProject() - 保存报价后缺失的调用位置:
deleteProduct() - 删除产品后没有显式调用(依赖 saveQuotationToProject)updateExistingProduct() - 更新产品后没有显式调用(依赖 saveQuotationToProject)createNewProduct() - 创建新产品后没有显式调用(依赖 saveQuotationToProject)虽然这些操作最终都会调用 saveQuotationToProject(),但如果:
就会导致 unifiedSpaces 没有被同步。
文件:stage-order.component.ts
export class StageOrderComponent implements OnInit, OnDestroy {
// 添加监听器属性
private productUpdateListener: any = null;
async ngOnInit() {
// ... 现有初始化代码 ...
// 🔥 添加产品更新监听器
this.setupProductUpdateListener();
}
ngOnDestroy() {
// ... 现有清理代码 ...
// 🔥 清理产品更新监听器
if (this.productUpdateListener) {
document.removeEventListener('product-spaces-updated', this.productUpdateListener);
console.log('🧹 [订单分配] 已清理产品更新监听器');
}
}
/**
* 🔥 设置产品更新监听器
*/
private setupProductUpdateListener(): void {
this.productUpdateListener = (event: any) => {
const detail = event.detail || {};
if (detail.projectId === this.projectId) {
console.log('🔔 [订单分配] 收到产品更新事件,重新加载数据...');
// 重新加载报价数据
this.loadData();
// 强制Angular检测变化
this.cdr.detectChanges();
}
};
document.addEventListener('product-spaces-updated', this.productUpdateListener);
console.log('✅ [订单分配] 已设置产品更新监听器');
}
}
文件:quotation-editor.component.ts
在 loadProjectDataFromProject() 中增强逻辑:
private async loadProjectDataFromProject(): Promise<void> {
// ... 加载产品 ...
await this.loadProjectProducts();
const data = this.project.get('data') || {};
// 🔥 检测旧项目:没有 unifiedSpaces 但有产品
if (!data.unifiedSpaces && this.products.length > 0) {
console.warn('⚠️ [报价编辑器] 检测到旧项目,自动生成 unifiedSpaces...');
// 生成报价(如果没有)
if (!data.quotation || !data.quotation.spaces) {
await this.generateQuotationFromProducts();
}
// 同步 unifiedSpaces
await this.syncUnifiedSpaces();
console.log('✅ [报价编辑器] 旧项目数据已迁移');
} else {
// 正常流程
if (data.quotation) {
this.quotation = data.quotation;
this.updateProductsFromQuotation();
} else if (this.products.length > 0) {
await this.generateQuotationFromProducts();
}
await this.syncUnifiedSpaces();
}
}
文件:quotation-editor.component.ts
增强 generateQuotationFromProducts() 方法:
async generateQuotationFromProducts(): Promise<void> {
// ... 生成报价逻辑 ...
// 🔥 检测到重复产品时,自动提示清理
if (duplicateProductIds.length > 0) {
console.warn(`⚠️ 检测到 ${duplicateProductIds.length} 个重复产品`);
// 如果可以编辑,提示用户清理
if (this.canEdit) {
const shouldClean = await window?.fmode?.confirm(
`检测到 ${duplicateProductIds.length} 个重复空间产品,是否自动清理?\n\n清理后将删除重复记录,保留第一个创建的空间。`
);
if (shouldClean) {
await this.removeDuplicateProducts(duplicateProductIds);
return; // 清理后重新生成报价
}
}
}
// ... 继续正常流程 ...
}
文件:quotation-editor.component.ts
在关键操作中添加同步保证:
async deleteProduct(productId: string): Promise<void> {
try {
// ... 删除逻辑 ...
await product.destroy();
// 更新本地数据
this.products = this.products.filter(p => p.id !== productId);
this.quotation.spaces = this.quotation.spaces.filter((s: any) => s.productId !== productId);
// 重新计算
this.calculateTotal();
this.updateProductBreakdown();
// 🔥 保存报价(会自动调用 syncUnifiedSpaces 和 broadcastProductUpdate)
await this.saveQuotationToProject();
// 🔥 额外保证:如果 saveQuotationToProject 失败,也要尝试同步
try {
await this.syncUnifiedSpaces();
} catch (syncError) {
console.error('❌ 同步 unifiedSpaces 失败:', syncError);
}
console.log('✅ [产品变更] 已删除产品,当前数量:', this.products.length);
} catch (error) {
console.error('❌ 删除产品失败:', error);
window?.fmode?.alert('删除失败,请重试');
// 🔥 失败时重新加载数据,确保界面与数据库一致
await this.loadProjectProducts();
}
}
用户操作(添加/编辑/删除空间)
↓
报价编辑器处理
├─ 更新本地 products 数组
├─ 更新本地 quotation.spaces 数组
└─ 重新计算总价
↓
saveQuotationToProject()
├─ 保存 data.quotation
├─ 同步 data.unifiedSpaces ← 关键
├─ 保存到数据库
└─ 调用 broadcastProductUpdate()
↓
广播全局事件 'product-spaces-updated'
↓
┌─────────────────┬─────────────────┬─────────────────┐
│ 订单分配阶段 │ 确认需求阶段 │ 交付执行阶段 │
│ (新增监听) │ (已有监听) │ (已有监听) │
└─────────────────┴─────────────────┴─────────────────┘
│ │ │
└───────────────────┴───────────────────┘
↓
重新加载数据
↓
界面自动刷新,数据一致 ✅
stage-order.component.tssetupProductUpdateListener() 方法ngOnInit 中调用ngOnDestroy 中清理quotation-editor.component.ts 的 loadProjectDataFromProject()unifiedSpaces 的旧项目unifiedSpacesgenerateQuotationFromProducts() 方法// 新项目
✅ [报价编辑器] 最终加载 3 个唯一空间
🔄 [报价编辑器] unifiedSpaces已同步: 3 → 3 个空间
📢 [报价编辑器] 已广播产品更新事件
🔔 [订单分配] 收到产品更新事件,重新加载数据...
✅ [订单分配] 数据重新加载完成
// 旧项目迁移
⚠️ [报价编辑器] 检测到旧项目,自动生成 unifiedSpaces...
✅ [报价编辑器] 旧项目数据已迁移
🔄 [报价编辑器] unifiedSpaces已同步: 0 → 5 个空间
// 重复数据清理
⚠️ 检测到 2 个重复产品,建议清理
🗑️ 开始清理 2 个重复产品...
✓ 已删除: 餐厅 (def456)
✓ 已删除: 主卧 (xyz789)
✅ 重复产品清理完成
修复后,所有项目(新旧项目)都将:
修复工作已规划完毕,准备实施! 🚀