|
|
@@ -0,0 +1,2080 @@
|
|
|
+# 项目管理 - 需求确认阶段 PRD
|
|
|
+
|
|
|
+## 1. 功能概述
|
|
|
+
|
|
|
+### 1.1 阶段定位
|
|
|
+需求确认阶段包含"需求沟通"和"方案确认"两个子环节,是连接订单分配与交付执行的关键桥梁。该阶段通过AI辅助分析工具深入理解客户需求,并将抽象需求转化为可执行的设计方案。
|
|
|
+
|
|
|
+### 1.2 核心目标
|
|
|
+- **需求沟通环节**:深度挖掘客户的色彩、空间、材质、照明等多维度需求
|
|
|
+- **方案确认环节**:基于需求分析生成初步设计方案,并获得客户确认
|
|
|
+- 建立需求与设计方案之间的映射关系
|
|
|
+- 为后续建模、软装、渲染阶段提供标准化输入
|
|
|
+
|
|
|
+### 1.3 涉及角色
|
|
|
+- **客服人员**:协助收集客户需求材料、沟通确认需求细节
|
|
|
+- **设计师**:主导需求分析、方案设计、与客户沟通确认
|
|
|
+- **组长**:审核方案可行性、协调资源、把控质量
|
|
|
+
|
|
|
+### 1.4 阶段划分
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph LR
|
|
|
+ A[订单分配] --> B[需求沟通]
|
|
|
+ B --> C[方案确认]
|
|
|
+ C --> D[建模]
|
|
|
+
|
|
|
+ style B fill:#e3f2fd
|
|
|
+ style C fill:#e8f5e9
|
|
|
+```
|
|
|
+
|
|
|
+## 2. 需求沟通环节
|
|
|
+
|
|
|
+### 2.1 需求沟通卡片组件
|
|
|
+
|
|
|
+#### 2.1.1 组件集成
|
|
|
+**组件标签**:
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.1.2 核心功能模块
|
|
|
+
|
|
|
+**四大需求采集流程**:
|
|
|
+1. **色彩氛围需求** → AI色彩分析
|
|
|
+2. **空间结构需求** → AI空间布局分析
|
|
|
+3. **材质权重需求** → AI材质识别分析
|
|
|
+4. **照明需求** → AI光照场景分析
|
|
|
+
|
|
|
+### 2.2 色彩氛围需求采集
|
|
|
+
|
|
|
+#### 2.2.1 数据结构
|
|
|
+```typescript
|
|
|
+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)
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.2.2 参考图上传与分析流程
|
|
|
+
|
|
|
+**上传触发**:
|
|
|
+```typescript
|
|
|
+// 用户点击"上传参考图"按钮
|
|
|
+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色彩分析**:
|
|
|
+```typescript
|
|
|
+// 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;
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**色彩结果映射**:
|
|
|
+```typescript
|
|
|
+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; // 中性
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.2.3 色彩分析可视化
|
|
|
+
|
|
|
+**右侧面板展示**(project-detail.html lines 1826-1900):
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+**色彩轮盘可视化组件**:
|
|
|
+```html
|
|
|
+<app-color-wheel-visualizer
|
|
|
+ [colors]="colorAnalysisResult?.colors"
|
|
|
+ [dominantColor]="dominantColorHex"
|
|
|
+ [showHarmony]="true">
|
|
|
+</app-color-wheel-visualizer>
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 空间结构需求采集
|
|
|
+
|
|
|
+#### 2.3.1 数据结构
|
|
|
+```typescript
|
|
|
+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; // 层高
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.3.2 CAD文件上传与解析
|
|
|
+
|
|
|
+**文件上传**:
|
|
|
+```typescript
|
|
|
+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文件解析失败,请检查文件格式');
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**空间指标映射**:
|
|
|
+```typescript
|
|
|
+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
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.3.3 空间结构可视化
|
|
|
+
|
|
|
+**空间分区图表**:
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+**动线效率雷达图**:
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+### 2.4 材质权重需求采集
|
|
|
+
|
|
|
+#### 2.4.1 数据结构
|
|
|
+```typescript
|
|
|
+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
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.4.2 材质识别流程
|
|
|
+
|
|
|
+**图片上传触发识别**:
|
|
|
+```typescript
|
|
|
+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;
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**材质权重计算**:
|
|
|
+```typescript
|
|
|
+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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.4.3 材质分析可视化
|
|
|
+
|
|
|
+**材质卡片网格**:
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+**纹理对比可视化组件**:
|
|
|
+```html
|
|
|
+<app-texture-comparison-visualizer
|
|
|
+ [materials]="materialAnalysisData"
|
|
|
+ [showProperties]="true">
|
|
|
+</app-texture-comparison-visualizer>
|
|
|
+```
|
|
|
+
|
|
|
+### 2.5 照明需求采集
|
|
|
+
|
|
|
+#### 2.5.1 数据结构
|
|
|
+```typescript
|
|
|
+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)
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.5.2 光照分析流程
|
|
|
+
|
|
|
+**图片上传触发分析**:
|
|
|
+```typescript
|
|
|
+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;
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**光照指标映射**:
|
|
|
+```typescript
|
|
|
+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
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.5.3 光照分析可视化
|
|
|
+
|
|
|
+**光照信息面板**:
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+### 2.6 需求映射总览
|
|
|
+
|
|
|
+#### 2.6.1 需求完成度检查
|
|
|
+```typescript
|
|
|
+// 检查四大需求是否全部完成
|
|
|
+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;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.6.2 需求数据汇总
|
|
|
+```typescript
|
|
|
+// 汇总所有需求数据
|
|
|
+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);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2.6.3 需求沟通完成触发
|
|
|
+```typescript
|
|
|
+// 当所有需求完成后触发
|
|
|
+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('需求沟通完成!即将进入方案确认阶段');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 3. 方案确认环节
|
|
|
+
|
|
|
+### 3.1 方案生成逻辑
|
|
|
+
|
|
|
+#### 3.1.1 AI方案生成触发
|
|
|
+```typescript
|
|
|
+// 基于需求数据生成初步设计方案
|
|
|
+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);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.1.2 方案数据结构
|
|
|
+```typescript
|
|
|
+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
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.1.3 方案生成实现(简化示例)
|
|
|
+```typescript
|
|
|
+// 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);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 方案展示与确认
|
|
|
+
|
|
|
+#### 3.2.1 方案概览面板
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.2.2 方案详情弹窗
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 方案确认流程
|
|
|
+
|
|
|
+#### 3.3.1 确认操作
|
|
|
+```typescript
|
|
|
+// 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('已跳转到建模阶段');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.3.2 方案数据持久化
|
|
|
+```typescript
|
|
|
+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('方案保存失败,请重试');
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 4. 数据流转与同步
|
|
|
+
|
|
|
+### 4.1 需求数据流转图
|
|
|
+
|
|
|
+```mermaid
|
|
|
+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: 同步到左侧信息面板
|
|
|
+```
|
|
|
+
|
|
|
+### 4.2 父子组件数据同步
|
|
|
+
|
|
|
+**子组件向父组件传递需求数据**:
|
|
|
+```typescript
|
|
|
+// 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);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**父组件接收并处理**:
|
|
|
+```typescript
|
|
|
+// 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();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 左侧信息面板实时更新
|
|
|
+
|
|
|
+**需求关键信息展示**(project-detail.html lines 350-450):
|
|
|
+```html
|
|
|
+<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>
|
|
|
+```
|
|
|
+
|
|
|
+## 5. 权限控制
|
|
|
+
|
|
|
+### 5.1 需求确认阶段权限矩阵
|
|
|
+
|
|
|
+| 操作 | 客服 | 设计师 | 组长 | 技术 |
|
|
|
+|-----|------|--------|------|------|
|
|
|
+| 查看需求沟通 | ✅ | ✅ | ✅ | ✅ |
|
|
|
+| 上传参考图 | ✅ | ✅ | ✅ | ❌ |
|
|
|
+| 上传CAD文件 | ✅ | ✅ | ✅ | ❌ |
|
|
|
+| 触发AI分析 | ✅ | ✅ | ✅ | ❌ |
|
|
|
+| 手动编辑指标 | ❌ | ✅ | ✅ | ❌ |
|
|
|
+| 生成设计方案 | ❌ | ✅ | ✅ | ❌ |
|
|
|
+| 确认方案 | ❌ | ✅ | ✅ | ❌ |
|
|
|
+| 推进到建模阶段 | ❌ | ✅ | ✅ | ❌ |
|
|
|
+
|
|
|
+### 5.2 权限控制实现
|
|
|
+
|
|
|
+**组件级别**:
|
|
|
+```html
|
|
|
+<!-- 需求沟通卡片只读模式 -->
|
|
|
+<app-requirements-confirm-card
|
|
|
+ [readonly]="!canEditStage('需求沟通')"
|
|
|
+ ...>
|
|
|
+</app-requirements-confirm-card>
|
|
|
+
|
|
|
+<!-- 方案确认按钮权限 -->
|
|
|
+<button class="btn-primary"
|
|
|
+ (click)="confirmProposal()"
|
|
|
+ [disabled]="!canEditStage('方案确认') || !proposalAnalysis">
|
|
|
+ 确认方案
|
|
|
+</button>
|
|
|
+```
|
|
|
+
|
|
|
+**操作级别**:
|
|
|
+```typescript
|
|
|
+generateDesignProposal(): void {
|
|
|
+ // 检查权限
|
|
|
+ if (!this.canEditStage('方案确认')) {
|
|
|
+ alert('您没有权限生成设计方案');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查前置条件
|
|
|
+ if (!this.areRequiredStagesCompleted()) {
|
|
|
+ alert('请先完成需求沟通的所有采集项');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行方案生成
|
|
|
+ this.isAnalyzing = true;
|
|
|
+ // ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 6. 异常处理
|
|
|
+
|
|
|
+### 6.1 文件上传失败
|
|
|
+```typescript
|
|
|
+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));
|
|
|
+ })
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 AI分析失败
|
|
|
+```typescript
|
|
|
+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;
|
|
|
+ // ...
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 方案生成失败
|
|
|
+```typescript
|
|
|
+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);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 7. 性能优化
|
|
|
+
|
|
|
+### 7.1 图片懒加载
|
|
|
+```typescript
|
|
|
+// 使用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));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 分析结果缓存
|
|
|
+```typescript
|
|
|
+// 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);
|
|
|
+ })
|
|
|
+ );
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 7.3 大文件分片上传
|
|
|
+```typescript
|
|
|
+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();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 8. 测试用例
|
|
|
+
|
|
|
+### 8.1 需求采集流程测试
|
|
|
+```typescript
|
|
|
+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();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 方案生成测试
|
|
|
+```typescript
|
|
|
+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);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 阶段推进测试
|
|
|
+```typescript
|
|
|
+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
|
|
|
+**维护人**:产品团队
|