日期: 2025-10-25
状态: ✅ 已完成
在设计师分配弹窗中集成真实的空间数据,从 Parse Server 的 Product 表自动加载项目的空间场景数据,实现真实的空间分配功能。
rules/schemas.md)根据 rules/schemas.md,Product 表是空间管理的核心:
Product {
objectId: 'prod001',
project: Pointer<Project>, // 所属项目
profile: Pointer<Profile>, // 负责设计师
productName: '主卧设计', // 空间名称
productType: 'bedroom', // 空间类型
status: 'in_progress', // 产品状态
space: { // 空间信息
spaceName: '主卧',
area: 18.5,
dimensions: { length: 4.5, width: 4.1, height: 2.8 },
features: ['朝南', '飘窗', '独立卫浴']
},
quotation: { /* 报价信息 */ },
requirements: { /* 设计需求 */ },
order: 1, // 排序
isDeleted: false
}
ProjectTeam {
objectId: 'team001',
project: Pointer<Project>, // 所属项目
profile: Pointer<Profile>, // 设计师
role: '设计师', // 角色
workload: 70, // 工作负载
data: {
assignedSpaces: ['prod001', 'prod002'], // 负责的空间Product ID列表
assignedDate: '2025-10-25'
},
isDeleted: false
}
import { ProductSpaceService, Project as ProductSpace } from
'../../../../../../modules/project/services/product-space.service';
constructor(
private cdr: ChangeDetectorRef,
private productSpaceService: ProductSpaceService
) {}
@Input() loadRealSpaces: boolean = true; // 是否自动加载真实空间数据
private parseProducts: ProductSpace[] = []; // 项目的产品空间列表
loadingSpaces = false; // 空间加载状态
spaceLoadError = ''; // 空间加载错误
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, // Product.objectId
name: product.name, // Product.productName
area: product.area, // Product.space.area
description: this.getProductDescription(product)
}));
console.log('成功加载项目空间数据:', this.spaceScenes);
} catch (err) {
console.error('加载项目空间数据失败:', err);
this.spaceLoadError = '加载项目空间数据失败';
} finally {
this.loadingSpaces = false;
this.cdr.markForCheck();
}
}
getProductDescription() 方法生成空间的描述信息:
private getProductDescription(product: ProductSpace): string {
const parts: string[] = [];
// 产品类型映射
if (product.type) {
const typeMap: Record<string, string> = {
'living_room': '客厅',
'bedroom': '卧室',
'kitchen': '厨房',
'bathroom': '卫生间',
'study': '书房',
'dining_room': '餐厅',
'balcony': '阳台',
'entrance': '玄关',
'other': '其他'
};
parts.push(typeMap[product.type] || product.type);
}
// 面积
if (product.area) {
parts.push(`${product.area}㎡`);
}
// 状态映射
if (product.status) {
const statusMap: Record<string, string> = {
'pending': '待开始',
'in_progress': '进行中',
'completed': '已完成',
'on_hold': '暂停中'
};
parts.push(statusMap[product.status] || product.status);
}
// 自定义描述
if (product.metadata?.description) {
parts.push(product.metadata.description);
}
return parts.join(' · ') || '暂无描述';
}
@if (loadingSpaces) {
<div class="space-loading">
<div class="spinner"></div>
<span>正在加载空间数据...</span>
</div>
}
@if (spaceLoadError && !loadingSpaces) {
<div class="space-error">
<svg class="icon-warning" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2L1 21h22L12 2z..."/>
</svg>
<span>{{ spaceLoadError }}</span>
</div>
}
@if (spaceScenes.length === 0) {
<div class="space-empty">
<svg class="icon-empty" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.48 2..."/>
</svg>
<p>该项目暂无空间数据</p>
<small>请先在项目中创建空间产品(Product)</small>
</div>
}
@for (space of spaceScenes; track space.id) {
<label class="space-checkbox-item">
<input
type="checkbox"
[checked]="isSpaceSelected(selectedDesignerForSpaceAssignment.id, space.id)"
(change)="toggleSpaceSelection(selectedDesignerForSpaceAssignment.id, space.id)"
>
<span class="checkbox-custom"></span>
<div class="space-info">
<span class="space-name">{{ space.name }}</span>
@if (space.area) {
<span class="space-area">{{ space.area }}㎡</span>
}
@if (space.description) {
<span class="space-desc">{{ space.description }}</span>
}
</div>
</label>
}
新增了以下样式类:
.space-loading - 加载状态样式.space-error - 错误提示样式.space-empty - 空状态样式@keyframes spin - 加载动画1. 用户打开设计师分配弹窗
↓
2. 弹窗初始化 (ngOnInit)
├─ loadRealProjectTeams() - 加载项目组和成员
└─ loadRealProjectSpaces() - 加载项目空间
↓
3. ProductSpaceService.getProjectProductSpaces(projectId)
├─ 查询 Product 表
│ WHERE project = projectId
│ ORDER BY createdAt ASC
└─ 返回 Product[] 列表
↓
4. 转换为 SpaceScene[] 格式
{
id: product.id,
name: product.productName,
area: product.space?.area,
description: 'bedroom · 18.5㎡ · 进行中'
}
↓
5. 用户选择设计师并分配空间
├─ 点击设计师卡片的 "🏠" 按钮
├─ 勾选该设计师负责的空间
└─ 点击"确认"
↓
6. 生成 DesignerAssignmentResult
{
selectedDesigners: [Designer[]],
spaceAssignments: [
{
designerId: 'profile001',
designerName: '张设计师',
spaceIds: ['prod001', 'prod002'] // Product IDs
}
]
}
↓
7. 父组件保存到 ProjectTeam
ProjectTeam.data.assignedSpaces = ['prod001', 'prod002']
// ProductSpaceService 中的查询
const query = new Parse.Query('Product');
query.equalTo('project', {
__type: 'Pointer',
className: 'Project',
objectId: projectId
});
query.include('profile');
query.ascending('createdAt');
const results = await query.find();
// Product -> SpaceScene
const spaceScene: SpaceScene = {
id: 'prod001', // Product.objectId
name: '主卧设计', // Product.productName
area: 18.5, // Product.space.area
description: 'bedroom · 18.5㎡ · 进行中'
};
<app-designer-team-assignment-modal
[visible]="showDesignerModal"
[projectId]="project?.id || ''"
[loadRealData]="true"
[loadRealSpaces]="true" <!-- 启用真实空间加载 -->
[enableSpaceAssignment]="true"
[calendarViewMode]="'month'"
[selectedTeamId]="modalSelectedTeamId"
(close)="closeDesignerModal()"
(confirm)="handleDesignerAssignment($event)"
></app-designer-team-assignment-modal>
async handleDesignerAssignment(result: DesignerAssignmentResult) {
for (const designer of result.selectedDesigners) {
// 查找该设计师负责的空间
const spaceAssignment = result.spaceAssignments.find(
sa => sa.designerId === designer.id
);
// 保存到ProjectTeam,空间ID列表保存在data.assignedSpaces
await this.saveDesignerToTeam(
designer,
spaceAssignment?.spaceIds || []
);
}
}
async saveDesignerToTeam(designer: Designer, spaceIds: string[]) {
const ProjectTeam = Parse.Object.extend('ProjectTeam');
const teamMember = new ProjectTeam();
teamMember.set('project', this.project.toPointer());
teamMember.set('profile', Parse.Object.extend('Profile')
.createWithoutData(designer.id));
teamMember.set('role', '设计师');
teamMember.set('data', {
assignedSpaces: spaceIds, // Product IDs
assignedDate: new Date().toISOString()
});
await teamMember.save();
}
前提: 项目已创建多个 Product
操作: 打开设计师分配弹窗
预期:
- 显示"正在加载空间数据..."
- 成功加载后显示所有空间列表
- 每个空间显示名称、面积、描述
前提: 项目未创建任何 Product
操作: 打开设计师分配弹窗
预期:
- 显示空状态图标和提示
- 提示"该项目暂无空间数据"
- 引导用户创建空间产品
前提: 网络异常或服务器错误
操作: 打开设计师分配弹窗
预期:
- 显示错误图标和错误信息
- 错误信息清晰明了
- 可以关闭弹窗重试
前提: 项目有空间数据
操作:
1. 选择设计师
2. 点击 "🏠" 按钮
3. 勾选空间
4. 确认分配
预期:
- 空间列表正确显示
- 勾选状态正确更新
- 分配结果正确保存到 ProjectTeam.data.assignedSpaces
{
objectId: String,
project: Pointer<Project>,
profile: Pointer<Profile>, // 负责设计师
productName: String, // 空间名称
productType: String, // 空间类型
space: { // 空间信息
spaceName: String,
area: Number,
dimensions: Object,
features: Array
},
quotation: Object,
order: Number,
isDeleted: Boolean
}
{
objectId: String,
project: Pointer<Project>,
profile: Pointer<Profile>,
role: String,
workload: Number,
data: {
assignedSpaces: Array<String>, // Product objectId 数组
assignedDate: String
},
isDeleted: Boolean
}
查询项目空间
query.equalTo('project', projectPointer)
query.notEqualTo('isDeleted', true)
query.ascending('order')
查询设计师负责的空间
query.equalTo('profile', designerPointer)
query.notEqualTo('isDeleted', true)
查询项目团队的空间分配
const team = await teamQuery.find();
const assignedSpaces = team.get('data')?.assignedSpaces || [];
loadRealSpaces=true 时加载projectId 时加载loadingSpaces 避免重复加载ChangeDetectorRef 优化检测src/app/pages/designer/project-detail/components/designer-team-assignment-modal/
designer-team-assignment-modal.component.ts - 组件逻辑designer-team-assignment-modal.component.html - 模板designer-team-assignment-modal.component.scss - 样式src/modules/project/services/product-space.service.ts - 空间数据服务src/modules/project/components/team-assign/ - 团队分配组件集成rules/schemas.md - Parse Server 数据范式<app-designer-team-assignment-modal
[visible]="showModal"
[projectId]="project.id"
[loadRealSpaces]="true"
[enableSpaceAssignment]="true"
(confirm)="handleAssignment($event)"
></app-designer-team-assignment-modal>
<app-designer-team-assignment-modal
[visible]="showModal"
[loadRealSpaces]="false"
[spaceScenes]="customSpaces"
[enableSpaceAssignment]="true"
(confirm)="handleAssignment($event)"
></app-designer-team-assignment-modal>