映三色设计师项目管理系统面临的核心挑战是如何优雅地处理多产品场景下的项目管理。虽然大部分项目为单产品,但多产品项目在各个环节都有不同的数据组织和展示需求。
当前系统已具备产品管理的统一架构:
interface Product {
// 产品基本信息
objectId: string;
project: Pointer<Project>;
profile: Pointer<Profile>; // 负责设计师
productName: string; // "李总主卧设计"
productType: string; // "bedroom", "living_room" 等
status: 'not_started' | 'in_progress' | 'awaiting_review' | 'completed';
// 空间信息字段
space: {
spaceName: string; // "主卧"
area: number; // 18.5
dimensions: {
length: number;
width: number;
height: number;
};
features: string[]; // ["朝南", "飘窗", "独立卫浴"]
constraints: string[]; // ["承重墙不可动"]
priority: number; // 优先级 1-10
complexity: string; // "medium"
};
// 产品需求字段
requirements: {
colorRequirement: Object;
materialRequirement: Object;
lightingRequirement: Object;
specificRequirements: string[];
constraints: Object;
};
// 产品报价字段
quotation: {
price: number;
currency: string; // "CNY"
breakdown: {
design: number;
modeling: number;
rendering: number;
softDecor: number;
};
status: string; // "pending" | "approved"
validUntil: Date;
};
// 产品评价字段
reviews: Array<Object>;
}
enum SpaceType {
LIVING_ROOM = 'living_room', // 客厅
BEDROOM = 'bedroom', // 卧室
KITCHEN = 'kitchen', // 厨房
BATHROOM = 'bathroom', // 卫生间
DINING_ROOM = 'dining_room', // 餐厅
STUDY = 'study', // 书房
BALCONY = 'balcony', // 阳台
CORRIDOR = 'corridor', // 走廊
STORAGE = 'storage', // 储物间
ENTRANCE = 'entrance', // 玄关
OTHER = 'other' // 其他
}
interface SpaceProgress {
spaceId: string; // 关联空间ID
stage: ProjectStage; // 当前阶段
progress: number; // 进度百分比 0-100
status: ProgressStatus;
timeline: StageTimeline[]; // 各阶段时间线
blockers?: string[]; // 阻碍因素
estimatedCompletion?: Date; // 预计完成时间
}
interface StageTimeline {
stage: ProjectStage;
startTime?: Date;
endTime?: Date;
duration?: number; // 持续时间(小时)
status: 'not_started' | 'in_progress' | 'completed' | 'blocked';
assignee?: string; // 负责人ID
}
enum ProgressStatus {
NOT_STARTED = 'not_started',
IN_PROGRESS = 'in_progress',
AWAITING_REVIEW = 'awaiting_review',
COMPLETED = 'completed',
BLOCKED = 'blocked',
DELAYED = 'delayed'
}
interface SpaceAssignment {
spaceId: string; // 空间ID
stage: ProjectStage; // 阶段
assigneeId: string; // 负责人ID
assigneeName: string; // 负责人姓名
role: AssignmentRole; // 分配角色
assignedAt: Date; // 分配时间
assignedBy: string; // 分配人ID
status: 'active' | 'completed' | 'reassigned';
workload: number; // 工作量占比 0-1
notes?: string; // 分配备注
}
enum AssignmentRole {
PRIMARY_DESIGNER = 'primary_designer', // 主设计师
MODELING_DESIGNER = 'modeling_designer', // 建模师
RENDERING_DESIGNER = 'rendering_designer',// 渲染师
SOFT_DECOR_DESIGNER = 'soft_decor_designer', // 软装师
QUALITY_REVIEWER = 'quality_reviewer' // 质量审核员
}
// 扩展 Project.quotation 数据结构
interface MultiSpaceQuotation {
totalAmount: number; // 总金额
currency: string; // 货币单位
// 按空间分项报价
spaceQuotations: SpaceQuotation[];
// 按费用类型汇总
breakdown: {
design: number; // 设计费
modeling: number; // 建模费
rendering: number; // 渲染费
softDecor: number; // 软装费
postProcess: number; // 后期费
};
// 折扣信息
discount?: {
type: 'percentage' | 'fixed';
value: number;
reason: string;
};
}
interface SpaceQuotation {
spaceId: string; // 空间ID
spaceName: string; // 空间名称
amount: number; // 该空间金额
items: QuotationItem[]; // 报价项明细
priority: number; // 优先级
notes?: string; // 备注
}
interface QuotationItem {
id: string;
category: 'design' | 'modeling' | 'rendering' | 'soft_decor' | 'post_process';
description: string; // 项目描述
quantity: number; // 数量
unitPrice: number; // 单价
totalPrice: number; // 小计
}
// 扩展 ProjectRequirement 数据结构
interface MultiSpaceRequirement {
spaces: SpaceRequirement[]; // 空间需求列表
globalRequirements: GlobalRequirements; // 全局需求
crossSpaceRequirements: CrossSpaceRequirement[]; // 跨空间需求
}
interface SpaceRequirement {
spaceId: string; // 空间ID
spaceName: string; // 空间名称
// 四大需求数据
colorRequirement: ColorAtmosphereRequirement;
spaceStructureRequirement: SpaceStructureRequirement;
materialRequirement: MaterialRequirement;
lightingRequirement: LightingRequirement;
// 空间特定需求
specificRequirements: {
functional?: string[]; // 功能需求:收纳、展示等
style?: string[]; // 风格偏好
constraints?: string[]; // 限制条件:承重、管道等
specialFeatures?: string[]; // 特殊功能:智能家居、无障碍设计等
};
priority: number; // 优先级
complexity: 'simple' | 'medium' | 'complex'; // 复杂度
}
interface GlobalRequirements {
overallStyle: string; // 整体风格
budget: {
total: number;
currency: string;
breakdown?: Record<string, number>;
};
timeline: {
preferredStartDate?: Date;
deadline: Date;
milestones?: Array<{
date: Date;
description: string;
}>;
};
familyComposition: string; // 家庭构成
lifestyle: string[]; // 生活习惯
}
interface CrossSpaceRequirement {
type: 'style_consistency' | 'color_flow' | 'material_matching' | 'traffic_flow';
description: string; // 跨空间需求描述
involvedSpaces: string[]; // 涉及的空间ID列表
priority: number; // 优先级
}
// 扩展现有的 deliveryProcesses 数据结构
interface MultiSpaceDeliveryProcess {
processId: string; // 流程ID:modeling、softDecor、rendering、postProcess
processName: string; // 流程名称
// 空间管理(增强版)
spaces: DeliverySpace[];
// 按空间组织的内容
content: Record<string, SpaceContent>;
// 跨空间协调
crossSpaceCoordination: {
dependencies: SpaceDependency[]; // 空间依赖关系
batchOperations: BatchOperation[]; // 批量操作
qualityStandards: QualityStandard[]; // 质量标准
};
// 整体进度管理
overallProgress: {
total: number; // 总体进度
bySpace: Record<string, number>; // 各空间进度
byStage: Record<string, number>; // 各阶段进度
estimatedCompletion: Date;
};
}
interface SpaceDependency {
fromSpace: string; // 源空间
toSpace: string; // 目标空间
type: 'style_reference' | 'color_flow' | 'material_matching' | 'size_reference';
description: string; // 依赖描述
status: 'pending' | 'satisfied' | 'blocked';
}
interface BatchOperation {
id: string;
type: 'style_sync' | 'color_adjustment' | 'material_update';
targetSpaces: string[]; // 目标空间列表
operation: any; // 具体操作内容
status: 'pending' | 'in_progress' | 'completed';
createdBy: string;
createdAt: Date;
}
interface QualityStandard {
spaceType: SpaceType; // 空间类型
criteria: QualityCriterion[]; // 质量标准
applyToAll: boolean; // 是否应用到所有该类型空间
}
interface QualityCriterion {
aspect: string; // 质量维度:色彩、材质、比例等
standard: string; // 标准描述
tolerance: string; // 容差范围
checkMethod: string; // 检查方法
}
// 扩展售后数据结构
interface MultiSpaceAfterCare {
spaceReviews: SpaceReview[]; // 各空间评价
crossSpaceAnalysis: CrossSpaceAnalysis; // 跨空间分析
spaceComparison: SpaceComparison[]; // 空间对比
}
interface SpaceReview {
spaceId: string; // 空间ID
spaceName: string; // 空间名称
// 客户评价
customerRating: {
overall: number; // 整体评分 1-5
aspects: {
design: number; // 设计评分
functionality: number; // 功能性评分
aesthetics: number; // 美观度评分
practicality: number; // 实用性评分
};
feedback: string; // 具体反馈
};
// 使用情况
usageMetrics: {
satisfaction: number; // 满意度 0-100
usageFrequency: string; // 使用频率
modifications: string[]; // 后续改动
issues: string[]; // 问题记录
};
// 经济价值
economicValue: {
costPerSpace: number; // 单空间成本
perceivedValue: number; // 感知价值
roi: number; // 投资回报率
};
}
interface CrossSpaceAnalysis {
styleConsistency: {
score: number; // 风格一致性评分 0-100
issues: string[]; // 不一致问题
improvements: string[]; // 改进建议
};
functionalFlow: {
score: number; // 功能流线评分
bottlenecks: string[]; // 瓶颈问题
optimizations: string[]; // 优化建议
};
spaceUtilization: {
efficiency: number; // 空间利用率
recommendations: string[]; // 优化建议
};
}
interface SpaceComparison {
spaceId: string; // 对比空间ID
comparisonType: 'before_after' | 'design_vs_reality' | 'similar_projects';
metrics: ComparisonMetric[];
insights: string[]; // 洞察发现
lessons: string[]; // 经验教训
}
interface ComparisonMetric {
name: string; // 指标名称
beforeValue?: number; // 改造前数值
afterValue?: number; // 改造后数值
plannedValue?: number; // 计划数值
actualValue?: number; // 实际数值
unit: string; // 单位
improvement?: number; // 改善程度
}
graph TD
A[客服接收需求] --> B{是否多空间项目?}
B -->|否| C[创建单空间项目]
B -->|是| D[分析空间需求]
D --> E[识别潜在空间]
E --> F[创建空间列表]
F --> G[设置空间优先级]
G --> H[分配空间ID]
H --> I[进入订单分配阶段]
style C fill:#e8f5e9
style I fill:#e3f2fd
class SpaceIdentifier {
// 基于关键词识别空间
identifySpacesFromDescription(description: string): string[] {
const spaceKeywords = {
[SpaceType.LIVING_ROOM]: ['客厅', '起居室', '会客厅', '茶室'],
[SpaceType.BEDROOM]: ['卧室', '主卧', '次卧', '儿童房', '老人房', '客房'],
[SpaceType.KITCHEN]: ['厨房', '开放式厨房', '中西厨'],
[SpaceType.BATHROOM]: ['卫生间', '浴室', '洗手间', '盥洗室'],
[SpaceType.DINING_ROOM]: ['餐厅', '餐厅区', '用餐区'],
[SpaceType.STUDY]: ['书房', '工作室', '办公室'],
[SpaceType.BALCONY]: ['阳台', '露台', '花园'],
[SpaceType.CORRIDOR]: ['走廊', '过道', '玄关'],
[SpaceType.STORAGE]: ['储物间', '衣帽间', '杂物间']
};
const identifiedSpaces: string[] = [];
for (const [spaceType, keywords] of Object.entries(spaceKeywords)) {
if (keywords.some(keyword => description.includes(keyword))) {
identifiedSpaces.push(spaceType);
}
}
return identifiedSpaces.length > 0 ? identifiedSpaces : [SpaceType.LIVING_ROOM];
}
// 基于面积和预算推断空间数量
estimateSpaceCount(totalArea: number, budget: number): number {
// 基于面积的空间数量估算
const areaBasedCount = Math.max(1, Math.floor(totalArea / 20)); // 每20平米一个主要空间
// 基于预算的空间数量估算
const budgetBasedCount = Math.max(1, Math.floor(budget / 30000)); // 每3万一个空间
// 综合判断
return Math.min(areaBasedCount, budgetBasedCount);
}
}
class MultiSpacePricingCalculator {
calculateSpacePricing(
spaces: ProjectSpace[],
globalRequirements: GlobalRequirements,
pricingRules: PricingRule[]
): MultiSpaceQuotation {
const spaceQuotations: SpaceQuotation[] = [];
let totalAmount = 0;
for (const space of spaces) {
const spaceQuotation = this.calculateSingleSpacePricing(space, globalRequirements, pricingRules);
spaceQuotations.push(spaceQuotation);
totalAmount += spaceQuotation.amount;
}
// 应用多空间折扣
const discount = this.calculateMultiSpaceDiscount(spaces.length, totalAmount);
const finalAmount = totalAmount - discount.value;
return {
totalAmount: finalAmount,
currency: 'CNY',
spaceQuotations,
breakdown: this.calculateBreakdown(spaceQuotations),
discount: discount.value > 0 ? discount : undefined
};
}
private calculateSingleSpacePricing(
space: ProjectSpace,
globalRequirements: GlobalRequirements,
pricingRules: PricingRule[]
): SpaceQuotation {
const basePrice = this.getBasePriceForSpaceType(space.type, space.area || 0);
const complexityMultiplier = this.getComplexityMultiplier(space.metadata.features || []);
const priorityAdjustment = this.getPriorityAdjustment(space.priority);
const items: QuotationItem[] = [
{
id: `${space.id}_design`,
category: 'design',
description: '设计费',
quantity: 1,
unitPrice: basePrice * 0.3 * complexityMultiplier,
totalPrice: basePrice * 0.3 * complexityMultiplier
},
{
id: `${space.id}_modeling`,
category: 'modeling',
description: '建模费',
quantity: 1,
unitPrice: basePrice * 0.25 * complexityMultiplier,
totalPrice: basePrice * 0.25 * complexityMultiplier
},
{
id: `${space.id}_rendering`,
category: 'rendering',
description: '渲染费',
quantity: 1,
unitPrice: basePrice * 0.25 * complexityMultiplier,
totalPrice: basePrice * 0.25 * complexityMultiplier
},
{
id: `${space.id}_soft_decor`,
category: 'soft_decor',
description: '软装费',
quantity: 1,
unitPrice: basePrice * 0.15 * complexityMultiplier,
totalPrice: basePrice * 0.15 * complexityMultiplier
},
{
id: `${space.id}_post_process`,
category: 'post_process',
description: '后期费',
quantity: 1,
unitPrice: basePrice * 0.05 * complexityMultiplier,
totalPrice: basePrice * 0.05 * complexityMultiplier
}
];
const totalAmount = items.reduce((sum, item) => sum + item.totalPrice, 0);
return {
spaceId: space.id,
spaceName: space.name,
amount: totalAmount * priorityAdjustment,
items,
priority: space.priority,
notes: `复杂度系数: ${complexityMultiplier}, 优先级调整: ${priorityAdjustment}`
};
}
private calculateMultiSpaceDiscount(spaceCount: number, totalAmount: number): { type: string; value: number; reason: string } {
if (spaceCount >= 5) {
return {
type: 'percentage',
value: totalAmount * 0.1, // 10% 折扣
reason: '5空间以上项目享受10%折扣'
};
} else if (spaceCount >= 3) {
return {
type: 'percentage',
value: totalAmount * 0.05, // 5% 折扣
reason: '3-4空间项目享受5%折扣'
};
} else if (totalAmount > 200000) {
return {
type: 'fixed',
value: 5000,
reason: '高额度项目固定优惠5000元'
};
}
return { type: 'percentage', value: 0, reason: '无折扣' };
}
}
class MultiSpaceRequirementCollector {
async collectRequirements(
spaces: ProjectSpace[],
globalRequirements: GlobalRequirements
): Promise<MultiSpaceRequirement> {
const spaceRequirements: SpaceRequirement[] = [];
// 1. 并行采集各空间需求
const requirementPromises = spaces.map(space =>
this.collectSpaceRequirements(space, globalRequirements)
);
const collectedRequirements = await Promise.all(requirementPromises);
spaceRequirements.push(...collectedRequirements);
// 2. 分析跨空间需求
const crossSpaceRequirements = await this.analyzeCrossSpaceRequirements(spaceRequirements);
// 3. 验证需求一致性
await this.validateRequirementConsistency(spaceRequirements, crossSpaceRequirements);
return {
spaces: spaceRequirements,
globalRequirements,
crossSpaceRequirements
};
}
private async collectSpaceRequirements(
space: ProjectSpace,
globalRequirements: GlobalRequirements
): Promise<SpaceRequirement> {
// 基于空间类型预填充需求模板
const template = this.getSpaceRequirementTemplate(space.type);
// 采集四大核心需求
const colorRequirement = await this.collectColorRequirement(space, template.colorTemplate);
const spaceStructureRequirement = await this.collectSpaceStructureRequirement(space, template.structureTemplate);
const materialRequirement = await this.collectMaterialRequirement(space, template.materialTemplate);
const lightingRequirement = await this.collectLightingRequirement(space, template.lightingTemplate);
// 采集空间特定需求
const specificRequirements = await this.collectSpecificRequirements(space, template.specificTemplate);
return {
spaceId: space.id,
spaceName: space.name,
colorRequirement,
spaceStructureRequirement,
materialRequirement,
lightingRequirement,
specificRequirements,
priority: space.priority,
complexity: this.assessSpaceComplexity(space, specificRequirements)
};
}
private async analyzeCrossSpaceRequirements(
spaceRequirements: SpaceRequirement[]
): Promise<CrossSpaceRequirement[]> {
const crossSpaceRequirements: CrossSpaceRequirement[] = [];
// 分析风格一致性需求
const styleRequirement = this.analyzeStyleConsistency(spaceRequirements);
if (styleRequirement) crossSpaceRequirements.push(styleRequirement);
// 分析色彩流线需求
const colorFlowRequirement = this.analyzeColorFlow(spaceRequirements);
if (colorFlowRequirement) crossSpaceRequirements.push(colorFlowRequirement);
// 分析材质匹配需求
const materialMatchingRequirement = this.analyzeMaterialMatching(spaceRequirements);
if (materialMatchingRequirement) crossSpaceRequirements.push(materialMatchingRequirement);
// 分析动线连接需求
const trafficFlowRequirement = this.analyzeTrafficFlow(spaceRequirements);
if (trafficFlowRequirement) crossSpaceRequirements.push(trafficFlowRequirement);
return crossSpaceRequirements;
}
}
class SpaceDependencyManager {
analyzeSpaceDependencies(spaces: ProjectSpace[]): SpaceDependency[] {
const dependencies: SpaceDependency[] = [];
// 分析风格参考依赖
const styleDependencies = this.analyzeStyleDependencies(spaces);
dependencies.push(...styleDependencies);
// 分析色彩流线依赖
const colorDependencies = this.analyzeColorDependencies(spaces);
dependencies.push(...colorDependencies);
// 分析尺寸参考依赖
const sizeDependencies = this.analyzeSizeDependencies(spaces);
dependencies.push(...sizeDependencies);
return dependencies;
}
private analyzeStyleDependencies(spaces: ProjectSpace[]): SpaceDependency[] {
const dependencies: SpaceDependency[] = [];
const livingRoom = spaces.find(s => s.type === SpaceType.LIVING_ROOM);
if (livingRoom) {
// 客厅通常是风格参考基准
const otherSpaces = spaces.filter(s => s.id !== livingRoom.id);
for (const space of otherSpaces) {
dependencies.push({
fromSpace: livingRoom.id,
toSpace: space.id,
type: 'style_reference',
description: `${space.name}需要与客厅风格保持一致`,
status: 'pending'
});
}
}
return dependencies;
}
async resolveDependency(dependency: SpaceDependency): Promise<boolean> {
switch (dependency.type) {
case 'style_reference':
return await this.resolveStyleDependency(dependency);
case 'color_flow':
return await this.resolveColorDependency(dependency);
case 'material_matching':
return await this.resolveMaterialDependency(dependency);
case 'size_reference':
return await this.resolveSizeDependency(dependency);
default:
return false;
}
}
private async resolveStyleDependency(dependency: SpaceDependency): Promise<boolean> {
// 实现风格依赖解决逻辑
// 1. 获取源空间的设计方案
// 2. 提取关键风格元素
// 3. 应用到目标空间
// 4. 验证一致性
console.log(`解决风格依赖: ${dependency.fromSpace} -> ${dependency.toSpace}`);
dependency.status = 'satisfied';
return true;
}
}
class SpaceBatchOperationManager {
async executeBatchOperation(operation: BatchOperation): Promise<boolean> {
try {
operation.status = 'in_progress';
switch (operation.type) {
case 'style_sync':
return await this.executeStyleSync(operation);
case 'color_adjustment':
return await this.executeColorAdjustment(operation);
case 'material_update':
return await this.executeMaterialUpdate(operation);
default:
throw new Error(`未知的批量操作类型: ${operation.type}`);
}
} catch (error) {
console.error(`批量操作失败:`, error);
return false;
}
}
private async executeStyleSync(operation: BatchOperation): Promise<boolean> {
const { targetSpaces, operation: syncData } = operation;
// 获取风格同步数据
const sourceStyle = syncData.sourceStyle;
const styleElements = syncData.elements;
// 批量应用到目标空间
for (const spaceId of targetSpaces) {
await this.applyStyleToSpace(spaceId, sourceStyle, styleElements);
}
operation.status = 'completed';
return true;
}
private async executeColorAdjustment(operation: BatchOperation): Promise<boolean> {
const { targetSpaces, operation: colorData } = operation;
// 获取色彩调整数据
const colorPalette = colorData.colorPalette;
const adjustmentType = colorData.adjustmentType;
// 批量调整目标空间色彩
for (const spaceId of targetSpaces) {
await this.adjustSpaceColors(spaceId, colorPalette, adjustmentType);
}
operation.status = 'completed';
return true;
}
}
<!-- 空间概览界面 -->
<div class="space-overview-container">
<!-- 全局信息栏 -->
<div class="global-info-bar">
<div class="project-info">
<h3>{{ project.title }}</h3>
<span class="space-count">{{ spaces.length }}个空间</span>
<span class="total-budget">总预算: ¥{{ totalBudget.toLocaleString() }}</span>
</div>
<div class="overall-progress">
<div class="progress-circle">
<svg width="120" height="120">
<circle cx="60" cy="60" r="50" fill="none" stroke="#e0e0e0" stroke-width="8"/>
<circle cx="60" cy="60" r="50" fill="none" stroke="#4CAF50" stroke-width="8"
[attr.stroke-dasharray]="circumference"
[attr.stroke-dashoffset]="progressOffset"/>
</svg>
<div class="progress-text">
<span class="percentage">{{ overallProgress }}%</span>
<span class="label">总体进度</span>
</div>
</div>
</div>
</div>
<!-- 空间卡片网格 -->
<div class="spaces-grid">
@for (space of spaces; track space.id) {
<div class="space-card"
[class.priority-high]="space.priority >= 8"
[class.priority-medium]="space.priority >= 5 && space.priority < 8"
[class.status-completed]="space.status === 'completed'"
[class.status-in-progress]="space.status === 'in_progress'">
<!-- 空间头部 -->
<div class="space-header">
<div class="space-icon">
<i class="icon-{{ getSpaceIcon(space.type) }}"></i>
</div>
<div class="space-info">
<h4>{{ space.name }}</h4>
<span class="space-type">{{ getSpaceTypeName(space.type) }}</span>
@if (space.area) {
<span class="space-area">{{ space.area }}m²</span>
}
</div>
<div class="space-actions">
<button class="btn-icon" (click)="editSpace(space.id)" title="编辑">
<i class="icon-edit"></i>
</button>
<button class="btn-icon" (click)="viewSpaceDetails(space.id)" title="查看详情">
<i class="icon-view"></i>
</button>
</div>
</div>
<!-- 空间进度 -->
<div class="space-progress">
<div class="progress-bar">
<div class="progress-fill"
[style.width.%]="getSpaceProgress(space.id)"
[class.color-warning]="getSpaceProgress(space.id) < 50"
[class.color-success]="getSpaceProgress(space.id) >= 80">
</div>
</div>
<span class="progress-text">{{ getSpaceProgress(space.id) }}%</span>
</div>
<!-- 当前阶段 -->
<div class="current-stage">
<span class="stage-label">当前阶段:</span>
<span class="stage-value">{{ getCurrentStage(space.id) }}</span>
</div>
<!-- 负责人 -->
<div class="assignee-info">
@if (getSpaceAssignee(space.id)) {
<div class="assignee-avatar">
<img [src]="getSpaceAssignee(space.id).avatar" [alt]="getSpaceAssignee(space.id).name">
</div>
<span class="assignee-name">{{ getSpaceAssignee(space.id).name }}</span>
} @else {
<span class="no-assignee">未分配</span>
}
</div>
<!-- 空间状态标签 -->
<div class="space-tags">
@if (space.priority >= 8) {
<span class="tag tag-high">高优先级</span>
}
@if (getSpaceComplexity(space.id) === 'complex') {
<span class="tag tag-complex">复杂</span>
}
@if (hasCrossSpaceDependencies(space.id)) {
<span class="tag tag-dependency">依赖</span>
}
</div>
<!-- 快速操作 -->
<div class="quick-actions">
<button class="btn-small"
[disabled]="!canAdvanceStage(space.id)"
(click)="advanceSpaceStage(space.id)">
推进阶段
</button>
<button class="btn-small btn-secondary"
(click)="viewSpaceFiles(space.id)">
查看文件
</button>
</div>
</div>
}
<!-- 添加新空间卡片 -->
<div class="space-card add-space-card" (click)="showAddSpaceDialog = true">
<div class="add-space-content">
<i class="icon-plus"></i>
<span>添加空间</span>
</div>
</div>
</div>
</div>
<!-- 空间详情弹窗 -->
<div class="space-detail-modal" *ngIf="selectedSpaceId">
<div class="modal-overlay" (click)="closeSpaceDetails()"></div>
<div class="modal-content large">
<div class="modal-header">
<h3>{{ getSpaceName(selectedSpaceId) }} - 详细信息</h3>
<div class="header-actions">
<button class="btn-secondary" (click)="editSpace(selectedSpaceId)">
<i class="icon-edit"></i> 编辑空间
</button>
<button class="btn-secondary" (click)="exportSpaceReport(selectedSpaceId)">
<i class="icon-export"></i> 导出报告
</button>
<button class="btn-icon" (click)="closeSpaceDetails()">
<i class="icon-close"></i>
</button>
</div>
</div>
<div class="modal-body">
<!-- 标签页导航 -->
<div class="tab-navigation">
<button class="tab-btn"
[class.active]="activeTab === 'overview'"
(click)="activeTab = 'overview'">
概览
</button>
<button class="tab-btn"
[class.active]="activeTab === 'requirements'"
(click)="activeTab = 'requirements'">
需求
</button>
<button class="tab-btn"
[class.active]="activeTab === 'delivery'"
(click)="activeTab = 'delivery'">
交付
</button>
<button class="tab-btn"
[class.active]="activeTab === 'timeline'"
(click)="activeTab = 'timeline'">
时间线
</button>
<button class="tab-btn"
[class.active]="activeTab === 'dependencies'"
(click)="activeTab = 'dependencies'">
依赖关系
</button>
</div>
<!-- 标签页内容 -->
<div class="tab-content">
<!-- 概览标签页 -->
<div *ngIf="activeTab === 'overview'" class="overview-tab">
<div class="space-metadata">
<h4>空间信息</h4>
<div class="metadata-grid">
<div class="metadata-item">
<label>空间类型:</label>
<span>{{ getSpaceTypeName(getSpace(selectedSpaceId).type) }}</span>
</div>
<div class="metadata-item">
<label>面积:</label>
<span>{{ getSpace(selectedSpaceId).area }}m²</span>
</div>
<div class="metadata-item">
<label>优先级:</label>
<span class="priority-badge priority-{{ getSpace(selectedSpaceId).priority }}">
{{ getSpace(selectedSpaceId).priority }}
</span>
</div>
<div class="metadata-item">
<label>复杂度:</label>
<span>{{ getSpaceComplexity(selectedSpaceId) }}</span>
</div>
</div>
</div>
<div class="space-progress-detail">
<h4>进度详情</h4>
<div class="progress-stages">
@for (stage of getAllStages(); track stage) {
<div class="stage-progress-item"
[class.completed]="isStageCompleted(selectedSpaceId, stage)"
[class.current]="isCurrentStage(selectedSpaceId, stage)">
<div class="stage-icon">
<i class="icon-{{ getStageIcon(stage) }}"></i>
</div>
<div class="stage-info">
<span class="stage-name">{{ stage }}</span>
<span class="stage-time">{{ getStageTime(selectedSpaceId, stage) }}</span>
</div>
<div class="stage-progress">
<div class="progress-bar small">
<div class="progress-fill"
[style.width.%]="getStageProgress(selectedSpaceId, stage)">
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
<!-- 需求标签页 -->
<div *ngIf="activeTab === 'requirements'" class="requirements-tab">
<app-space-requirements-view
[spaceId]="selectedSpaceId"
[readonly]="isReadOnly()">
</app-space-requirements-view>
</div>
<!-- 交付标签页 -->
<div *ngIf="activeTab === 'delivery'" class="delivery-tab">
<app-space-delivery-view
[spaceId]="selectedSpaceId"
[readonly]="isReadOnly()">
</app-space-delivery-view>
</div>
<!-- 时间线标签页 -->
<div *ngIf="activeTab === 'timeline'" class="timeline-tab">
<app-space-timeline-view
[spaceId]="selectedSpaceId">
</app-space-timeline-view>
</div>
<!-- 依赖关系标签页 -->
<div *ngIf="activeTab === 'dependencies'" class="dependencies-tab">
<app-space-dependencies-view
[spaceId]="selectedSpaceId">
</app-space-dependencies-view>
</div>
</div>
</div>
</div>
</div>
<!-- 多空间文件浏览器 -->
<div class="multi-space-file-browser">
<!-- 空间选择器 -->
<div class="space-selector">
<div class="space-tabs">
@for (space of spaces; track space.id) {
<button class="space-tab"
[class.active]="selectedSpaceId === space.id"
[class.has-files]="getSpaceFileCount(space.id) > 0"
(click)="selectSpace(space.id)">
<div class="tab-content">
<i class="icon-{{ getSpaceIcon(space.type) }}"></i>
<span class="space-name">{{ space.name }}</span>
<span class="file-count" *ngIf="getSpaceFileCount(space.id) > 0">
{{ getSpaceFileCount(space.id) }}
</span>
</div>
</button>
}
</div>
<!-- 全选/批量操作 -->
<div class="batch-actions">
<label class="checkbox-label">
<input type="checkbox"
[(ngModel)]="selectAllSpaces"
(change)="toggleSelectAllSpaces()">
<span>全选空间</span>
</label>
@if (selectedSpaces.length > 0) {
<div class="selected-actions">
<span class="selected-count">已选择 {{ selectedSpaces.length }} 个空间</span>
<button class="btn-small" (click)="batchUploadFiles()">
批量上传
</button>
<button class="btn-small btn-secondary" (click)="batchDownloadFiles()">
批量下载
</button>
</div>
}
</div>
</div>
<!-- 文件列表 -->
<div class="file-content-area">
@if (selectedSpaceId) {
<div class="space-file-view">
<!-- 当前空间信息 -->
<div class="current-space-header">
<div class="space-info">
<i class="icon-{{ getSpaceIcon(getSpace(selectedSpaceId).type) }}"></i>
<h4>{{ getSpace(selectedSpaceId).name }}</h4>
<span class="file-total">{{ getSpaceFileCount(selectedSpaceId) }} 个文件</span>
</div>
<div class="view-options">
<div class="view-toggle">
<button class="btn-icon"
[class.active]="viewMode === 'grid'"
(click)="viewMode = 'grid'"
title="网格视图">
<i class="icon-grid"></i>
</button>
<button class="btn-icon"
[class.active]="viewMode === 'list'"
(click)="viewMode = 'list'"
title="列表视图">
<i class="icon-list"></i>
</button>
</div>
<button class="btn-primary" (click)="triggerFileUpload(selectedSpaceId)">
<i class="icon-upload"></i> 上传文件
</button>
</div>
</div>
<!-- 文件上传区域 -->
<div class="upload-zone"
[class.drag-over]="isDragOver"
(dragover)="isDragOver = true"
(dragleave)="isDragOver = false"
(drop)="handleFileDrop($event, selectedSpaceId)">
<div class="upload-prompt">
<i class="icon-upload"></i>
<p>拖拽文件到此处上传</p>
<p class="hint">或点击上传按钮选择文件</p>
</div>
</div>
<!-- 文件网格视图 -->
@if (viewMode === 'grid') {
<div class="files-grid">
@for (file of getSpaceFiles(selectedSpaceId); track file.id) {
<div class="file-card"
[class.selected]="selectedFiles.has(file.id)"
(click)="toggleFileSelection(file.id)">
<div class="file-preview">
@if (isImageFile(file)) {
<img [src]="file.url" [alt]="file.name">
} @else {
<div class="file-icon-placeholder">
<i class="icon-{{ getFileIcon(file.type) }}"></i>
</div>
}
</div>
<div class="file-info">
<span class="file-name" [title]="file.name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<span class="file-date">{{ formatDate(file.uploadTime) }}</span>
</div>
<div class="file-actions">
<button class="btn-icon" (click)="previewFile(file)" title="预览">
<i class="icon-eye"></i>
</button>
<button class="btn-icon" (click)="downloadFile(file)" title="下载">
<i class="icon-download"></i>
</button>
<button class="btn-icon" (click)="deleteFile(file)" title="删除">
<i class="icon-delete"></i>
</button>
</div>
</div>
}
</div>
}
<!-- 文件列表视图 -->
@if (viewMode === 'list') {
<div class="files-list">
<div class="list-header">
<div class="header-cell">
<input type="checkbox"
[(ngModel)]="selectAllFiles"
(change)="toggleSelectAllFiles()">
</div>
<div class="header-cell">文件名</div>
<div class="header-cell">大小</div>
<div class="header-cell">类型</div>
<div class="header-cell">上传时间</div>
<div class="header-cell">操作</div>
</div>
@for (file of getSpaceFiles(selectedSpaceId); track file.id) {
<div class="list-row"
[class.selected]="selectedFiles.has(file.id)">
<div class="list-cell">
<input type="checkbox"
[(ngModel)]="selectedFiles.has(file.id)"
(change)="toggleFileSelection(file.id)">
</div>
<div class="list-cell file-name-cell">
<i class="icon-{{ getFileIcon(file.type) }}"></i>
<span>{{ file.name }}</span>
</div>
<div class="list-cell">{{ formatFileSize(file.size) }}</div>
<div class="list-cell">{{ getFileTypeLabel(file.type) }}</div>
<div class="list-cell">{{ formatDate(file.uploadTime) }}</div>
<div class="list-cell actions-cell">
<button class="btn-icon small" (click)="previewFile(file)" title="预览">
<i class="icon-eye"></i>
</button>
<button class="btn-icon small" (click)="downloadFile(file)" title="下载">
<i class="icon-download"></i>
</button>
<button class="btn-icon small" (click)="deleteFile(file)" title="删除">
<i class="icon-delete"></i>
</button>
</div>
</div>
}
</div>
}
</div>
} @else {
<div class="no-space-selected">
<i class="icon-folder"></i>
<p>请选择一个空间查看文件</p>
</div>
}
</div>
</div>
-- 为 Project 表添加多空间支持字段
ALTER TABLE Project ADD COLUMN spaceType VARCHAR(20) DEFAULT 'single';
ALTER TABLE Project ADD COLUMN spaces JSON;
ALTER TABLE Project ADD COLUMN spaceProgress JSON;
ALTER TABLE Project ADD COLUMN spaceAssignment JSON;
-- 创建空间索引
CREATE INDEX idx_project_spaceType ON Project(spaceType);
CREATE INDEX idx_project_spaces ON Project USING GIN(spaces);
-- 创建项目空间表
CREATE TABLE ProjectSpace (
id VARCHAR(50) PRIMARY KEY,
projectId VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(50) NOT NULL,
area DECIMAL(8,2),
priority INTEGER DEFAULT 5,
status VARCHAR(20) DEFAULT 'pending',
metadata JSON,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (projectId) REFERENCES Project(objectId)
);
-- 创建空间进度表
CREATE TABLE SpaceProgress (
id VARCHAR(50) PRIMARY KEY,
spaceId VARCHAR(50) NOT NULL,
stage VARCHAR(50) NOT NULL,
progress INTEGER DEFAULT 0,
status VARCHAR(20) DEFAULT 'not_started',
timeline JSON,
blockers JSON,
estimatedCompletion DATETIME,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (spaceId) REFERENCES ProjectSpace(id)
);
-- 创建空间分配表
CREATE TABLE SpaceAssignment (
id VARCHAR(50) PRIMARY KEY,
spaceId VARCHAR(50) NOT NULL,
stage VARCHAR(50) NOT NULL,
assigneeId VARCHAR(50) NOT NULL,
assigneeName VARCHAR(100) NOT NULL,
role VARCHAR(50) NOT NULL,
assignedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
assignedBy VARCHAR(50),
status VARCHAR(20) DEFAULT 'active',
workload DECIMAL(3,2) DEFAULT 0.0,
notes TEXT,
FOREIGN KEY (spaceId) REFERENCES ProjectSpace(id)
);
class DataMigrationService {
async migrateExistingProjects(): Promise<void> {
console.log('开始迁移现有项目数据...');
// 1. 获取所有现有项目
const projects = await this.getAllProjects();
for (const project of projects) {
await this.migrateProject(project);
}
console.log('项目数据迁移完成');
}
private async migrateProject(project: any): Promise<void> {
// 2. 分析项目是否为多空间
const isMultiSpace = await this.analyzeProjectSpaceType(project);
if (isMultiSpace) {
// 3. 创建空间记录
const spaces = await this.createSpacesForProject(project);
// 4. 更新项目记录
await this.updateProjectWithSpaces(project.objectId, spaces);
// 5. 迁移交付数据到空间维度
await this.migrateDeliveryData(project, spaces);
// 6. 迁移需求数据到空间维度
await this.migrateRequirementData(project, spaces);
} else {
// 单空间项目,创建默认空间
const defaultSpace = await this.createDefaultSpace(project);
await this.updateProjectWithSpaces(project.objectId, [defaultSpace]);
}
}
private async analyzeProjectSpaceType(project: any): Promise<boolean> {
// 基于项目标题、描述、文件等信息判断是否为多空间
const indicators = [
project.title?.includes('全屋') || project.title?.includes('整套'),
project.data?.description?.includes('多空间'),
(project.data?.quotation?.items?.length || 0) > 3,
await this.hasMultipleRoomTypes(project)
];
return indicators.some(indicator => indicator === true);
}
private async createSpacesForProject(project: any): Promise<ProjectSpace[]> {
const spaces: ProjectSpace[] = [];
// 基于报价项创建空间
if (project.data?.quotation?.items) {
for (const item of project.data.quotation.items) {
const spaceType = this.inferSpaceTypeFromDescription(item.description);
if (spaceType) {
const space: ProjectSpace = {
id: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
projectId: project.objectId,
name: item.room || this.getDefaultSpaceName(spaceType),
type: spaceType,
priority: this.calculateSpacePriority(item.amount),
status: 'pending',
metadata: {
budgetAllocation: item.amount
},
createdAt: new Date(),
updatedAt: new Date()
};
spaces.push(space);
}
}
}
// 如果没有从报价识别出空间,创建默认空间
if (spaces.length === 0) {
const defaultSpace = await this.createDefaultSpace(project);
spaces.push(defaultSpace);
}
return spaces;
}
private async createDefaultSpace(project: any): Promise<ProjectSpace> {
return {
id: `space_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
projectId: project.objectId,
name: '主空间',
type: SpaceType.LIVING_ROOM,
priority: 5,
status: 'pending',
metadata: {},
createdAt: new Date(),
updatedAt: new Date()
};
}
private async migrateDeliveryData(project: any, spaces: ProjectSpace[]): Promise<void> {
// 迁移交付执行数据到对应空间
if (project.data?.deliveryProcesses) {
for (const process of project.data.deliveryProcesses) {
for (const space of spaces) {
// 将交付数据关联到对应空间
await this.createSpaceDeliveryData(space.id, process);
}
}
}
}
private async migrateRequirementData(project: any, spaces: ProjectSpace[]): Promise<void> {
// 迁移需求数据到对应空间
if (project.data?.requirements) {
for (const space of spaces) {
await this.createSpaceRequirementData(space.id, project.data.requirements);
}
}
}
}
class ProjectDataAdapter {
// 适配旧的单空间项目数据格式
adaptLegacyProject(legacyProject: any): Project {
const adaptedProject: Project = {
...legacyProject,
spaceType: 'single',
spaces: this.createDefaultSpaceFromLegacy(legacyProject),
spaceProgress: this.createDefaultProgressFromLegacy(legacyProject),
spaceAssignment: this.createDefaultAssignmentFromLegacy(legacyProject)
};
return adaptedProject;
}
private createDefaultSpaceFromLegacy(legacyProject: any): ProjectSpace[] {
return [{
id: `default_space_${legacyProject.objectId}`,
projectId: legacyProject.objectId,
name: '主空间',
type: SpaceType.LIVING_ROOM,
priority: 5,
status: this.legacyStatusToSpaceStatus(legacyProject.status),
metadata: {
legacyData: legacyProject.data
},
createdAt: legacyProject.createdAt,
updatedAt: legacyProject.updatedAt
}];
}
// 适配新的多空间数据格式到旧格式(用于兼容性接口)
adaptToLegacyFormat(multiSpaceProject: Project): any {
if (multiSpaceProject.spaceType === 'single') {
return {
...multiSpaceProject,
// 将单空间数据扁平化到原有格式
data: {
...multiSpaceProject.data,
deliveryProcesses: this.extractDeliveryProcessesFromSpaces(multiSpaceProject),
requirements: this.extractRequirementsFromSpaces(multiSpaceProject)
}
};
}
// 多空间项目返回增强格式的数据
return multiSpaceProject;
}
}
文档版本:v3.0 (Product表统一空间管理) 更新日期:2025-10-20 维护者:YSS Development Team