项目-需求确认.md 62 KB

项目管理 - 需求确认阶段 PRD

1. 功能概述

1.1 阶段定位

需求确认阶段包含"需求沟通"和"方案确认"两个子环节,是连接订单分配与交付执行的关键桥梁。该阶段通过AI辅助分析工具深入理解客户需求,并将抽象需求转化为可执行的设计方案。

1.2 核心目标

  • 需求沟通环节:深度挖掘客户的色彩、空间、材质、照明等多维度需求
  • 方案确认环节:基于需求分析生成初步设计方案,并获得客户确认
  • 建立需求与设计方案之间的映射关系
  • 为后续建模、软装、渲染阶段提供标准化输入

1.3 涉及角色

  • 客服人员:协助收集客户需求材料、沟通确认需求细节
  • 设计师:主导需求分析、方案设计、与客户沟通确认
  • 组长:审核方案可行性、协调资源、把控质量

1.4 阶段划分

graph LR
    A[订单分配] --> B[需求沟通]
    B --> C[方案确认]
    C --> D[建模]

    style B fill:#e3f2fd
    style C fill:#e8f5e9

2. 需求沟通环节

2.1 需求沟通卡片组件

2.1.1 组件集成

组件标签

<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 数据结构

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 参考图上传与分析流程

上传触发

// 用户点击"上传参考图"按钮
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; // 中性
}

2.2.3 色彩分析可视化

右侧面板展示(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>

2.3 空间结构需求采集

2.3.1 数据结构

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文件上传与解析

文件上传

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
  };
}

2.3.3 空间结构可视化

空间分区图表

<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>

2.4 材质权重需求采集

2.4.1 数据结构

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 材质识别流程

图片上传触发识别

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;
}

2.4.3 材质分析可视化

材质卡片网格

<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>

2.5 照明需求采集

2.5.1 数据结构

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 光照分析流程

图片上传触发分析

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
  };
}

2.5.3 光照分析可视化

光照信息面板

<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 需求完成度检查

// 检查四大需求是否全部完成
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 需求数据汇总

// 汇总所有需求数据
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 需求沟通完成触发

// 当所有需求完成后触发
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方案生成触发

// 基于需求数据生成初步设计方案
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 方案数据结构

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 方案生成实现(简化示例)

// 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 方案概览面板

<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 方案详情弹窗

<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 确认操作

// 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 方案数据持久化

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 需求数据流转图

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 父子组件数据同步

子组件向父组件传递需求数据

// 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();
  }
}

4.3 左侧信息面板实时更新

需求关键信息展示(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>

5. 权限控制

5.1 需求确认阶段权限矩阵

操作 客服 设计师 组长 技术
查看需求沟通
上传参考图
上传CAD文件
触发AI分析
手动编辑指标
生成设计方案
确认方案
推进到建模阶段

5.2 权限控制实现

组件级别

<!-- 需求沟通卡片只读模式 -->
<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;
  // ...
}

6. 异常处理

6.1 文件上传失败

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分析失败

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 方案生成失败

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 图片懒加载

// 使用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 分析结果缓存

// 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 大文件分片上传

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 需求采集流程测试

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 方案生成测试

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 阶段推进测试

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 维护人:产品团队