需求确认阶段包含"需求沟通"和"方案确认"两个子环节,是连接订单分配与交付执行的关键桥梁。该阶段通过AI辅助分析工具深入理解客户需求,并将抽象需求转化为可执行的设计方案。
graph LR
A[订单分配] --> B[需求沟通]
B --> C[方案确认]
C --> D[建模]
style B fill:#e3f2fd
style C fill:#e8f5e9
组件标签:
<app-requirements-confirm-card
#requirementsCard
[project]="project"
[readonly]="!canEditStage('需求沟通')"
(requirementDataUpdated)="onRequirementDataUpdated($event)"
(mappingDataUpdated)="onMappingDataUpdated($event)"
(uploadModalRequested)="onUploadModalRequested($event)"
(stageCompleted)="onRequirementsStageCompleted($event)">
</app-requirements-confirm-card>
四大需求采集流程:
interface ColorAtmosphereRequirement {
// 用户描述
description: string; // 客户对色彩氛围的文字描述
referenceImages: Array<{ // 参考图片
id: string;
url: string;
name: string;
uploadTime: Date;
}>;
// AI分析结果
colorAnalysisResult?: {
originalImage: string; // 原始参考图URL
colors: Array<{
hex: string; // 十六进制颜色值
rgb: string; // RGB值
percentage: number; // 占比 0-100
name: string; // 颜色名称
}>;
dominantColor: { // 主色
hex: string;
rgb: string;
name: string;
};
colorHarmony: { // 色彩调和
type: string; // 调和类型:monochromatic/analogous/complementary
temperature: 'warm' | 'neutral' | 'cool'; // 色温
contrast: number; // 对比度 0-100
};
mood: string; // 氛围:温馨/冷静/活力/优雅
};
// 映射到设计指标
colorIndicators?: {
mainColor: { r: number; g: number; b: number };
colorRange: string; // 色彩范围描述
colorTemperature: number; // 色温值(K)
};
}
上传触发:
// 用户点击"上传参考图"按钮
onUploadReferenceImages(event: Event): void {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files);
// 1. 验证文件类型
const validFiles = files.filter(file =>
/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)
);
if (validFiles.length === 0) {
alert('请上传有效的图片文件(JPG/PNG/GIF/BMP/WEBP)');
return;
}
// 2. 显示上传进度
this.isUploadingFiles = true;
this.uploadProgress = 0;
// 3. 上传文件到服务器
this.uploadFiles(validFiles).subscribe({
next: (uploadedFiles) => {
// 4. 添加到参考图列表
this.referenceImages.push(...uploadedFiles);
// 5. 触发AI色彩分析
this.triggerColorAnalysis(uploadedFiles[0].url);
},
error: (error) => {
console.error('上传失败:', error);
alert('图片上传失败,请重试');
this.isUploadingFiles = false;
}
});
}
AI色彩分析:
// ColorAnalysisService 调用
triggerColorAnalysis(imageUrl: string): void {
this.isAnalyzingColors = true;
this.colorAnalysisService.analyzeImage(imageUrl).subscribe({
next: (result: ColorAnalysisResult) => {
// 保存分析结果
this.colorAnalysisResult = result;
// 计算主色
const colors = result.colors || [];
if (colors.length > 0) {
const dominant = colors.reduce((max, cur) =>
cur.percentage > max.percentage ? cur : max,
colors[0]
);
this.dominantColorHex = dominant.hex;
}
// 映射到需求指标
this.mapColorResultToIndicators(result);
this.isAnalyzingColors = false;
// 通知父组件更新
this.requirementDataUpdated.emit({
colorAnalysisResult: result,
colorIndicators: this.colorIndicators
});
},
error: (error) => {
console.error('色彩分析失败:', error);
alert('AI色彩分析失败,请重试');
this.isAnalyzingColors = false;
}
});
}
色彩结果映射:
mapColorResultToIndicators(result: ColorAnalysisResult): void {
if (!result.dominantColor) return;
// 将十六进制颜色转换为RGB
const rgb = this.hexToRgb(result.dominantColor.hex);
this.colorIndicators = {
mainColor: { r: rgb.r, g: rgb.g, b: rgb.b },
colorRange: this.describeColorRange(result.colors),
colorTemperature: this.calculateColorTemperature(rgb)
};
}
// 计算色温(简化算法)
calculateColorTemperature(rgb: {r: number; g: number; b: number}): number {
// 基于RGB值估算色温
// 暖色调:2700K-3500K,中性:4000K-5000K,冷色调:5500K-6500K
const warmth = (rgb.r - rgb.b) / 255;
if (warmth > 0.3) return 2700 + warmth * 800; // 暖色调
if (warmth < -0.3) return 5500 - warmth * 1000; // 冷色调
return 4500; // 中性
}
右侧面板展示(project-detail.html lines 1826-1900):
<div class="analysis-visualization-panel">
<h4>色彩分析结果</h4>
@if (colorAnalysisResult) {
<!-- 主色展示 -->
<div class="dominant-color-display">
<div class="color-swatch"
[style.background-color]="dominantColorHex">
</div>
<div class="color-info">
<span class="color-name">{{ colorAnalysisResult.dominantColor.name }}</span>
<span class="color-hex">{{ colorAnalysisResult.dominantColor.hex }}</span>
</div>
</div>
<!-- 色彩占比饼图 -->
<div class="color-distribution">
@for (color of colorAnalysisResult.colors; track color.hex) {
<div class="color-bar">
<div class="color-swatch-small"
[style.background-color]="color.hex">
</div>
<div class="color-bar-fill"
[style.width.%]="color.percentage"
[style.background-color]="color.hex">
</div>
<span class="percentage">{{ color.percentage }}%</span>
</div>
}
</div>
<!-- 色彩调和信息 -->
<div class="color-harmony-info">
<div class="info-item">
<span class="label">调和类型:</span>
<span class="value">{{ getColorHarmonyName(colorAnalysisResult.colorHarmony?.type) }}</span>
</div>
<div class="info-item">
<span class="label">色温:</span>
<span class="value">{{ getTemperatureName(colorAnalysisResult.colorHarmony?.temperature) }}</span>
</div>
<div class="info-item">
<span class="label">对比度:</span>
<span class="value">{{ colorAnalysisResult.colorHarmony?.contrast }}%</span>
</div>
</div>
<!-- 色彩氛围标签 -->
<div class="mood-tags">
<span class="mood-tag">{{ colorAnalysisResult.mood }}</span>
</div>
<!-- 原图预览按钮 -->
<button class="btn-secondary" (click)="previewColorRefImage()">
查看原图
</button>
} @else {
<div class="empty-state">
<p>上传参考图后将显示AI色彩分析结果</p>
</div>
}
</div>
色彩轮盘可视化组件:
<app-color-wheel-visualizer
[colors]="colorAnalysisResult?.colors"
[dominantColor]="dominantColorHex"
[showHarmony]="true">
</app-color-wheel-visualizer>
interface SpaceStructureRequirement {
// CAD文件上传
cadFiles: Array<{
id: string;
name: string;
url: string;
uploadTime: Date;
fileSize: number;
}>;
// 手动输入
dimensions?: {
length: number; // 长度(米)
width: number; // 宽度(米)
height: number; // 层高(米)
area: number; // 面积(平方米)
};
// AI分析结果
spaceAnalysis?: {
dimensions: {
length: number;
width: number;
height: number;
area: number;
volume: number;
};
functionalZones: Array<{
zone: string; // 功能区名称
area: number;
percentage: number;
requirements: string[];
furniture: string[];
}>;
circulation: {
mainPaths: string[]; // 主要动线
pathWidth: number; // 动线宽度
efficiency: number; // 动线效率 0-100
};
layoutType: string; // 布局类型:open/enclosed/semi-open
};
// 映射到设计指标
spaceIndicators?: {
lineRatio: number; // 线条占比 0-1
blankRatio: number; // 留白占比 0-1
flowWidth: number; // 流线宽度
aspectRatio: number; // 空间比例
ceilingHeight: number; // 层高
};
}
文件上传:
onCADFilesSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files);
// 1. 验证文件类型(支持DWG/DXF/PDF等)
const validFiles = files.filter(file =>
/\.(dwg|dxf|pdf)$/i.test(file.name)
);
if (validFiles.length === 0) {
alert('请上传有效的CAD文件(DWG/DXF/PDF)');
return;
}
// 2. 上传并解析CAD文件
this.uploadAndParseCAD(validFiles).subscribe({
next: (parsedData) => {
this.cadFiles.push(...parsedData.files);
this.spaceAnalysis = parsedData.analysis;
// 映射到设计指标
this.mapSpaceAnalysisToIndicators(parsedData.analysis);
// 通知父组件
this.requirementDataUpdated.emit({
spaceAnalysis: this.spaceAnalysis,
spaceIndicators: this.spaceIndicators
});
},
error: (error) => {
console.error('CAD解析失败:', error);
alert('CAD文件解析失败,请检查文件格式');
}
});
}
空间指标映射:
mapSpaceAnalysisToIndicators(analysis: SpaceAnalysis): void {
if (!analysis) return;
const { dimensions, functionalZones, circulation } = analysis;
this.spaceIndicators = {
// 线条占比:基于功能区划分密度
lineRatio: functionalZones.length / 10, // 简化计算
// 留白占比:基于功能区总占比
blankRatio: 1 - functionalZones.reduce((sum, zone) =>
sum + zone.percentage / 100, 0
),
// 流线宽度
flowWidth: circulation.pathWidth,
// 空间比例(长宽比)
aspectRatio: dimensions.length / dimensions.width,
// 层高
ceilingHeight: dimensions.height
};
}
空间分区图表:
<div class="space-zones-chart">
<h4>功能区分布</h4>
@if (spaceAnalysis?.functionalZones) {
<div class="zones-grid">
@for (zone of spaceAnalysis.functionalZones; track zone.zone) {
<div class="zone-card">
<div class="zone-header">
<span class="zone-name">{{ zone.zone }}</span>
<span class="zone-percentage">{{ zone.percentage }}%</span>
</div>
<div class="zone-area">面积:{{ zone.area }}m²</div>
<div class="zone-requirements">
<span class="label">需求:</span>
<div class="tags">
@for (req of zone.requirements; track req) {
<span class="tag">{{ req }}</span>
}
</div>
</div>
<div class="zone-furniture">
<span class="label">家具:</span>
<div class="tags">
@for (furn of zone.furniture; track furn) {
<span class="tag furniture-tag">{{ furn }}</span>
}
</div>
</div>
</div>
}
</div>
}
</div>
动线效率雷达图:
<div class="circulation-chart">
<h4>动线分析</h4>
@if (spaceAnalysis?.circulation) {
<div class="circulation-info">
<div class="info-row">
<span class="label">主要动线:</span>
<span class="value">{{ spaceAnalysis.circulation.mainPaths.join(' → ') }}</span>
</div>
<div class="info-row">
<span class="label">动线宽度:</span>
<span class="value">{{ spaceAnalysis.circulation.pathWidth }}m</span>
</div>
<div class="info-row">
<span class="label">效率评分:</span>
<div class="efficiency-bar">
<div class="bar-fill"
[style.width.%]="spaceAnalysis.circulation.efficiency"
[class.excellent]="spaceAnalysis.circulation.efficiency >= 85"
[class.good]="spaceAnalysis.circulation.efficiency >= 70 && spaceAnalysis.circulation.efficiency < 85"
[class.average]="spaceAnalysis.circulation.efficiency < 70">
</div>
<span class="score">{{ spaceAnalysis.circulation.efficiency }}分</span>
</div>
</div>
</div>
}
</div>
interface MaterialRequirement {
// 参考图片
materialImages: Array<{
id: string;
url: string;
name: string;
}>;
// AI材质识别结果
materialAnalysis?: Array<{
id: string;
name: string; // 材质名称
category: string; // 类别:wood/metal/fabric/leather/plastic/glass/ceramic/stone
confidence: number; // 识别置信度 0-1
properties: {
texture: string; // 纹理:smooth/rough/woven/carved
color: string;
finish: string; // 表面处理:matte/glossy/satin
hardness: number; // 硬度 0-10
};
usage: {
suitableAreas: string[]; // 适用区域
priority: 'primary' | 'secondary' | 'accent';
};
}>;
// 映射到设计指标
materialIndicators?: {
fabricRatio: number; // 布艺占比 0-100
woodRatio: number; // 木质占比 0-100
metalRatio: number; // 金属占比 0-100
smoothness: number; // 平滑度 0-10
glossiness: number; // 光泽度 0-10
};
}
图片上传触发识别:
onMaterialImagesSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files);
this.isAnalyzingMaterials = true;
// 1. 上传图片
this.uploadFiles(files).subscribe({
next: (uploadedFiles) => {
this.materialImages.push(...uploadedFiles);
// 2. 触发AI材质识别
this.analyzeMaterials(uploadedFiles.map(f => f.url));
}
});
}
analyzeMaterials(imageUrls: string[]): void {
// 调用材质识别服务(可以是本地模型或云端API)
this.materialAnalysisService.analyzeImages(imageUrls).subscribe({
next: (results) => {
this.materialAnalysisData = results;
// 计算材质权重
this.calculateMaterialWeights(results);
this.isAnalyzingMaterials = false;
// 通知父组件
this.requirementDataUpdated.emit({
materialAnalysisData: results,
materialIndicators: this.materialIndicators
});
},
error: (error) => {
console.error('材质识别失败:', error);
this.isAnalyzingMaterials = false;
}
});
}
材质权重计算:
calculateMaterialWeights(materials: MaterialAnalysis[]): void {
if (!materials || materials.length === 0) return;
// 按类别分组统计
const categoryCount: Record<string, number> = {};
const categoryConfidence: Record<string, number> = {};
materials.forEach(mat => {
categoryCount[mat.category] = (categoryCount[mat.category] || 0) + 1;
categoryConfidence[mat.category] =
(categoryConfidence[mat.category] || 0) + mat.confidence;
});
const total = materials.length;
// 计算加权占比
this.materialIndicators = {
fabricRatio: Math.round(
(categoryCount['fabric'] || 0) / total *
(categoryConfidence['fabric'] || 0) / (categoryCount['fabric'] || 1) *
100
),
woodRatio: Math.round(
(categoryCount['wood'] || 0) / total *
(categoryConfidence['wood'] || 0) / (categoryCount['wood'] || 1) *
100
),
metalRatio: Math.round(
(categoryCount['metal'] || 0) / total *
(categoryConfidence['metal'] || 0) / (categoryCount['metal'] || 1) *
100
),
// 根据材质属性计算平滑度和光泽度
smoothness: this.calculateAverageSmoothness(materials),
glossiness: this.calculateAverageGlossiness(materials)
};
}
calculateAverageSmoothness(materials: MaterialAnalysis[]): number {
const textureScores: Record<string, number> = {
'smooth': 10,
'satin': 7,
'rough': 3,
'woven': 5,
'carved': 2
};
const scores = materials
.map(m => textureScores[m.properties.texture] || 5)
.filter(s => s > 0);
return scores.length > 0
? Math.round(scores.reduce((sum, s) => sum + s, 0) / scores.length)
: 5;
}
材质卡片网格:
<div class="material-analysis-grid">
<h4>识别的材质</h4>
@if (materialAnalysisData && materialAnalysisData.length > 0) {
<div class="material-cards">
@for (material of materialAnalysisData; track material.id) {
<div class="material-card">
<div class="material-header">
<span class="material-name">{{ material.name }}</span>
<span class="confidence-badge"
[class.high]="material.confidence >= 0.8"
[class.medium]="material.confidence >= 0.6 && material.confidence < 0.8"
[class.low]="material.confidence < 0.6">
{{ (material.confidence * 100).toFixed(0) }}%
</span>
</div>
<div class="material-category">
{{ getMaterialName(material.category) }}
</div>
<div class="material-properties">
<div class="property">
<span class="prop-label">纹理:</span>
<span class="prop-value">{{ material.properties.texture }}</span>
</div>
<div class="property">
<span class="prop-label">表面:</span>
<span class="prop-value">{{ material.properties.finish }}</span>
</div>
<div class="property">
<span class="prop-label">硬度:</span>
<div class="hardness-bar">
<div class="bar-fill"
[style.width.%]="material.properties.hardness * 10">
</div>
</div>
</div>
</div>
<div class="material-usage">
<span class="usage-label">适用区域:</span>
<div class="area-tags">
@for (area of material.usage.suitableAreas; track area) {
<span class="area-tag">{{ area }}</span>
}
</div>
</div>
</div>
}
</div>
<!-- 材质占比饼图 -->
<div class="material-distribution-chart">
<h5>材质分布</h5>
<div class="pie-chart">
<!-- 使用图表库绘制饼图 -->
<canvas #materialPieChart></canvas>
</div>
<div class="chart-legend">
<div class="legend-item">
<span class="color-dot" style="background-color: #8B4513;"></span>
<span>木质 {{ materialIndicators?.woodRatio }}%</span>
</div>
<div class="legend-item">
<span class="color-dot" style="background-color: #C0C0C0;"></span>
<span>金属 {{ materialIndicators?.metalRatio }}%</span>
</div>
<div class="legend-item">
<span class="color-dot" style="background-color: #DEB887;"></span>
<span>布艺 {{ materialIndicators?.fabricRatio }}%</span>
</div>
</div>
</div>
} @else {
<div class="empty-state">
上传材质参考图后将显示AI识别结果
</div>
}
</div>
纹理对比可视化组件:
<app-texture-comparison-visualizer
[materials]="materialAnalysisData"
[showProperties]="true">
</app-texture-comparison-visualizer>
interface LightingRequirement {
// 照明场景图片
lightingImages: Array<{
id: string;
url: string;
name: string;
}>;
// AI光照分析结果
lightingAnalysis?: {
naturalLight: {
direction: string[]; // 采光方向:north/south/east/west
intensity: string; // 光照强度:strong/moderate/weak
duration: string; // 日照时长
quality: number; // 光照质量 0-100
};
artificialLight: {
mainLighting: {
type: string; // 主照明类型:ceiling/chandelier/downlight
distribution: string; // 分布方式:uniform/concentrated/layered
brightness: number; // 亮度 0-100
};
accentLighting: {
type: string; // 重点照明类型:spotlight/wallwash/uplighting
locations: string[];
intensity: number;
};
ambientLighting: {
type: string; // 环境照明类型:cove/indirect/decorative
mood: string; // 氛围:warm/cool/neutral
colorTemperature: number; // 色温(K)
};
};
lightingMood: string; // 整体照明氛围:dramatic/romantic/energetic/calm
};
// 映射到设计指标
lightingIndicators?: {
naturalLightRatio: number; // 自然光占比 0-1
artificialLightRatio: number; // 人工光占比 0-1
mainLightIntensity: number; // 主光强度 0-100
accentLightIntensity: number; // 辅助光强度 0-100
ambientColorTemp: number; // 环境色温(K)
};
}
图片上传触发分析:
onLightingImagesSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files);
this.isAnalyzingLighting = true;
// 1. 上传图片
this.uploadFiles(files).subscribe({
next: (uploadedFiles) => {
this.lightingImages.push(...uploadedFiles);
// 2. 触发AI光照分析
this.analyzeLighting(uploadedFiles.map(f => f.url));
}
});
}
analyzeLighting(imageUrls: string[]): void {
this.lightingAnalysisService.analyzeImages(imageUrls).subscribe({
next: (result) => {
this.lightingAnalysis = result;
// 映射到设计指标
this.mapLightingToIndicators(result);
this.isAnalyzingLighting = false;
// 通知父组件
this.requirementDataUpdated.emit({
lightingAnalysis: result,
lightingIndicators: this.lightingIndicators
});
},
error: (error) => {
console.error('光照分析失败:', error);
this.isAnalyzingLighting = false;
}
});
}
光照指标映射:
mapLightingToIndicators(analysis: LightingAnalysis): void {
if (!analysis) return;
// 根据自然光质量和人工光配置计算占比
const naturalQuality = analysis.naturalLight.quality || 50;
const artificialBrightness = analysis.artificialLight.mainLighting.brightness || 50;
const totalLight = naturalQuality + artificialBrightness;
this.lightingIndicators = {
naturalLightRatio: naturalQuality / totalLight,
artificialLightRatio: artificialBrightness / totalLight,
mainLightIntensity: analysis.artificialLight.mainLighting.brightness,
accentLightIntensity: analysis.artificialLight.accentLighting.intensity,
ambientColorTemp: analysis.artificialLight.ambientLighting.colorTemperature
};
}
光照信息面板:
<div class="lighting-analysis-panel">
<h4>光照分析</h4>
@if (lightingAnalysis) {
<!-- 自然光信息 -->
<div class="natural-light-section">
<h5>自然光</h5>
<div class="light-info-grid">
<div class="info-card">
<span class="label">采光方向</span>
<div class="direction-icons">
@for (dir of lightingAnalysis.naturalLight.direction; track dir) {
<span class="direction-icon">{{ dir }}</span>
}
</div>
</div>
<div class="info-card">
<span class="label">光照强度</span>
<span class="value intensity-{{ lightingAnalysis.naturalLight.intensity }}">
{{ lightingAnalysis.naturalLight.intensity }}
</span>
</div>
<div class="info-card">
<span class="label">日照时长</span>
<span class="value">{{ lightingAnalysis.naturalLight.duration }}</span>
</div>
<div class="info-card">
<span class="label">光照质量</span>
<div class="quality-bar">
<div class="bar-fill"
[style.width.%]="lightingAnalysis.naturalLight.quality">
</div>
<span class="score">{{ lightingAnalysis.naturalLight.quality }}分</span>
</div>
</div>
</div>
</div>
<!-- 人工光信息 -->
<div class="artificial-light-section">
<h5>人工光</h5>
<!-- 主照明 -->
<div class="light-type-card">
<h6>主照明</h6>
<div class="type-info">
<span class="label">类型:</span>
<span class="value">{{ lightingAnalysis.artificialLight.mainLighting.type }}</span>
</div>
<div class="type-info">
<span class="label">分布:</span>
<span class="value">{{ lightingAnalysis.artificialLight.mainLighting.distribution }}</span>
</div>
<div class="type-info">
<span class="label">亮度:</span>
<div class="brightness-bar">
<div class="bar-fill"
[style.width.%]="lightingAnalysis.artificialLight.mainLighting.brightness">
</div>
</div>
</div>
</div>
<!-- 重点照明 -->
<div class="light-type-card">
<h6>重点照明</h6>
<div class="type-info">
<span class="label">类型:</span>
<span class="value">{{ lightingAnalysis.artificialLight.accentLighting.type }}</span>
</div>
<div class="type-info">
<span class="label">位置:</span>
<div class="location-tags">
@for (loc of lightingAnalysis.artificialLight.accentLighting.locations; track loc) {
<span class="location-tag">{{ loc }}</span>
}
</div>
</div>
</div>
<!-- 环境照明 -->
<div class="light-type-card">
<h6>环境照明</h6>
<div class="type-info">
<span class="label">氛围:</span>
<span class="value mood-{{ lightingAnalysis.artificialLight.ambientLighting.mood }}">
{{ lightingAnalysis.artificialLight.ambientLighting.mood }}
</span>
</div>
<div class="type-info">
<span class="label">色温:</span>
<span class="value">{{ lightingAnalysis.artificialLight.ambientLighting.colorTemperature }}K</span>
</div>
</div>
</div>
<!-- 整体照明氛围 -->
<div class="lighting-mood-section">
<h5>照明氛围</h5>
<span class="mood-badge mood-{{ lightingAnalysis.lightingMood }}">
{{ getLightingMoodName(lightingAnalysis.lightingMood) }}
</span>
</div>
} @else {
<div class="empty-state">
上传照明场景图后将显示AI光照分析
</div>
}
</div>
// 检查四大需求是否全部完成
areAllRequirementsCompleted(): boolean {
const hasColorData = !!this.colorAnalysisResult || !!this.colorIndicators;
const hasSpaceData = !!this.spaceAnalysis || !!this.spaceIndicators;
const hasMaterialData = !!this.materialAnalysisData?.length || !!this.materialIndicators;
const hasLightingData = !!this.lightingAnalysis || !!this.lightingIndicators;
return hasColorData && hasSpaceData && hasMaterialData && hasLightingData;
}
// 汇总所有需求数据
getRequirementSummary(): RequirementSummary {
return {
colorRequirement: {
description: this.colorDescription,
referenceImages: this.referenceImages,
analysisResult: this.colorAnalysisResult,
indicators: this.colorIndicators
},
spaceRequirement: {
cadFiles: this.cadFiles,
dimensions: this.manualDimensions,
analysisResult: this.spaceAnalysis,
indicators: this.spaceIndicators
},
materialRequirement: {
materialImages: this.materialImages,
analysisResult: this.materialAnalysisData,
indicators: this.materialIndicators
},
lightingRequirement: {
lightingImages: this.lightingImages,
analysisResult: this.lightingAnalysis,
indicators: this.lightingIndicators
},
completionRate: this.calculateCompletionRate()
};
}
calculateCompletionRate(): number {
let completed = 0;
const total = 4;
if (this.colorAnalysisResult) completed++;
if (this.spaceAnalysis) completed++;
if (this.materialAnalysisData?.length) completed++;
if (this.lightingAnalysis) completed++;
return Math.round((completed / total) * 100);
}
// 当所有需求完成后触发
completeRequirementsCommunication(): void {
if (!this.areAllRequirementsCompleted()) {
alert('请完成所有需求采集项:色彩氛围、空间结构、材质权重、照明需求');
return;
}
// 1. 保存需求数据
const summary = this.getRequirementSummary();
// 2. 通知父组件推进到方案确认阶段
this.stageCompleted.emit({
stage: 'requirements-communication',
allStagesCompleted: true,
data: summary
});
// 3. 显示成功提示
alert('需求沟通完成!即将进入方案确认阶段');
}
// 基于需求数据生成初步设计方案
generateDesignProposal(): void {
if (!this.areRequiredStagesCompleted()) {
alert('请先完成需求沟通的所有采集项');
return;
}
this.isAnalyzing = true;
this.analysisProgress = 0;
// 模拟方案生成进度
const progressInterval = setInterval(() => {
this.analysisProgress += Math.random() * 15;
if (this.analysisProgress >= 100) {
this.analysisProgress = 100;
clearInterval(progressInterval);
this.completeProposalGeneration();
}
}, 500);
}
interface ProposalAnalysis {
id: string;
name: string;
version: string;
createdAt: Date;
status: 'analyzing' | 'completed' | 'approved' | 'rejected';
// 材质方案
materials: MaterialAnalysis[];
// 设计风格
designStyle: {
primaryStyle: string;
styleElements: Array<{
element: string;
description: string;
influence: number; // 影响程度 0-100
}>;
characteristics: Array<{
feature: string;
value: string;
importance: 'high' | 'medium' | 'low';
}>;
compatibility: {
withMaterials: string[];
withColors: string[];
score: number; // 兼容性评分 0-100
};
};
// 色彩方案
colorScheme: {
palette: Array<{
color: string;
hex: string;
rgb: string;
percentage: number;
role: 'dominant' | 'secondary' | 'accent' | 'neutral';
}>;
harmony: {
type: string;
temperature: 'warm' | 'cool' | 'neutral';
contrast: number;
};
psychology: {
mood: string;
atmosphere: string;
suitability: string[];
};
};
// 空间布局
spaceLayout: {
dimensions: {
length: number;
width: number;
height: number;
area: number;
volume: number;
};
functionalZones: Array<{
zone: string;
area: number;
percentage: number;
requirements: string[];
furniture: string[];
}>;
circulation: {
mainPaths: string[];
pathWidth: number;
efficiency: number;
};
lighting: {
natural: {
direction: string[];
intensity: string;
duration: string;
};
artificial: {
zones: string[];
requirements: string[];
};
};
};
// 预算方案
budget: {
total: number;
breakdown: Array<{
category: string;
amount: number;
percentage: number;
}>;
};
// 时间规划
timeline: Array<{
phase: string;
duration: number;
dependencies: string[];
}>;
// 可行性评估
feasibility: {
technical: number; // 技术可行性 0-100
budget: number; // 预算可行性 0-100
timeline: number; // 时间可行性 0-100
overall: number; // 综合可行性 0-100
};
}
// project-detail.ts lines 3237-3512
private completeProposalGeneration(): void {
this.isAnalyzing = false;
// 基于需求指标生成方案
this.proposalAnalysis = {
id: 'proposal-' + Date.now(),
name: '现代简约风格方案',
version: 'v1.0',
createdAt: new Date(),
status: 'completed',
// 材质方案:基于materialIndicators
materials: this.generateMaterialProposal(),
// 设计风格:基于整体需求
designStyle: this.generateStyleProposal(),
// 色彩方案:基于colorIndicators
colorScheme: this.generateColorSchemeProposal(),
// 空间布局:基于spaceIndicators
spaceLayout: this.generateSpaceLayoutProposal(),
// 预算方案:基于quotationData
budget: this.generateBudgetProposal(),
// 时间规划
timeline: this.generateTimelineProposal(),
// 可行性评估
feasibility: this.assessFeasibility()
};
console.log('方案生成完成:', this.proposalAnalysis);
}
<div class="proposal-overview-panel">
<h3>设计方案概览</h3>
@if (proposalAnalysis && proposalAnalysis.status === 'completed') {
<!-- 方案基本信息 -->
<div class="proposal-header">
<div class="proposal-name">{{ proposalAnalysis.name }}</div>
<div class="proposal-meta">
<span class="version">{{ proposalAnalysis.version }}</span>
<span class="created-date">{{ formatDate(proposalAnalysis.createdAt) }}</span>
</div>
</div>
<!-- 可行性评分卡片 -->
<div class="feasibility-cards">
<div class="feasibility-card">
<span class="label">技术可行性</span>
<div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.technical)">
<span class="score">{{ proposalAnalysis.feasibility.technical }}</span>
</div>
</div>
<div class="feasibility-card">
<span class="label">预算可行性</span>
<div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.budget)">
<span class="score">{{ proposalAnalysis.feasibility.budget }}</span>
</div>
</div>
<div class="feasibility-card">
<span class="label">时间可行性</span>
<div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.timeline)">
<span class="score">{{ proposalAnalysis.feasibility.timeline }}</span>
</div>
</div>
<div class="feasibility-card overall">
<span class="label">综合可行性</span>
<div class="score-circle" [class]="getScoreClass(proposalAnalysis.feasibility.overall)">
<span class="score">{{ proposalAnalysis.feasibility.overall }}</span>
</div>
</div>
</div>
<!-- 方案摘要 -->
<div class="proposal-summary">
<div class="summary-section">
<h4>材质方案</h4>
<p>{{ getMaterialCategories() }}</p>
</div>
<div class="summary-section">
<h4>设计风格</h4>
<p>{{ getStyleSummary() }}</p>
</div>
<div class="summary-section">
<h4>色彩方案</h4>
<p>{{ getColorSummary() }}</p>
</div>
<div class="summary-section">
<h4>空间效率</h4>
<p>{{ getSpaceEfficiency() }}%</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="proposal-actions">
<button class="btn-secondary" (click)="viewProposalDetails()">
查看详情
</button>
<button class="btn-primary"
(click)="confirmProposal()"
[disabled]="!canEditStage('方案确认')">
确认方案
</button>
</div>
} @else if (isAnalyzing) {
<!-- 方案生成中 -->
<div class="analyzing-state">
<div class="spinner"></div>
<p>AI正在分析需求并生成设计方案...</p>
<div class="progress-bar">
<div class="progress-fill" [style.width.%]="analysisProgress"></div>
</div>
<span class="progress-text">{{ analysisProgress.toFixed(0) }}%</span>
</div>
} @else {
<!-- 未生成方案 -->
<div class="empty-state">
<p>完成需求沟通后可生成设计方案</p>
<button class="btn-primary"
(click)="generateDesignProposal()"
[disabled]="!areRequiredStagesCompleted()">
生成设计方案
</button>
</div>
}
</div>
<div class="proposal-detail-modal" *ngIf="showProposalDetailModal">
<div class="modal-overlay" (click)="closeProposalDetailModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>设计方案详情</h3>
<button class="close-btn" (click)="closeProposalDetailModal()">×</button>
</div>
<div class="modal-body">
<!-- 材质方案详情 -->
<section class="detail-section">
<h4>材质方案</h4>
<div class="material-list">
@for (material of proposalAnalysis.materials; track material.category) {
<div class="material-detail-card">
<div class="material-name">{{ material.category }}</div>
<div class="material-specs">
<div class="spec-item">
<span class="label">类型:</span>
<span>{{ material.specifications.type }}</span>
</div>
<div class="spec-item">
<span class="label">等级:</span>
<span>{{ material.specifications.grade }}</span>
</div>
<div class="spec-item">
<span class="label">使用区域:</span>
<span>{{ material.usage.area }}</span>
</div>
<div class="spec-item">
<span class="label">占比:</span>
<span>{{ material.usage.percentage }}%</span>
</div>
</div>
</div>
}
</div>
</section>
<!-- 设计风格详情 -->
<section class="detail-section">
<h4>设计风格:{{ proposalAnalysis.designStyle.primaryStyle }}</h4>
<div class="style-elements">
@for (elem of proposalAnalysis.designStyle.styleElements; track elem.element) {
<div class="style-element">
<span class="element-name">{{ elem.element }}</span>
<p class="element-desc">{{ elem.description }}</p>
<div class="influence-bar">
<div class="bar-fill" [style.width.%]="elem.influence"></div>
<span>{{ elem.influence }}%</span>
</div>
</div>
}
</div>
</section>
<!-- 色彩方案详情 -->
<section class="detail-section">
<h4>色彩方案</h4>
<div class="color-palette">
@for (color of proposalAnalysis.colorScheme.palette; track color.hex) {
<div class="color-item">
<div class="color-swatch" [style.background-color]="color.hex"></div>
<div class="color-info">
<span class="color-name">{{ color.color }}</span>
<span class="color-hex">{{ color.hex }}</span>
<span class="color-role">{{ color.role }}</span>
<span class="color-percentage">{{ color.percentage }}%</span>
</div>
</div>
}
</div>
<div class="color-psychology">
<h5>色彩心理</h5>
<p><strong>氛围:</strong>{{ proposalAnalysis.colorScheme.psychology.atmosphere }}</p>
<p><strong>情绪:</strong>{{ proposalAnalysis.colorScheme.psychology.mood }}</p>
</div>
</section>
<!-- 空间布局详情 -->
<section class="detail-section">
<h4>空间布局</h4>
<div class="space-dimensions">
<p><strong>总面积:</strong>{{ proposalAnalysis.spaceLayout.dimensions.area }}m²</p>
<p><strong>层高:</strong>{{ proposalAnalysis.spaceLayout.dimensions.height }}m</p>
</div>
<div class="functional-zones">
<h5>功能分区</h5>
@for (zone of proposalAnalysis.spaceLayout.functionalZones; track zone.zone) {
<div class="zone-item">
<div class="zone-header">
<span class="zone-name">{{ zone.zone }}</span>
<span class="zone-area">{{ zone.area }}m² ({{ zone.percentage }}%)</span>
</div>
<div class="zone-details">
<p><strong>功能需求:</strong>{{ zone.requirements.join('、') }}</p>
<p><strong>家具配置:</strong>{{ zone.furniture.join('、') }}</p>
</div>
</div>
}
</div>
</section>
<!-- 预算方案详情 -->
<section class="detail-section">
<h4>预算方案</h4>
<div class="budget-total">
<span>总预算:</span>
<span class="amount">¥{{ proposalAnalysis.budget.total.toLocaleString() }}</span>
</div>
<div class="budget-breakdown">
@for (item of proposalAnalysis.budget.breakdown; track item.category) {
<div class="budget-item">
<div class="budget-bar">
<span class="category">{{ item.category }}</span>
<div class="bar">
<div class="bar-fill" [style.width.%]="item.percentage"></div>
</div>
<span class="amount">¥{{ item.amount.toLocaleString() }}</span>
</div>
</div>
}
</div>
</section>
<!-- 时间规划详情 -->
<section class="detail-section">
<h4>时间规划</h4>
<div class="timeline">
@for (phase of proposalAnalysis.timeline; track phase.phase) {
<div class="timeline-item">
<div class="phase-name">{{ phase.phase }}</div>
<div class="phase-duration">预计{{ phase.duration }}天</div>
@if (phase.dependencies.length > 0) {
<div class="phase-dependencies">
依赖:{{ phase.dependencies.join('、') }}
</div>
}
</div>
}
</div>
</section>
</div>
<div class="modal-footer">
<button class="btn-secondary" (click)="closeProposalDetailModal()">关闭</button>
<button class="btn-primary" (click)="confirmProposalFromDetail()">确认方案</button>
</div>
</div>
</div>
// project-detail.ts lines 2577-2585
confirmProposal(): void {
console.log('确认方案按钮被点击');
if (!this.proposalAnalysis || this.proposalAnalysis.status !== 'completed') {
alert('请先生成设计方案');
return;
}
// 标记方案为已确认
this.proposalAnalysis.status = 'approved';
// 保存方案数据到项目
this.saveProposalToProject();
// 使用统一的阶段推进方法
this.advanceToNextStage('方案确认');
console.log('已跳转到建模阶段');
}
saveProposalToProject(): void {
if (!this.proposalAnalysis) return;
const proposalData = {
projectId: this.projectId,
proposalId: this.proposalAnalysis.id,
proposal: this.proposalAnalysis,
approvedAt: new Date(),
approvedBy: this.getCurrentDesignerName()
};
this.projectService.saveProposal(proposalData).subscribe({
next: (result) => {
console.log('方案已保存:', result);
},
error: (error) => {
console.error('方案保存失败:', error);
alert('方案保存失败,请重试');
}
});
}
sequenceDiagram
participant User as 用户
participant UI as 需求沟通UI
participant Service as AnalysisService
participant AI as AI引擎
participant Parent as 项目详情页
User->>UI: 上传参考图/CAD
UI->>Service: 调用分析接口
Service->>AI: 发送分析请求
AI-->>Service: 返回分析结果
Service-->>UI: 返回结构化数据
UI->>UI: 映射到设计指标
UI->>Parent: emit requirementDataUpdated
Parent->>Parent: 更新 requirementKeyInfo
Parent->>Parent: 同步到左侧信息面板
子组件向父组件传递需求数据:
// requirements-confirm-card.component.ts
@Output() requirementDataUpdated = new EventEmitter<any>();
// 当任一需求数据更新时触发
onDataUpdated(): void {
const data = {
colorAnalysisResult: this.colorAnalysisResult,
colorIndicators: this.colorIndicators,
spaceAnalysis: this.spaceAnalysis,
spaceIndicators: this.spaceIndicators,
materialAnalysisData: this.materialAnalysisData,
materialIndicators: this.materialIndicators,
lightingAnalysis: this.lightingAnalysis,
lightingIndicators: this.lightingIndicators,
detailedAnalysis: {
enhancedColorAnalysis: this.enhancedColorAnalysis,
formAnalysis: this.formAnalysis,
textureAnalysis: this.textureAnalysis,
patternAnalysis: this.patternAnalysis,
lightingAnalysis: this.lightingAnalysis
},
materials: [
...this.referenceImages.map(img => ({ ...img, type: 'image' })),
...this.cadFiles.map(file => ({ ...file, type: 'cad' }))
]
};
this.requirementDataUpdated.emit(data);
}
父组件接收并处理:
// project-detail.ts lines 3071-3187
onRequirementDataUpdated(data: any): void {
console.log('收到需求数据更新:', data);
// 1. 同步关键信息到左侧面板
this.syncRequirementKeyInfo(data);
// 2. 更新项目信息显示
this.updateProjectInfoFromRequirementData(data);
}
private syncRequirementKeyInfo(requirementData: any): void {
if (requirementData) {
// 同步色彩氛围信息
if (requirementData.colorIndicators) {
this.requirementKeyInfo.colorAtmosphere = {
description: requirementData.colorIndicators.colorRange || '',
mainColor: `rgb(${requirementData.colorIndicators.mainColor?.r || 0}, ...)`,
colorTemp: `${requirementData.colorIndicators.colorTemperature || 0}K`,
materials: []
};
}
// 同步空间结构信息
if (requirementData.spaceIndicators) {
this.requirementKeyInfo.spaceStructure = {
lineRatio: requirementData.spaceIndicators.lineRatio || 0,
blankRatio: requirementData.spaceIndicators.blankRatio || 0,
flowWidth: requirementData.spaceIndicators.flowWidth || 0,
aspectRatio: requirementData.spaceIndicators.aspectRatio || 0,
ceilingHeight: requirementData.spaceIndicators.ceilingHeight || 0
};
}
// 同步材质权重信息
if (requirementData.materialIndicators) {
this.requirementKeyInfo.materialWeights = {
fabricRatio: requirementData.materialIndicators.fabricRatio || 0,
woodRatio: requirementData.materialIndicators.woodRatio || 0,
metalRatio: requirementData.materialIndicators.metalRatio || 0,
smoothness: requirementData.materialIndicators.smoothness || 0,
glossiness: requirementData.materialIndicators.glossiness || 0
};
}
// 处理详细分析数据
if (requirementData.detailedAnalysis) {
this.enhancedColorAnalysis = requirementData.detailedAnalysis.enhancedColorAnalysis;
this.formAnalysis = requirementData.detailedAnalysis.formAnalysis;
this.textureAnalysis = requirementData.detailedAnalysis.textureAnalysis;
this.patternAnalysis = requirementData.detailedAnalysis.patternAnalysis;
this.lightingAnalysis = requirementData.detailedAnalysis.lightingAnalysis;
}
// 拆分参考图片和CAD文件
const materials = Array.isArray(requirementData?.materials) ? requirementData.materials : [];
this.referenceImages = materials.filter((m: any) => m?.type === 'image');
this.cadFiles = materials.filter((m: any) => m?.type === 'cad');
// 触发变更检测
this.cdr.detectChanges();
}
}
需求关键信息展示(project-detail.html lines 350-450):
<div class="requirement-key-info-panel">
<h4>需求关键信息</h4>
<!-- 色彩氛围 -->
<div class="key-info-section">
<h5>色彩氛围</h5>
@if (requirementKeyInfo.colorAtmosphere.description) {
<p class="info-value">{{ requirementKeyInfo.colorAtmosphere.description }}</p>
<div class="color-preview">
<div class="color-swatch" [style.background-color]="requirementKeyInfo.colorAtmosphere.mainColor"></div>
<span>主色 {{ requirementKeyInfo.colorAtmosphere.colorTemp }}</span>
</div>
} @else {
<p class="empty-hint">待采集</p>
}
</div>
<!-- 空间结构 -->
<div class="key-info-section">
<h5>空间结构</h5>
@if (requirementKeyInfo.spaceStructure.aspectRatio > 0) {
<div class="info-grid">
<div class="info-item">
<span class="label">空间比例:</span>
<span class="value">{{ requirementKeyInfo.spaceStructure.aspectRatio.toFixed(1) }}</span>
</div>
<div class="info-item">
<span class="label">层高:</span>
<span class="value">{{ requirementKeyInfo.spaceStructure.ceilingHeight }}m</span>
</div>
<div class="info-item">
<span class="label">线条占比:</span>
<span class="value">{{ (requirementKeyInfo.spaceStructure.lineRatio * 100).toFixed(0) }}%</span>
</div>
<div class="info-item">
<span class="label">留白占比:</span>
<span class="value">{{ (requirementKeyInfo.spaceStructure.blankRatio * 100).toFixed(0) }}%</span>
</div>
</div>
} @else {
<p class="empty-hint">待采集</p>
}
</div>
<!-- 材质权重 -->
<div class="key-info-section">
<h5>材质权重</h5>
@if (requirementKeyInfo.materialWeights.woodRatio > 0 ||
requirementKeyInfo.materialWeights.fabricRatio > 0 ||
requirementKeyInfo.materialWeights.metalRatio > 0) {
<div class="material-bars">
<div class="material-bar">
<span class="material-label">木质</span>
<div class="bar">
<div class="bar-fill wood" [style.width.%]="requirementKeyInfo.materialWeights.woodRatio"></div>
</div>
<span class="percentage">{{ requirementKeyInfo.materialWeights.woodRatio }}%</span>
</div>
<div class="material-bar">
<span class="material-label">布艺</span>
<div class="bar">
<div class="bar-fill fabric" [style.width.%]="requirementKeyInfo.materialWeights.fabricRatio"></div>
</div>
<span class="percentage">{{ requirementKeyInfo.materialWeights.fabricRatio }}%</span>
</div>
<div class="material-bar">
<span class="material-label">金属</span>
<div class="bar">
<div class="bar-fill metal" [style.width.%]="requirementKeyInfo.materialWeights.metalRatio"></div>
</div>
<span class="percentage">{{ requirementKeyInfo.materialWeights.metalRatio }}%</span>
</div>
</div>
} @else {
<p class="empty-hint">待采集</p>
}
</div>
<!-- 预设氛围 -->
<div class="key-info-section">
<h5>预设氛围</h5>
@if (requirementKeyInfo.presetAtmosphere.name) {
<p class="info-value">{{ requirementKeyInfo.presetAtmosphere.name }}</p>
<div class="atmosphere-details">
<span>色温:{{ requirementKeyInfo.presetAtmosphere.colorTemp }}</span>
<span>主材:{{ requirementKeyInfo.presetAtmosphere.materials.join('、') }}</span>
</div>
} @else {
<p class="empty-hint">待采集</p>
}
</div>
</div>
操作 | 客服 | 设计师 | 组长 | 技术 |
---|---|---|---|---|
查看需求沟通 | ✅ | ✅ | ✅ | ✅ |
上传参考图 | ✅ | ✅ | ✅ | ❌ |
上传CAD文件 | ✅ | ✅ | ✅ | ❌ |
触发AI分析 | ✅ | ✅ | ✅ | ❌ |
手动编辑指标 | ❌ | ✅ | ✅ | ❌ |
生成设计方案 | ❌ | ✅ | ✅ | ❌ |
确认方案 | ❌ | ✅ | ✅ | ❌ |
推进到建模阶段 | ❌ | ✅ | ✅ | ❌ |
组件级别:
<!-- 需求沟通卡片只读模式 -->
<app-requirements-confirm-card
[readonly]="!canEditStage('需求沟通')"
...>
</app-requirements-confirm-card>
<!-- 方案确认按钮权限 -->
<button class="btn-primary"
(click)="confirmProposal()"
[disabled]="!canEditStage('方案确认') || !proposalAnalysis">
确认方案
</button>
操作级别:
generateDesignProposal(): void {
// 检查权限
if (!this.canEditStage('方案确认')) {
alert('您没有权限生成设计方案');
return;
}
// 检查前置条件
if (!this.areRequiredStagesCompleted()) {
alert('请先完成需求沟通的所有采集项');
return;
}
// 执行方案生成
this.isAnalyzing = true;
// ...
}
uploadFiles(files: File[]): Observable<any[]> {
const formData = new FormData();
files.forEach(file => formData.append('files', file));
return this.http.post<any>('/api/upload', formData).pipe(
catchError(error => {
let errorMessage = '文件上传失败';
if (error.status === 413) {
errorMessage = '文件过大,请上传小于10MB的文件';
} else if (error.status === 415) {
errorMessage = '文件格式不支持';
} else if (error.status === 500) {
errorMessage = '服务器错误,请稍后重试';
}
return throwError(() => new Error(errorMessage));
})
);
}
triggerColorAnalysis(imageUrl: string): void {
this.isAnalyzingColors = true;
this.colorAnalysisService.analyzeImage(imageUrl).pipe(
retry(2), // 失败后重试2次
timeout(30000), // 30秒超时
catchError(error => {
this.isAnalyzingColors = false;
let errorMessage = 'AI色彩分析失败';
if (error.name === 'TimeoutError') {
errorMessage = '分析超时,请稍后重试';
} else if (error.status === 400) {
errorMessage = '图片格式不符合要求';
}
alert(errorMessage);
return of(null);
})
).subscribe({
next: (result) => {
if (result) {
this.colorAnalysisResult = result;
// ...
}
}
});
}
generateDesignProposal(): void {
this.isAnalyzing = true;
this.analysisProgress = 0;
// 设置超时保护
const timeout = setTimeout(() => {
if (this.isAnalyzing) {
this.isAnalyzing = false;
alert('方案生成超时,请重试');
}
}, 60000); // 60秒超时
// 模拟生成进度
const progressInterval = setInterval(() => {
this.analysisProgress += Math.random() * 15;
if (this.analysisProgress >= 100) {
this.analysisProgress = 100;
clearInterval(progressInterval);
clearTimeout(timeout);
try {
this.completeProposalGeneration();
} catch (error) {
console.error('方案生成失败:', error);
alert('方案生成失败,请重试');
this.isAnalyzing = false;
}
}
}, 500);
}
// 使用Intersection Observer实现图片懒加载
@ViewChild('imageContainer') imageContainer?: ElementRef;
ngAfterViewInit(): void {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
const src = img.dataset['src'];
if (src) {
img.src = src;
observer.unobserve(img);
}
}
});
});
const images = this.imageContainer?.nativeElement.querySelectorAll('img[data-src]');
images?.forEach((img: HTMLImageElement) => observer.observe(img));
}
}
// ColorAnalysisService with caching
private analysisCache = new Map<string, ColorAnalysisResult>();
analyzeImage(imageUrl: string): Observable<ColorAnalysisResult> {
// 检查缓存
const cached = this.analysisCache.get(imageUrl);
if (cached) {
console.log('使用缓存的分析结果');
return of(cached);
}
// 调用API分析
return this.http.post<ColorAnalysisResult>('/api/analyze/color', { imageUrl }).pipe(
tap(result => {
// 缓存结果(限制缓存大小)
if (this.analysisCache.size >= 50) {
const firstKey = this.analysisCache.keys().next().value;
this.analysisCache.delete(firstKey);
}
this.analysisCache.set(imageUrl, result);
})
);
}
uploadLargeFile(file: File): Observable<UploadProgress> {
const chunkSize = 1024 * 1024; // 1MB per chunk
const chunks = Math.ceil(file.size / chunkSize);
const uploadProgress$ = new Subject<UploadProgress>();
let uploadedChunks = 0;
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i.toString());
formData.append('totalChunks', chunks.toString());
formData.append('fileName', file.name);
this.http.post('/api/upload/chunk', formData).subscribe({
next: () => {
uploadedChunks++;
uploadProgress$.next({
progress: (uploadedChunks / chunks) * 100,
status: 'uploading'
});
if (uploadedChunks === chunks) {
uploadProgress$.next({ progress: 100, status: 'completed' });
uploadProgress$.complete();
}
},
error: (error) => {
uploadProgress$.error(error);
}
});
}
return uploadProgress$.asObservable();
}
describe('Requirements Collection Flow', () => {
it('should complete all four requirement types', async () => {
// 1. 上传色彩参考图
await uploadFile('color-reference.jpg', 'colorInput');
expect(component.colorAnalysisResult).toBeTruthy();
// 2. 上传CAD文件
await uploadFile('floor-plan.dwg', 'cadInput');
expect(component.spaceAnalysis).toBeTruthy();
// 3. 上传材质图片
await uploadFile('material-ref.jpg', 'materialInput');
expect(component.materialAnalysisData.length).toBeGreaterThan(0);
// 4. 上传照明场景图
await uploadFile('lighting-scene.jpg', 'lightingInput');
expect(component.lightingAnalysis).toBeTruthy();
// 5. 验证完成度
expect(component.areAllRequirementsCompleted()).toBeTruthy();
});
});
describe('Proposal Generation', () => {
it('should generate design proposal based on requirements', async () => {
// 准备需求数据
component.colorIndicators = mockColorIndicators;
component.spaceIndicators = mockSpaceIndicators;
component.materialIndicators = mockMaterialIndicators;
component.lightingIndicators = mockLightingIndicators;
// 触发方案生成
component.generateDesignProposal();
// 等待生成完成
await waitForCondition(() => component.proposalAnalysis !== null);
// 验证方案数据
expect(component.proposalAnalysis.status).toBe('completed');
expect(component.proposalAnalysis.materials.length).toBeGreaterThan(0);
expect(component.proposalAnalysis.designStyle).toBeTruthy();
expect(component.proposalAnalysis.colorScheme).toBeTruthy();
expect(component.proposalAnalysis.spaceLayout).toBeTruthy();
expect(component.proposalAnalysis.feasibility.overall).toBeGreaterThan(70);
});
});
describe('Stage Progression', () => {
it('should advance from requirements to proposal to modeling', async () => {
// 1. 完成需求沟通
component.completeRequirementsCommunication();
expect(component.currentStage).toBe('方案确认');
// 2. 生成并确认方案
component.generateDesignProposal();
await waitForCondition(() => component.proposalAnalysis !== null);
component.confirmProposal();
expect(component.currentStage).toBe('建模');
expect(component.expandedStages['建模']).toBeTruthy();
});
});
文档版本:v1.0.0 创建日期:2025-10-16 最后更新:2025-10-16 维护人:产品团队