Browse Source

fix: stage-order quotation

ryanemax 1 month ago
parent
commit
f06345befd

+ 1675 - 0
docs/prd/项目-交付执行.md

@@ -0,0 +1,1675 @@
+# 项目管理 - 交付执行阶段 PRD
+
+## 1. 功能概述
+
+### 1.1 阶段定位
+交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
+
+### 1.2 核心目标
+- 按空间维度组织文件上传和进度管理
+- 实现四个执行阶段的串行推进
+- 提供实时进度跟踪和状态可视化
+- 支持组长审核和质量把控
+- 确保交付物符合质量标准
+
+### 1.3 涉及角色
+- **设计师**:负责建模、软装、后期等设计工作
+- **渲染师**:负责渲染阶段的大图输出
+- **组长**:审核各阶段交付物、把控质量
+- **技术**:验收最终交付物、确认质量
+
+### 1.4 四大执行子阶段
+
+```mermaid
+graph LR
+    A[方案确认] --> B[建模]
+    B --> C[软装]
+    C --> D[渲染]
+    D --> E[后期]
+    E --> F[尾款结算]
+
+    style B fill:#e3f2fd
+    style C fill:#fff3e0
+    style D fill:#fce4ec
+    style E fill:#f3e5f5
+```
+
+## 2. 空间管理系统
+
+### 2.1 空间数据结构
+
+#### 2.1.1 DeliveryProcess 接口
+```typescript
+interface DeliveryProcess {
+  id: string;                           // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
+  name: string;                         // 流程名称:建模/软装/渲染/后期
+  type: 'modeling' | 'softDecor' | 'rendering' | 'postProcess';
+  isExpanded: boolean;                  // 是否展开
+  spaces: DeliverySpace[];              // 空间列表
+  content: {
+    [spaceId: string]: SpaceContent;    // 按空间ID索引的内容
+  };
+}
+
+interface DeliverySpace {
+  id: string;                           // 空间ID
+  name: string;                         // 空间名称:卧室/客厅/厨房等
+  isExpanded: boolean;                  // 是否展开
+  order: number;                        // 排序顺序
+}
+
+interface SpaceContent {
+  images: Array<{
+    id: string;
+    name: string;
+    url: string;
+    size?: string;
+    reviewStatus?: 'pending' | 'approved' | 'rejected';
+    synced?: boolean;                   // 是否已同步到客户端
+  }>;
+  progress: number;                     // 进度 0-100
+  status: 'pending' | 'in_progress' | 'completed' | 'approved';
+  notes: string;                        // 备注信息
+  lastUpdated: Date;                    // 最后更新时间
+}
+```
+
+#### 2.1.2 初始空间配置
+```typescript
+// project-detail.ts lines 458-523
+deliveryProcesses: DeliveryProcess[] = [
+  {
+    id: 'modeling',
+    name: '建模',
+    type: 'modeling',
+    isExpanded: true,                   // 默认展开第一个
+    spaces: [
+      { id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
+      { id: 'living', name: '客厅', isExpanded: false, order: 2 },
+      { id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
+    ],
+    content: {
+      'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
+      'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
+      'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
+    }
+  },
+  // 软装、渲染、后期流程结构相同
+];
+```
+
+### 2.2 空间管理功能
+
+#### 2.2.1 添加新空间
+```typescript
+// project-detail.ts lines 5150-5184
+addSpace(processId: string): void {
+  const spaceName = this.newSpaceName[processId]?.trim();
+  if (!spaceName) return;
+
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process) return;
+
+  // 生成新的空间ID
+  const spaceId = `space_${Date.now()}`;
+
+  // 添加到spaces数组
+  const newSpace: DeliverySpace = {
+    id: spaceId,
+    name: spaceName,
+    isExpanded: false,
+    order: process.spaces.length + 1
+  };
+
+  process.spaces.push(newSpace);
+
+  // 初始化content数据
+  process.content[spaceId] = {
+    images: [],
+    progress: 0,
+    status: 'pending',
+    notes: '',
+    lastUpdated: new Date()
+  };
+
+  // 清空输入框并隐藏
+  this.newSpaceName[processId] = '';
+  this.showAddSpaceInput[processId] = false;
+
+  console.log(`已添加空间: ${spaceName} 到流程 ${process.name}`);
+}
+```
+
+**UI交互**:
+```html
+<!-- 添加空间输入框 -->
+@if (showAddSpaceInput[process.id]) {
+  <div class="add-space-input">
+    <input
+      type="text"
+      [(ngModel)]="newSpaceName[process.id]"
+      placeholder="输入空间名称(如:次卧、书房)"
+      (keydown.enter)="addSpace(process.id)"
+      (keydown.escape)="cancelAddSpace(process.id)">
+    <button class="btn-primary" (click)="addSpace(process.id)">确定</button>
+    <button class="btn-secondary" (click)="cancelAddSpace(process.id)">取消</button>
+  </div>
+} @else {
+  <button class="btn-add-space" (click)="showAddSpaceInput[process.id] = true">
+    + 添加空间
+  </button>
+}
+```
+
+#### 2.2.2 删除空间
+```typescript
+// project-detail.ts lines 5219-5242
+removeSpace(processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process) return;
+
+  // 从spaces数组中移除
+  const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
+  if (spaceIndex > -1) {
+    const spaceName = process.spaces[spaceIndex].name;
+    process.spaces.splice(spaceIndex, 1);
+
+    // 清理content数据
+    if (process.content[spaceId]) {
+      // 释放图片URL资源
+      process.content[spaceId].images.forEach(img => {
+        if (img.url && img.url.startsWith('blob:')) {
+          URL.revokeObjectURL(img.url);
+        }
+      });
+      delete process.content[spaceId];
+    }
+
+    console.log(`已删除空间: ${spaceName} 从流程 ${process.name}`);
+  }
+}
+```
+
+#### 2.2.3 空间展开/收起
+```typescript
+// project-detail.ts lines 5200-5208
+toggleSpace(processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process) return;
+
+  const space = process.spaces.find(s => s.id === spaceId);
+  if (space) {
+    space.isExpanded = !space.isExpanded;
+  }
+}
+```
+
+### 2.3 进度管理
+
+#### 2.3.1 进度计算逻辑
+```typescript
+// project-detail.ts lines 5377-5397
+private updateSpaceProgress(processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  const content = process.content[spaceId];
+  const imageCount = content.images.length;
+
+  // 根据图片数量和状态计算进度
+  if (imageCount === 0) {
+    content.progress = 0;
+    content.status = 'pending';
+  } else if (imageCount < 3) {
+    content.progress = Math.min(imageCount * 30, 90);
+    content.status = 'in_progress';
+  } else {
+    content.progress = 100;
+    content.status = 'completed';
+  }
+
+  content.lastUpdated = new Date();
+}
+```
+
+**进度规则**:
+- 0张图片:0%进度,状态为待开始
+- 1-2张图片:30%-60%进度,状态为进行中
+- 3张及以上:100%进度,状态为已完成
+
+#### 2.3.2 获取空间进度
+```typescript
+// project-detail.ts lines 5211-5216
+getSpaceProgress(processId: string, spaceId: string): number {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return 0;
+
+  return process.content[spaceId].progress || 0;
+}
+```
+
+#### 2.3.3 进度可视化
+```html
+<div class="space-progress-bar">
+  <div class="progress-fill"
+       [style.width.%]="getSpaceProgress(process.id, space.id)"
+       [class.pending]="getSpaceProgress(process.id, space.id) === 0"
+       [class.in-progress]="getSpaceProgress(process.id, space.id) > 0 && getSpaceProgress(process.id, space.id) < 100"
+       [class.completed]="getSpaceProgress(process.id, space.id) === 100">
+  </div>
+  <span class="progress-text">{{ getSpaceProgress(process.id, space.id) }}%</span>
+</div>
+```
+
+## 3. 建模阶段
+
+### 3.1 功能特点
+- 白模图片上传
+- 模型检查项验证
+- 户型匹配度检查
+- 尺寸精度验证
+
+### 3.2 白模上传
+
+#### 3.2.1 文件上传处理
+```typescript
+// project-detail.ts lines 1838-1850
+onWhiteModelSelected(event: Event): void {
+  const input = event.target as HTMLInputElement;
+  if (!input.files || input.files.length === 0) return;
+  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
+  const items = files.map(f => this.makeImageItem(f));
+  this.whiteModelImages.unshift(...items);
+  input.value = '';
+}
+
+removeWhiteModelImage(id: string): void {
+  const target = this.whiteModelImages.find(i => i.id === id);
+  if (target) this.revokeUrl(target.url);
+  this.whiteModelImages = this.whiteModelImages.filter(i => i.id !== id);
+}
+```
+
+#### 3.2.2 图片对象生成
+```typescript
+// project-detail.ts lines 1826-1830
+private makeImageItem(file: File): { id: string; name: string; url: string; size: string } {
+  const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+  const url = URL.createObjectURL(file);
+  return { id, name: file.name, url, size: this.formatFileSize(file.size) };
+}
+```
+
+#### 3.2.3 文件大小格式化
+```typescript
+// project-detail.ts lines 1815-1823
+private formatFileSize(bytes: number): string {
+  if (bytes < 1024) return `${bytes}B`;
+  const kb = bytes / 1024;
+  if (kb < 1024) return `${kb.toFixed(1)}KB`;
+  const mb = kb / 1024;
+  if (mb < 1024) return `${mb.toFixed(1)}MB`;
+  const gb = mb / 1024;
+  return `${gb.toFixed(2)}GB`;
+}
+```
+
+### 3.3 模型检查项
+
+#### 3.3.1 检查项数据结构
+```typescript
+interface ModelCheckItem {
+  id: string;
+  name: string;
+  isPassed: boolean;
+  notes: string;
+}
+
+// project-detail.ts lines 449-455
+modelCheckItems: ModelCheckItem[] = [
+  { id: 'check-1', name: '户型匹配度检查', isPassed: false, notes: '' },
+  { id: 'check-2', name: '尺寸精度验证', isPassed: false, notes: '' },
+  { id: 'check-3', name: '材质贴图检查', isPassed: false, notes: '' },
+  { id: 'check-4', name: '光影效果验证', isPassed: false, notes: '' },
+  { id: 'check-5', name: '细节完整性检查', isPassed: false, notes: '' }
+];
+```
+
+#### 3.3.2 检查项UI
+```html
+<div class="model-check-list">
+  <h4>模型检查项</h4>
+  @for (item of modelCheckItems; track item.id) {
+    <div class="check-item">
+      <label>
+        <input
+          type="checkbox"
+          [(ngModel)]="item.isPassed"
+          [disabled]="isReadOnly()">
+        <span class="check-name">{{ item.name }}</span>
+      </label>
+      <input
+        type="text"
+        [(ngModel)]="item.notes"
+        placeholder="备注说明"
+        [disabled]="isReadOnly()"
+        class="check-notes">
+    </div>
+  }
+</div>
+```
+
+### 3.4 建模阶段完成
+
+#### 3.4.1 确认上传方法
+```typescript
+// project-detail.ts lines 1853-1866
+confirmWhiteModelUpload(): void {
+  // 检查建模阶段的图片数据
+  const modelingProcess = this.deliveryProcesses.find(p => p.id === 'modeling');
+  if (!modelingProcess) return;
+
+  // 检查是否有任何空间上传了图片
+  const hasImages = modelingProcess.spaces.some(space => {
+    const content = modelingProcess.content[space.id];
+    return content && content.images && content.images.length > 0;
+  });
+
+  if (!hasImages) return;
+  this.advanceToNextStage('建模');
+}
+```
+
+#### 3.4.2 阶段推进逻辑
+```typescript
+// project-detail.ts lines 1391-1423
+advanceToNextStage(afterStage: ProjectStage): void {
+  const idx = this.stageOrder.indexOf(afterStage);
+  if (idx >= 0 && idx < this.stageOrder.length - 1) {
+    const next = this.stageOrder[idx + 1];
+
+    // 更新项目阶段
+    this.updateProjectStage(next);
+
+    // 更新展开状态,折叠当前、展开下一阶段
+    this.expandedStages[afterStage] = false;
+    this.expandedStages[next] = true;
+
+    // 更新板块展开状态
+    const nextSection = this.getSectionKeyForStage(next);
+    this.expandedSection = nextSection;
+
+    // 触发变更检测以更新导航栏颜色
+    this.cdr.detectChanges();
+  }
+}
+```
+
+## 4. 软装阶段
+
+### 4.1 功能特点
+- 小图上传(建议≤1MB,不强制)
+- 支持拖拽上传
+- 实时预览功能
+- 按空间组织
+
+### 4.2 小图上传
+
+#### 4.2.1 文件选择处理
+```typescript
+// project-detail.ts lines 1869-1881
+onSoftDecorSmallPicsSelected(event: Event): void {
+  const input = event.target as HTMLInputElement;
+  if (!input.files || input.files.length === 0) return;
+  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
+  const warnOversize = files.filter(f => f.size > 1024 * 1024);
+  if (warnOversize.length > 0) {
+    // 仅提示,不阻断
+    console.warn('软装小图建议≤1MB,以下文件较大:', warnOversize.map(f => f.name));
+  }
+  const items = files.map(f => this.makeImageItem(f));
+  this.softDecorImages.unshift(...items);
+  input.value = '';
+}
+```
+
+**文件大小校验**:
+- 建议≤1MB,超过仅警告不阻断
+- 支持 JPG、JPEG、PNG 格式
+- 自动过滤非图片文件
+
+#### 4.2.2 拖拽上传支持
+```typescript
+// project-detail.ts lines 1956-1998
+onDragOver(event: DragEvent): void {
+  event.preventDefault();
+  event.stopPropagation();
+  this.isDragOver = true;
+}
+
+onDragLeave(event: DragEvent): void {
+  event.preventDefault();
+  event.stopPropagation();
+  this.isDragOver = false;
+}
+
+onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
+  event.preventDefault();
+  event.stopPropagation();
+  this.isDragOver = false;
+
+  const files = event.dataTransfer?.files;
+  if (!files || files.length === 0) return;
+
+  // 创建模拟的input事件
+  const mockEvent = {
+    target: {
+      files: files
+    }
+  } as any;
+
+  // 根据类型调用相应的处理方法
+  switch (type) {
+    case 'softDecor':
+      this.onSoftDecorSmallPicsSelected(mockEvent);
+      break;
+    // ... 其他类型
+  }
+}
+```
+
+**拖拽区域样式**:
+```html
+<div class="upload-zone"
+     [class.drag-over]="isDragOver"
+     (dragover)="onDragOver($event)"
+     (dragleave)="onDragLeave($event)"
+     (drop)="onFileDrop($event, 'softDecor')">
+  <div class="upload-prompt">
+    <i class="icon-upload"></i>
+    <p>拖拽图片到此处上传</p>
+    <p class="hint">或点击选择文件(建议≤1MB)</p>
+  </div>
+</div>
+```
+
+### 4.3 图片预览
+
+#### 4.3.1 预览功能
+```typescript
+// project-detail.ts lines 1890-1903
+previewImage(img: any): void {
+  const isRenderLarge = !!this.renderLargeImages.find(i => i.id === img?.id);
+  if (isRenderLarge && img?.locked) {
+    alert('该渲染大图已加锁,需完成尾款结算并上传/识别支付凭证后方可预览。');
+    return;
+  }
+  this.previewImageData = img;
+  this.showImagePreview = true;
+}
+
+closeImagePreview(): void {
+  this.showImagePreview = false;
+  this.previewImageData = null;
+}
+```
+
+#### 4.3.2 预览弹窗
+```html
+@if (showImagePreview && previewImageData) {
+  <div class="image-preview-modal">
+    <div class="modal-overlay" (click)="closeImagePreview()"></div>
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3>{{ previewImageData.name }}</h3>
+        <button class="close-btn" (click)="closeImagePreview()">×</button>
+      </div>
+      <div class="modal-body">
+        <img [src]="previewImageData.url" [alt]="previewImageData.name">
+      </div>
+      <div class="modal-footer">
+        <button class="btn-secondary" (click)="downloadImage(previewImageData)">
+          下载图片
+        </button>
+        <button class="btn-danger" (click)="removeImageFromPreview()">
+          删除图片
+        </button>
+      </div>
+    </div>
+  </div>
+}
+```
+
+### 4.4 软装阶段完成
+
+```typescript
+// project-detail.ts lines 2098-2111
+confirmSoftDecorUpload(): void {
+  // 检查软装阶段的图片数据
+  const softDecorProcess = this.deliveryProcesses.find(p => p.id === 'soft-decoration');
+  if (!softDecorProcess) return;
+
+  // 检查是否有任何空间上传了图片
+  const hasImages = softDecorProcess.spaces.some(space => {
+    const content = softDecorProcess.content[space.id];
+    return content && content.images && content.images.length > 0;
+  });
+
+  if (!hasImages) return;
+  this.advanceToNextStage('软装');
+}
+```
+
+## 5. 渲染阶段
+
+### 5.1 功能特点
+- 4K图片强制校验(最大边≥4000像素)
+- 渲染大图自动加锁
+- 渲染进度监控
+- 异常反馈系统
+
+### 5.2 4K图片校验
+
+#### 5.2.1 图片尺寸验证
+```typescript
+// 4K校验方法
+private async validateImage4K(file: File): Promise<boolean> {
+  return new Promise((resolve, reject) => {
+    const img = new Image();
+    const url = URL.createObjectURL(file);
+
+    img.onload = () => {
+      URL.revokeObjectURL(url);
+      const maxDimension = Math.max(img.width, img.height);
+
+      // 4K标准:最大边需≥4000像素
+      if (maxDimension >= 4000) {
+        resolve(true);
+      } else {
+        resolve(false);
+      }
+    };
+
+    img.onerror = () => {
+      URL.revokeObjectURL(url);
+      reject(new Error('图片加载失败'));
+    };
+
+    img.src = url;
+  });
+}
+```
+
+#### 5.2.2 渲染大图上传
+```typescript
+// project-detail.ts lines 2142-2164
+async onRenderLargePicsSelected(event: Event): Promise<void> {
+  const input = event.target as HTMLInputElement;
+  if (!input.files || input.files.length === 0) return;
+  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
+
+  for (const f of files) {
+    const ok = await this.validateImage4K(f).catch(() => false);
+    if (!ok) {
+      alert(`图片不符合4K标准(最大边需≥4000像素):${f.name}`);
+      continue;
+    }
+    const item = this.makeImageItem(f);
+    // 直接添加到正式列表,渲染大图默认加锁
+    this.renderLargeImages.unshift({
+      id: item.id,
+      name: item.name,
+      url: item.url,
+      size: this.formatFileSize(f.size),
+      locked: true  // 渲染大图默认加锁
+    });
+  }
+  input.value = '';
+}
+```
+
+**校验规则**:
+- 支持 JPG、JPEG、PNG 格式
+- 最大边(宽或高)必须≥4000像素
+- 不符合标准的图片拒绝上传并提示
+
+### 5.3 渲染大图加锁机制
+
+#### 5.3.1 加锁逻辑
+```typescript
+// 渲染大图默认加锁
+renderLargeImages: Array<{
+  id: string;
+  name: string;
+  url: string;
+  size?: string;
+  locked?: boolean;  // 加锁标记
+  reviewStatus?: 'pending' | 'approved' | 'rejected';
+  synced?: boolean;
+}> = [];
+```
+
+**加锁规则**:
+- 所有渲染大图上传后自动加锁
+- 加锁状态下不可预览和下载
+- 需完成尾款结算后自动解锁
+
+#### 5.3.2 解锁逻辑
+```typescript
+// 尾款到账后自动解锁
+onPaymentReceived(paymentInfo?: any): void {
+  // 更新结算状态
+  this.settlementRecord.status = 'completed';
+  this.settlementRecord.paidAmount = paymentInfo?.amount || this.settlementRecord.remainingAmount;
+  this.settlementRecord.paidAt = new Date();
+
+  // 解锁渲染大图
+  this.autoUnlockAndSendImages();
+
+  // 发送支付确认通知
+  this.sendPaymentConfirmationNotifications();
+}
+
+private autoUnlockAndSendImages(): void {
+  // 解锁所有渲染大图
+  this.renderLargeImages.forEach(img => {
+    img.locked = false;
+  });
+
+  console.log('✅ 渲染大图已自动解锁');
+  alert('尾款已到账,渲染大图已解锁!客服可发送给客户。');
+}
+```
+
+### 5.4 渲染异常反馈
+
+#### 5.4.1 异常类型
+```typescript
+type ExceptionType = 'failed' | 'stuck' | 'quality' | 'other';
+
+interface ExceptionHistory {
+  id: string;
+  type: ExceptionType;
+  description: string;
+  submitTime: Date;
+  status: '待处理' | '处理中' | '已解决';
+  screenshotUrl?: string;
+  resolver?: string;
+  resolvedAt?: Date;
+}
+```
+
+#### 5.4.2 提交异常反馈
+```typescript
+// project-detail.ts lines 1715-1749
+submitExceptionFeedback(): void {
+  if (!this.exceptionDescription.trim() || this.isSubmittingFeedback) {
+    alert('请填写异常类型和描述');
+    return;
+  }
+
+  this.isSubmittingFeedback = true;
+
+  // 模拟提交反馈到服务器
+  setTimeout(() => {
+    const newException: ExceptionHistory = {
+      id: `exception-${Date.now()}`,
+      type: this.exceptionType,
+      description: this.exceptionDescription,
+      submitTime: new Date(),
+      status: '待处理'
+    };
+
+    // 添加到历史记录中
+    this.exceptionHistories.unshift(newException);
+
+    // 通知客服和技术支持
+    this.notifyTechnicalSupport(newException);
+
+    // 清空表单
+    this.exceptionDescription = '';
+    this.clearExceptionScreenshot();
+    this.showExceptionForm = false;
+
+    // 显示成功消息
+    alert('异常反馈已提交,技术支持将尽快处理');
+
+    this.isSubmittingFeedback = false;
+  }, 1000);
+}
+```
+
+#### 5.4.3 异常反馈UI
+```html
+<div class="exception-feedback-section">
+  <h4>渲染异常反馈</h4>
+
+  <button class="btn-report-exception" (click)="showExceptionForm = true">
+    报告渲染异常
+  </button>
+
+  @if (showExceptionForm) {
+    <div class="exception-form">
+      <div class="form-group">
+        <label>异常类型</label>
+        <select [(ngModel)]="exceptionType">
+          <option value="failed">渲染失败</option>
+          <option value="stuck">渲染卡顿</option>
+          <option value="quality">渲染质量问题</option>
+          <option value="other">其他问题</option>
+        </select>
+      </div>
+
+      <div class="form-group">
+        <label>问题描述</label>
+        <textarea
+          [(ngModel)]="exceptionDescription"
+          placeholder="请详细描述遇到的问题..."
+          rows="4">
+        </textarea>
+      </div>
+
+      <div class="form-group">
+        <label>上传截图(可选)</label>
+        <input
+          type="file"
+          id="screenshot-upload"
+          accept="image/*"
+          (change)="uploadExceptionScreenshot($event)">
+        @if (exceptionScreenshotUrl) {
+          <img [src]="exceptionScreenshotUrl" class="screenshot-preview">
+          <button class="btn-remove" (click)="clearExceptionScreenshot()">移除</button>
+        }
+      </div>
+
+      <div class="form-actions">
+        <button class="btn-primary"
+                (click)="submitExceptionFeedback()"
+                [disabled]="isSubmittingFeedback">
+          {{ isSubmittingFeedback ? '提交中...' : '提交反馈' }}
+        </button>
+        <button class="btn-secondary" (click)="showExceptionForm = false">
+          取消
+        </button>
+      </div>
+    </div>
+  }
+
+  <!-- 异常历史记录 -->
+  <div class="exception-history">
+    <h5>异常记录</h5>
+    @for (exception of exceptionHistories; track exception.id) {
+      <div class="exception-item" [class.resolved]="exception.status === '已解决'">
+        <div class="exception-header">
+          <span class="type-badge">{{ getExceptionTypeText(exception.type) }}</span>
+          <span class="status-badge">{{ exception.status }}</span>
+        </div>
+        <div class="exception-content">
+          <p>{{ exception.description }}</p>
+          <span class="time">{{ formatDateTime(exception.submitTime) }}</span>
+        </div>
+      </div>
+    }
+  </div>
+</div>
+```
+
+### 5.5 渲染阶段完成
+
+```typescript
+// project-detail.ts lines 2114-2127
+confirmRenderUpload(): void {
+  // 检查渲染阶段的图片数据
+  const renderProcess = this.deliveryProcesses.find(p => p.id === 'rendering');
+  if (!renderProcess) return;
+
+  // 检查是否有任何空间上传了图片
+  const hasImages = renderProcess.spaces.some(space => {
+    const content = renderProcess.content[space.id];
+    return content && content.images && content.images.length > 0;
+  });
+
+  if (!hasImages) return;
+  this.advanceToNextStage('渲染');
+}
+```
+
+## 6. 后期阶段
+
+### 6.1 功能特点
+- 最终图片处理
+- 色彩校正确认
+- 细节优化验证
+- 交付物整理
+
+### 6.2 后期图片上传
+
+```typescript
+// project-detail.ts lines 2030-2051
+async onPostProcessPicsSelected(event: Event): Promise<void> {
+  const input = event.target as HTMLInputElement;
+  if (!input.files || input.files.length === 0) return;
+  const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
+
+  for (const f of files) {
+    const item = this.makeImageItem(f);
+    this.postProcessImages.unshift({
+      id: item.id,
+      name: item.name,
+      url: item.url,
+      size: this.formatFileSize(f.size)
+    });
+  }
+  input.value = '';
+}
+
+removePostProcessImage(id: string): void {
+  const target = this.postProcessImages.find(i => i.id === id);
+  if (target) this.revokeUrl(target.url);
+  this.postProcessImages = this.postProcessImages.filter(i => i.id !== id);
+}
+```
+
+### 6.3 后期处理项
+
+**常见后期处理任务**:
+- 色彩校正和调整
+- 亮度/对比度优化
+- 细节锐化
+- 瑕疵修复
+- 水印添加(可选)
+- 文件格式转换
+
+### 6.4 后期阶段完成
+
+```typescript
+// project-detail.ts lines 2054-2067
+confirmPostProcessUpload(): void {
+  // 检查后期阶段的图片数据
+  const postProcessProcess = this.deliveryProcesses.find(p => p.id === 'post-processing');
+  if (!postProcessProcess) return;
+
+  // 检查是否有任何空间上传了图片
+  const hasImages = postProcessProcess.spaces.some(space => {
+    const content = postProcessProcess.content[space.id];
+    return content && content.images && content.images.length > 0;
+  });
+
+  if (!hasImages) return;
+  this.advanceToNextStage('后期');
+}
+```
+
+## 7. 统一空间文件处理
+
+### 7.1 空间文件上传
+
+#### 7.1.1 触发文件选择
+```typescript
+// project-detail.ts lines 5244-5251
+triggerSpaceFileInput(processId: string, spaceId: string): void {
+  const inputId = `space-file-input-${processId}-${spaceId}`;
+  const input = document.getElementById(inputId) as HTMLInputElement;
+  if (input) {
+    input.click();
+  }
+}
+```
+
+#### 7.1.2 处理空间文件
+```typescript
+// project-detail.ts lines 5265-5284
+private handleSpaceFiles(files: File[], processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  files.forEach(file => {
+    if (/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)) {
+      const imageItem = this.makeImageItem(file);
+      process.content[spaceId].images.push({
+        id: imageItem.id,
+        name: imageItem.name,
+        url: imageItem.url,
+        size: this.formatFileSize(file.size),
+        reviewStatus: 'pending'
+      });
+
+      // 更新进度
+      this.updateSpaceProgress(processId, spaceId);
+    }
+  });
+}
+```
+
+#### 7.1.3 空间文件拖拽
+```typescript
+// project-detail.ts lines 5254-5262
+onSpaceFileDrop(event: DragEvent, processId: string, spaceId: string): void {
+  event.preventDefault();
+  event.stopPropagation();
+
+  const files = event.dataTransfer?.files;
+  if (!files || files.length === 0) return;
+
+  this.handleSpaceFiles(Array.from(files), processId, spaceId);
+}
+```
+
+### 7.2 获取空间图片列表
+
+```typescript
+// project-detail.ts lines 5287-5292
+getSpaceImages(processId: string, spaceId: string): Array<{
+  id: string;
+  name: string;
+  url: string;
+  size?: string;
+  reviewStatus?: 'pending' | 'approved' | 'rejected'
+}> {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return [];
+
+  return process.content[spaceId].images || [];
+}
+```
+
+### 7.3 空间图片删除
+
+```typescript
+// 从空间中删除图片
+removeSpaceImage(processId: string, spaceId: string, imageId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  const images = process.content[spaceId].images;
+  const imageIndex = images.findIndex(img => img.id === imageId);
+
+  if (imageIndex > -1) {
+    // 释放URL资源
+    const image = images[imageIndex];
+    if (image.url && image.url.startsWith('blob:')) {
+      URL.revokeObjectURL(image.url);
+    }
+
+    // 从数组中移除
+    images.splice(imageIndex, 1);
+
+    // 更新进度
+    this.updateSpaceProgress(processId, spaceId);
+  }
+}
+```
+
+## 8. 审核流程
+
+### 8.1 审核状态管理
+
+#### 8.1.1 审核状态枚举
+```typescript
+type ReviewStatus = 'pending' | 'approved' | 'rejected';
+
+interface ImageWithReview {
+  id: string;
+  name: string;
+  url: string;
+  size?: string;
+  reviewStatus?: ReviewStatus;
+  reviewNotes?: string;
+  reviewedBy?: string;
+  reviewedAt?: Date;
+  synced?: boolean;  // 是否已同步到客户端
+}
+```
+
+#### 8.1.2 审核操作
+```typescript
+// 组长审核图片
+reviewSpaceImage(
+  processId: string,
+  spaceId: string,
+  imageId: string,
+  status: ReviewStatus,
+  notes?: string
+): void {
+  if (!this.isTeamLeaderView()) {
+    alert('仅组长可以审核图片');
+    return;
+  }
+
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  const image = process.content[spaceId].images.find(img => img.id === imageId);
+  if (!image) return;
+
+  // 更新审核状态
+  image.reviewStatus = status;
+  image.reviewNotes = notes;
+  image.reviewedBy = this.getCurrentUserName();
+  image.reviewedAt = new Date();
+
+  // 如果审核通过,标记为已同步
+  if (status === 'approved') {
+    image.synced = true;
+  }
+
+  console.log(`图片审核完成: ${image.name} - ${status}`);
+}
+```
+
+### 8.2 批量审核
+
+```typescript
+// 批量审核空间内所有图片
+batchReviewSpaceImages(
+  processId: string,
+  spaceId: string,
+  status: ReviewStatus
+): void {
+  if (!this.isTeamLeaderView()) {
+    alert('仅组长可以批量审核图片');
+    return;
+  }
+
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  const images = process.content[spaceId].images;
+  const pendingImages = images.filter(img => img.reviewStatus === 'pending');
+
+  if (pendingImages.length === 0) {
+    alert('没有待审核的图片');
+    return;
+  }
+
+  const confirmed = confirm(
+    `确定要批量${status === 'approved' ? '通过' : '驳回'}${pendingImages.length}张图片吗?`
+  );
+
+  if (!confirmed) return;
+
+  pendingImages.forEach(image => {
+    image.reviewStatus = status;
+    image.reviewedBy = this.getCurrentUserName();
+    image.reviewedAt = new Date();
+    if (status === 'approved') {
+      image.synced = true;
+    }
+  });
+
+  alert(`已批量审核${pendingImages.length}张图片`);
+}
+```
+
+### 8.3 审核统计
+
+```typescript
+// 获取空间审核统计
+getSpaceReviewStats(processId: string, spaceId: string): {
+  total: number;
+  pending: number;
+  approved: number;
+  rejected: number;
+} {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) {
+    return { total: 0, pending: 0, approved: 0, rejected: 0 };
+  }
+
+  const images = process.content[spaceId].images;
+
+  return {
+    total: images.length,
+    pending: images.filter(img => img.reviewStatus === 'pending').length,
+    approved: images.filter(img => img.reviewStatus === 'approved').length,
+    rejected: images.filter(img => img.reviewStatus === 'rejected').length
+  };
+}
+```
+
+## 9. 权限控制
+
+### 9.1 角色权限矩阵
+
+| 操作 | 设计师 | 渲染师 | 组长 | 技术 |
+|-----|--------|--------|------|------|
+| 查看交付执行板块 | ✅ | ✅ | ✅ | ✅ |
+| 上传建模图片 | ✅ | ❌ | ✅ | ❌ |
+| 上传软装图片 | ✅ | ❌ | ✅ | ❌ |
+| 上传渲染图片 | ❌ | ✅ | ✅ | ❌ |
+| 上传后期图片 | ✅ | ❌ | ✅ | ❌ |
+| 添加/删除空间 | ✅ | ✅ | ✅ | ❌ |
+| 审核图片 | ❌ | ❌ | ✅ | ❌ |
+| 确认阶段完成 | ✅ | ✅ | ✅ | ❌ |
+| 报告渲染异常 | ❌ | ✅ | ✅ | ❌ |
+| 最终验收 | ❌ | ❌ | ❌ | ✅ |
+
+### 9.2 权限检查方法
+
+```typescript
+// project-detail.ts lines 911-936
+isDesignerView(): boolean {
+  return this.roleContext === 'designer';
+}
+
+isTeamLeaderView(): boolean {
+  return this.roleContext === 'team-leader';
+}
+
+isTechnicalView(): boolean {
+  return this.roleContext === 'technical';
+}
+
+canEditSection(sectionKey: SectionKey): boolean {
+  if (this.isCustomerServiceView()) {
+    return sectionKey === 'order' ||
+           sectionKey === 'requirements' ||
+           sectionKey === 'aftercare';
+  }
+  return true; // 设计师和组长可以编辑所有板块
+}
+
+canEditStage(stage: ProjectStage): boolean {
+  if (this.isCustomerServiceView()) {
+    const editableStages: ProjectStage[] = [
+      '订单分配', '需求沟通', '方案确认',
+      '尾款结算', '客户评价', '投诉处理'
+    ];
+    return editableStages.includes(stage);
+  }
+  return true;
+}
+```
+
+### 9.3 UI权限控制
+
+```html
+<!-- 上传按钮权限 -->
+@if (!isReadOnly() && canEditStage('建模')) {
+  <button class="btn-upload" (click)="triggerSpaceFileInput(process.id, space.id)">
+    上传图片
+  </button>
+}
+
+<!-- 审核按钮权限 -->
+@if (isTeamLeaderView()) {
+  <button class="btn-review" (click)="reviewSpaceImage(process.id, space.id, image.id, 'approved')">
+    通过
+  </button>
+  <button class="btn-reject" (click)="reviewSpaceImage(process.id, space.id, image.id, 'rejected')">
+    驳回
+  </button>
+}
+
+<!-- 删除按钮权限 -->
+@if (!isReadOnly() && (isDesignerView() || isTeamLeaderView())) {
+  <button class="btn-delete" (click)="removeSpaceImage(process.id, space.id, image.id)">
+    删除
+  </button>
+}
+```
+
+## 10. 数据流转
+
+### 10.1 阶段推进流程
+
+```mermaid
+sequenceDiagram
+    participant Designer as 设计师
+    participant System as 系统
+    participant Leader as 组长
+    participant Next as 下一阶段
+
+    Designer->>System: 上传图片到空间
+    System->>System: 更新空间进度
+    System->>System: 计算阶段完成度
+    Designer->>System: 确认阶段上传
+    System->>System: 验证图片数量
+    alt 有图片
+        System->>Leader: 通知审核
+        Leader->>System: 审核图片
+        System->>Next: 推进到下一阶段
+    else 无图片
+        System->>Designer: 提示先上传图片
+    end
+```
+
+### 10.2 进度同步机制
+
+```typescript
+// 更新空间进度后同步到项目
+private syncProgressToProject(processId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process) return;
+
+  // 计算整体进度
+  const spaces = process.spaces;
+  const totalProgress = spaces.reduce((sum, space) => {
+    return sum + (process.content[space.id]?.progress || 0);
+  }, 0);
+
+  const averageProgress = spaces.length > 0
+    ? Math.round(totalProgress / spaces.length)
+    : 0;
+
+  // 更新项目进度
+  if (this.project) {
+    this.project.progress = averageProgress;
+  }
+
+  // 触发变更检测
+  this.cdr.detectChanges();
+}
+```
+
+### 10.3 客户端数据同步
+
+```typescript
+// 审核通过后同步到客户端
+private syncApprovedImagesToClient(processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  const approvedImages = process.content[spaceId].images
+    .filter(img => img.reviewStatus === 'approved' && !img.synced);
+
+  if (approvedImages.length === 0) return;
+
+  // 调用API同步到客户端
+  this.projectService.syncImagesToClient(
+    this.projectId,
+    processId,
+    spaceId,
+    approvedImages.map(img => img.id)
+  ).subscribe({
+    next: (result) => {
+      if (result.success) {
+        // 标记为已同步
+        approvedImages.forEach(img => {
+          img.synced = true;
+        });
+        console.log(`已同步${approvedImages.length}张图片到客户端`);
+      }
+    },
+    error: (error) => {
+      console.error('同步图片失败:', error);
+    }
+  });
+}
+```
+
+## 11. 异常处理
+
+### 11.1 文件上传失败
+
+```typescript
+// 文件上传错误处理
+private handleFileUploadError(error: any, fileName: string): void {
+  let errorMessage = '文件上传失败';
+
+  if (error.status === 413) {
+    errorMessage = `文件过大:${fileName}(最大10MB)`;
+  } else if (error.status === 415) {
+    errorMessage = `不支持的文件格式:${fileName}`;
+  } else if (error.status === 500) {
+    errorMessage = '服务器错误,请稍后重试';
+  }
+
+  alert(errorMessage);
+  console.error('文件上传失败:', error);
+}
+```
+
+### 11.2 4K校验失败
+
+```typescript
+// 4K校验失败处理
+private handle4KValidationFailure(file: File, dimensions: {width: number; height: number}): void {
+  const maxDimension = Math.max(dimensions.width, dimensions.height);
+
+  const message = `
+    图片不符合4K标准
+
+    文件名: ${file.name}
+    当前尺寸: ${dimensions.width} × ${dimensions.height}
+    最大边: ${maxDimension}px
+    要求: 最大边 ≥ 4000px
+
+    请使用符合4K标准的图片重新上传。
+  `;
+
+  alert(message);
+  console.warn('4K校验失败:', file.name, dimensions);
+}
+```
+
+### 11.3 渲染异常处理
+
+```typescript
+// 渲染超时预警
+checkRenderTimeout(): void {
+  if (!this.renderProgress || !this.project) return;
+
+  const deliveryTime = new Date(this.project.deadline);
+  const currentTime = new Date();
+  const timeDifference = deliveryTime.getTime() - currentTime.getTime();
+  const hoursRemaining = Math.floor(timeDifference / (1000 * 60 * 60));
+
+  if (hoursRemaining <= 3 && hoursRemaining > 0) {
+    alert('渲染进度预警:交付前3小时,请关注渲染进度');
+  }
+
+  if (hoursRemaining <= 1 && hoursRemaining > 0) {
+    alert('渲染进度严重预警:交付前1小时,渲染可能无法按时完成!');
+    this.notifyTeamLeader('render-failed');
+  }
+}
+```
+
+### 11.4 空间操作失败
+
+```typescript
+// 删除空间时的安全检查
+removeSpace(processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process) return;
+
+  const space = process.spaces.find(s => s.id === spaceId);
+  if (!space) return;
+
+  // 检查空间是否有图片
+  const hasImages = process.content[spaceId]?.images?.length > 0;
+
+  if (hasImages) {
+    const confirmed = confirm(
+      `空间"${space.name}"中有${process.content[spaceId].images.length}张图片,确定要删除吗?\n删除后图片将无法恢复。`
+    );
+    if (!confirmed) return;
+  }
+
+  // 执行删除
+  const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
+  if (spaceIndex > -1) {
+    process.spaces.splice(spaceIndex, 1);
+
+    // 清理资源
+    if (process.content[spaceId]) {
+      process.content[spaceId].images.forEach(img => {
+        if (img.url && img.url.startsWith('blob:')) {
+          URL.revokeObjectURL(img.url);
+        }
+      });
+      delete process.content[spaceId];
+    }
+
+    console.log(`已删除空间: ${space.name}`);
+  }
+}
+```
+
+## 12. 性能优化
+
+### 12.1 Blob URL管理
+
+```typescript
+// 组件销毁时清理所有Blob URL
+ngOnDestroy(): void {
+  // 释放所有 blob 预览 URL
+  const revokeList: string[] = [];
+
+  // 收集所有Blob URL
+  this.deliveryProcesses.forEach(process => {
+    Object.values(process.content).forEach(content => {
+      content.images.forEach(img => {
+        if (img.url && img.url.startsWith('blob:')) {
+          revokeList.push(img.url);
+        }
+      });
+    });
+  });
+
+  // 批量释放
+  revokeList.forEach(url => URL.revokeObjectURL(url));
+
+  console.log(`已释放${revokeList.length}个Blob URL`);
+}
+```
+
+### 12.2 图片懒加载
+
+```typescript
+// 使用Intersection Observer实现懒加载
+private setupImageLazyLoading(): void {
+  if (!('IntersectionObserver' in window)) return;
+
+  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);
+        }
+      }
+    });
+  }, {
+    rootMargin: '50px'  // 提前50px开始加载
+  });
+
+  // 观察所有懒加载图片
+  document.querySelectorAll('img[data-src]').forEach(img => {
+    observer.observe(img);
+  });
+}
+```
+
+### 12.3 进度计算优化
+
+```typescript
+// 使用防抖避免频繁计算
+private progressUpdateDebounce: any;
+
+private updateSpaceProgress(processId: string, spaceId: string): void {
+  // 清除之前的定时器
+  if (this.progressUpdateDebounce) {
+    clearTimeout(this.progressUpdateDebounce);
+  }
+
+  // 延迟300ms执行
+  this.progressUpdateDebounce = setTimeout(() => {
+    this.doUpdateSpaceProgress(processId, spaceId);
+  }, 300);
+}
+
+private doUpdateSpaceProgress(processId: string, spaceId: string): void {
+  const process = this.deliveryProcesses.find(p => p.id === processId);
+  if (!process || !process.content[spaceId]) return;
+
+  const content = process.content[spaceId];
+  const imageCount = content.images.length;
+
+  // 计算进度
+  if (imageCount === 0) {
+    content.progress = 0;
+    content.status = 'pending';
+  } else if (imageCount < 3) {
+    content.progress = Math.min(imageCount * 30, 90);
+    content.status = 'in_progress';
+  } else {
+    content.progress = 100;
+    content.status = 'completed';
+  }
+
+  content.lastUpdated = new Date();
+
+  // 同步到项目
+  this.syncProgressToProject(processId);
+}
+```
+
+## 13. 测试用例
+
+### 13.1 空间管理测试
+
+```typescript
+describe('Space Management', () => {
+  it('should add new space to process', () => {
+    component.newSpaceName['modeling'] = '书房';
+    component.addSpace('modeling');
+
+    const modelingProcess = component.deliveryProcesses.find(p => p.id === 'modeling');
+    expect(modelingProcess?.spaces.length).toBe(4);
+    expect(modelingProcess?.spaces[3].name).toBe('书房');
+    expect(modelingProcess?.content['space_*']).toBeDefined();
+  });
+
+  it('should remove space and clean up resources', () => {
+    const process = component.deliveryProcesses[0];
+    const spaceId = process.spaces[0].id;
+
+    // 添加一些图片
+    process.content[spaceId].images = [
+      { id: '1', name: 'test.jpg', url: 'blob:test', size: '1MB' }
+    ];
+
+    component.removeSpace(process.id, spaceId);
+
+    expect(process.spaces.length).toBe(2);
+    expect(process.content[spaceId]).toBeUndefined();
+  });
+
+  it('should toggle space expansion', () => {
+    const process = component.deliveryProcesses[0];
+    const space = process.spaces[0];
+    const initialState = space.isExpanded;
+
+    component.toggleSpace(process.id, space.id);
+
+    expect(space.isExpanded).toBe(!initialState);
+  });
+});
+```
+
+### 13.2 文件上传测试
+
+```typescript
+describe('File Upload', () => {
+  it('should validate 4K images correctly', async () => {
+    const file = new File([''], 'test-4k.jpg', { type: 'image/jpeg' });
+
+    // Mock image dimensions
+    spyOn<any>(component, 'validateImage4K').and.returnValue(Promise.resolve(true));
+
+    await component.onRenderLargePicsSelected({
+      target: { files: [file] }
+    } as any);
+
+    expect(component.renderLargeImages.length).toBeGreaterThan(0);
+    expect(component.renderLargeImages[0].locked).toBe(true);
+  });
+
+  it('should reject non-4K images', async () => {
+    const file = new File([''], 'small.jpg', { type: 'image/jpeg' });
+
+    spyOn<any>(component, 'validateImage4K').and.returnValue(Promise.resolve(false));
+    spyOn(window, 'alert');
+
+    await component.onRenderLargePicsSelected({
+      target: { files: [file] }
+    } as any);
+
+    expect(window.alert).toHaveBeenCalledWith(jasmine.stringContaining('不符合4K标准'));
+  });
+
+  it('should handle soft decor upload with size warning', () => {
+    const largeFile = new File(['x'.repeat(2 * 1024 * 1024)], 'large.jpg', { type: 'image/jpeg' });
+
+    spyOn(console, 'warn');
+
+    component.onSoftDecorSmallPicsSelected({
+      target: { files: [largeFile] }
+    } as any);
+
+    expect(console.warn).toHaveBeenCalled();
+    expect(component.softDecorImages.length).toBeGreaterThan(0);
+  });
+});
+```
+
+### 13.3 进度更新测试
+
+```typescript
+describe('Progress Tracking', () => {
+  it('should update space progress based on image count', () => {
+    const process = component.deliveryProcesses[0];
+    const spaceId = process.spaces[0].id;
+
+    // 添加2张图片
+    process.content[spaceId].images = [
+      { id: '1', name: 'img1.jpg', url: 'blob:1' },
+      { id: '2', name: 'img2.jpg', url: 'blob:2' }
+    ];
+
+    component['updateSpaceProgress'](process.id, spaceId);
+
+    expect(process.content[spaceId].progress).toBe(60);
+    expect(process.content[spaceId].status).toBe('in_progress');
+  });
+
+  it('should mark as completed with 3+ images', () => {
+    const process = component.deliveryProcesses[0];
+    const spaceId = process.spaces[0].id;
+
+    process.content[spaceId].images = [
+      { id: '1', name: 'img1.jpg', url: 'blob:1' },
+      { id: '2', name: 'img2.jpg', url: 'blob:2' },
+      { id: '3', name: 'img3.jpg', url: 'blob:3' }
+    ];
+
+    component['updateSpaceProgress'](process.id, spaceId);
+
+    expect(process.content[spaceId].progress).toBe(100);
+    expect(process.content[spaceId].status).toBe('completed');
+  });
+});
+```
+
+### 13.4 阶段推进测试
+
+```typescript
+describe('Stage Progression', () => {
+  it('should advance to next stage after confirmation', () => {
+    // 设置建模阶段有图片
+    const modelingProcess = component.deliveryProcesses.find(p => p.id === 'modeling');
+    if (modelingProcess) {
+      modelingProcess.content['bedroom'].images = [
+        { id: '1', name: 'test.jpg', url: 'blob:test' }
+      ];
+    }
+
+    component.currentStage = '建模';
+    component.confirmWhiteModelUpload();
+
+    expect(component.currentStage).toBe('软装');
+    expect(component.expandedStages['软装']).toBe(true);
+    expect(component.expandedStages['建模']).toBe(false);
+  });
+
+  it('should not advance without images', () => {
+    component.currentStage = '建模';
+    const initialStage = component.currentStage;
+
+    component.confirmWhiteModelUpload();
+
+    expect(component.currentStage).toBe(initialStage);
+  });
+});
+```
+
+---
+
+**文档版本**:v1.0.0
+**创建日期**:2025-10-16
+**最后更新**:2025-10-16
+**维护人**:产品团队

+ 1645 - 0
docs/prd/项目-售后归档.md

@@ -0,0 +1,1645 @@
+# 项目管理 - 售后归档阶段 PRD
+
+## 1. 功能概述
+
+### 1.1 阶段定位
+售后归档阶段是项目管理流程的收尾环节,包含尾款结算、全景图合成、客户评价、投诉处理、项目复盘五大核心模块。该阶段负责完成项目交付、收集反馈、总结经验,为后续项目优化提供数据支撑。
+
+### 1.2 核心目标
+- 实现自动化尾款结算流程
+- 生成全景图分享链接
+- 收集客户多维度评价
+- 处理客户投诉反馈
+- 生成项目复盘报告
+
+### 1.3 涉及角色
+- **客服人员**:跟进尾款支付、发送评价链接、处理投诉
+- **技术人员**:验收交付物、启动自动结算、合成全景图
+- **组长**:审核复盘报告、处理投诉、优化流程
+- **财务人员**:确认款项到账、核对支付凭证
+
+### 1.4 五大核心模块
+
+```mermaid
+graph TD
+    A[后期完成] --> B[尾款结算]
+    B --> C[全景图合成]
+    C --> D[客户评价]
+    D --> E[投诉处理]
+    E --> F[项目复盘]
+
+    style B fill:#e8f5e9
+    style C fill:#fff3e0
+    style D fill:#e3f2fd
+    style E fill:#fce4ec
+    style F fill:#f3e5f5
+```
+
+## 2. 尾款结算模块
+
+### 2.1 功能特点
+- 技术验收触发自动化结算
+- 小程序支付自动监听
+- 支付凭证智能识别
+- 渲染大图自动解锁
+- 客服一键发图
+
+### 2.2 自动化结算流程
+
+#### 2.2.1 启动自动化结算
+```typescript
+// project-detail.ts lines 3892-3938
+initiateAutoSettlement(): void {
+  console.log('🚀 启动自动化尾款结算流程');
+
+  // 1. 权限验证
+  if (!this.isTechnicalView()) {
+    alert('⚠️ 仅技术人员可以启动自动化结算流程');
+    return;
+  }
+
+  // 2. 验收状态检查
+  if (!this.isAllDeliveryCompleted()) {
+    alert('⚠️ 请先完成所有交付阶段验收');
+    return;
+  }
+
+  console.log('✅ 验收状态检查通过');
+
+  // 3. 激活小程序支付监听
+  this.miniprogramPaymentStatus = 'active';
+  console.log('📱 小程序支付监听已激活');
+
+  // 4. 创建尾款结算记录
+  this.createFinalPaymentRecord();
+
+  // 5. 通知客服跟进尾款
+  this.notifyCustomerServiceForFinalPayment();
+
+  // 6. 启动支付自动化
+  this.setupPaymentAutomation();
+
+  alert('✅ 自动化结算流程已启动!\n\n- 小程序支付监听已激活\n- 客服已收到尾款跟进通知\n- 支付到账后将自动解锁大图');
+}
+```
+
+**权限验证**:
+- 仅技术人员可以启动
+- 确保所有交付阶段已完成
+- 验证交付物质量合格
+
+#### 2.2.2 创建结算记录
+```typescript
+// project-detail.ts lines 3940-3962
+private createFinalPaymentRecord(): void {
+  const totalAmount = this.orderAmount || 150000;
+  const downPayment = totalAmount * 0.5; // 假设定金50%
+  const remainingAmount = totalAmount - downPayment;
+
+  this.settlementRecord = {
+    id: `settlement-${Date.now()}`,
+    projectId: this.projectId,
+    totalAmount: totalAmount,
+    downPayment: downPayment,
+    remainingAmount: remainingAmount,
+    paidAmount: 0,
+    status: 'pending',
+    createdAt: new Date(),
+    dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7天后到期
+    paymentMethod: undefined,
+    paidAt: undefined,
+    notes: '技术验收完成,等待客户支付尾款'
+  };
+
+  console.log('📝 尾款结算记录已创建:', this.settlementRecord);
+}
+```
+
+**结算记录结构**:
+```typescript
+interface SettlementRecord {
+  id: string;
+  projectId: string;
+  totalAmount: number;          // 订单总金额
+  downPayment: number;          // 定金金额
+  remainingAmount: number;      // 尾款金额
+  paidAmount: number;           // 已支付金额
+  status: 'pending' | 'partial' | 'completed' | 'overdue';
+  createdAt: Date;
+  dueDate: Date;                // 到期日期
+  paymentMethod?: 'wechat' | 'alipay' | 'bank';
+  paidAt?: Date;
+  voucherUrl?: string;          // 支付凭证URL
+  notes?: string;
+}
+```
+
+#### 2.2.3 通知客服跟进
+```typescript
+// project-detail.ts lines 3964-3978
+private notifyCustomerServiceForFinalPayment(): void {
+  const notification = {
+    type: 'final-payment-reminder',
+    projectId: this.projectId,
+    projectName: this.project?.name || '未命名项目',
+    amount: this.settlementRecord?.remainingAmount || 0,
+    dueDate: this.settlementRecord?.dueDate,
+    message: `项目"${this.project?.name}"已完成技术验收,请跟进客户支付尾款 ¥${this.settlementRecord?.remainingAmount.toLocaleString()}`
+  };
+
+  // 实际应用中调用通知服务
+  console.log('📧 已发送客服通知:', notification);
+
+  // 模拟通知发送
+  alert(`✉️ 已通知客服跟进尾款\n\n项目: ${notification.projectName}\n尾款金额: ¥${notification.amount.toLocaleString()}`);
+}
+```
+
+### 2.3 支付监听系统
+
+#### 2.3.1 小程序支付监听
+```typescript
+// project-detail.ts lines 3980-4012
+private setupPaymentAutomation(): void {
+  console.log('🔧 设置支付自动化监听');
+
+  // 监听小程序支付状态变化
+  // 实际应用中应使用WebSocket或轮询API
+  this.miniprogramPaymentStatus = 'active';
+
+  // 模拟支付监听(实际项目中应替换为真实的WebSocket连接)
+  this.simulatePaymentMonitoring();
+}
+
+private simulatePaymentMonitoring(): void {
+  console.log('🔄 开始模拟支付监听...');
+
+  // 实际项目中应该是:
+  // 1. 建立WebSocket连接到支付服务器
+  // 2. 监听支付成功事件
+  // 3. 接收支付信息(金额、方式、时间等)
+  // 4. 自动触发解锁流程
+
+  // 这里仅作演示,实际不会自动触发
+  console.log('💡 提示: 客户通过小程序支付后,系统将自动接收通知');
+  console.log('💡 提示: 支付凭证识别功能可手动上传截图触发');
+}
+```
+
+**监听流程**:
+```mermaid
+sequenceDiagram
+    participant Customer as 客户
+    participant MiniApp as 小程序
+    participant PaymentGateway as 支付网关
+    participant System as 系统
+    participant Designer as 设计师
+
+    Customer->>MiniApp: 发起尾款支付
+    MiniApp->>PaymentGateway: 调用支付接口
+    PaymentGateway-->>MiniApp: 支付成功回调
+    MiniApp->>System: 推送支付通知
+    System->>System: 更新结算状态
+    System->>System: 解锁渲染大图
+    System->>Designer: 通知客服发图
+```
+
+#### 2.3.2 支付到账处理
+```typescript
+// project-detail.ts lines 4014-4048
+onPaymentReceived(paymentInfo?: any): void {
+  console.log('💰 收到支付通知:', paymentInfo);
+
+  if (!this.settlementRecord) {
+    console.error('❌ 结算记录不存在');
+    return;
+  }
+
+  // 更新结算状态
+  this.settlementRecord.status = 'completed';
+  this.settlementRecord.paidAmount = paymentInfo?.amount || this.settlementRecord.remainingAmount;
+  this.settlementRecord.paymentMethod = paymentInfo?.method || 'wechat';
+  this.settlementRecord.paidAt = new Date();
+
+  console.log('✅ 结算状态已更新:', this.settlementRecord);
+
+  // 自动解锁渲染大图
+  this.autoUnlockAndSendImages();
+
+  // 发送支付确认通知
+  this.sendPaymentConfirmationNotifications();
+
+  // 停止支付监听
+  this.miniprogramPaymentStatus = 'completed';
+
+  console.log('🎉 尾款结算流程完成');
+}
+```
+
+#### 2.3.3 自动解锁大图
+```typescript
+// project-detail.ts lines 4050-4068
+private autoUnlockAndSendImages(): void {
+  console.log('🔓 开始自动解锁渲染大图');
+
+  // 解锁所有渲染大图
+  let unlockedCount = 0;
+  this.renderLargeImages.forEach(img => {
+    if (img.locked) {
+      img.locked = false;
+      unlockedCount++;
+    }
+  });
+
+  console.log(`✅ 已解锁${unlockedCount}张渲染大图`);
+
+  // 通知客服可以发送大图
+  alert(`✅ 尾款已到账,${unlockedCount}张渲染大图已解锁!\n\n客服可一键发送给客户。`);
+
+  // 触发界面更新
+  this.cdr.detectChanges();
+}
+```
+
+### 2.4 支付凭证识别
+
+#### 2.4.1 凭证上传
+```typescript
+// 上传支付凭证
+uploadPaymentVoucher(event: Event): void {
+  const input = event.target as HTMLInputElement;
+  if (!input.files || input.files.length === 0) return;
+
+  const file = input.files[0];
+
+  // 验证文件类型
+  if (!file.type.startsWith('image/')) {
+    alert('请上传图片格式的支付凭证');
+    return;
+  }
+
+  this.isUploadingVoucher = true;
+
+  // 上传文件到服务器
+  this.uploadFile(file).then(url => {
+    // 触发智能识别
+    this.recognizePaymentVoucher(url);
+  }).catch(error => {
+    console.error('支付凭证上传失败:', error);
+    alert('上传失败,请重试');
+    this.isUploadingVoucher = false;
+  });
+}
+```
+
+#### 2.4.2 智能识别
+```typescript
+// 调用支付凭证识别服务
+private recognizePaymentVoucher(imageUrl: string): void {
+  this.paymentVoucherService.recognize(imageUrl).subscribe({
+    next: (result) => {
+      console.log('识别结果:', result);
+
+      // 显示识别结果
+      this.voucherRecognitionResult = {
+        amount: result.amount,
+        paymentMethod: result.method,
+        transactionId: result.transactionId,
+        transactionTime: result.time,
+        confidence: result.confidence
+      };
+
+      // 如果识别置信度高,自动填充
+      if (result.confidence > 0.8) {
+        this.autoFillPaymentInfo(result);
+      }
+
+      this.isUploadingVoucher = false;
+    },
+    error: (error) => {
+      console.error('支付凭证识别失败:', error);
+      alert('识别失败,请手动填写支付信息');
+      this.isUploadingVoucher = false;
+    }
+  });
+}
+```
+
+**识别结果结构**:
+```typescript
+interface VoucherRecognitionResult {
+  amount: number;                           // 支付金额
+  paymentMethod: 'wechat' | 'alipay';      // 支付方式
+  transactionId: string;                    // 交易单号
+  transactionTime: Date;                    // 交易时间
+  confidence: number;                       // 识别置信度 0-1
+  merchantName?: string;                    // 商户名称
+  remarks?: string;                         // 备注信息
+}
+```
+
+### 2.5 一键发图功能
+
+```typescript
+// 客服一键发送渲染大图
+sendImagesToCustomer(): void {
+  const unlockedImages = this.renderLargeImages.filter(img => !img.locked);
+
+  if (unlockedImages.length === 0) {
+    alert('没有可发送的图片(渲染大图未解锁)');
+    return;
+  }
+
+  // 生成图片下载链接
+  const imageLinks = unlockedImages.map(img => ({
+    name: img.name,
+    url: img.url,
+    size: img.size
+  }));
+
+  // 调用发送服务
+  this.projectService.sendImagesToCustomer(
+    this.projectId,
+    imageLinks
+  ).subscribe({
+    next: (result) => {
+      if (result.success) {
+        alert(`✅ 已成功发送${unlockedImages.length}张图片给客户!`);
+
+        // 标记为已发送
+        unlockedImages.forEach(img => {
+          img.synced = true;
+        });
+      }
+    },
+    error: (error) => {
+      console.error('发送图片失败:', error);
+      alert('发送失败,请重试');
+    }
+  });
+}
+```
+
+## 3. 全景图合成模块
+
+### 3.1 功能特点
+- KR Panel集成
+- 智能空间标注
+- 自动生成分享链接
+- 漫游式预览体验
+
+### 3.2 全景图合成流程
+
+#### 3.2.1 启动合成
+```typescript
+// 开始全景图合成
+startPanoramicSynthesis(): void {
+  console.log('🖼️ 启动全景图合成');
+
+  // 打开文件选择对话框
+  const input = document.createElement('input');
+  input.type = 'file';
+  input.multiple = true;
+  input.accept = 'image/*';
+
+  input.onchange = (event: any) => {
+    const files = Array.from(event.target.files) as File[];
+    if (files.length === 0) return;
+
+    this.uploadAndSynthesizePanoramic(files);
+  };
+
+  input.click();
+}
+```
+
+#### 3.2.2 文件上传与合成
+```typescript
+// project-detail.ts lines 4217-4288
+private uploadAndSynthesizePanoramic(files: File[]): void {
+  console.log(`📤 开始上传${files.length}个文件...`);
+
+  this.isUploadingPanoramicFiles = true;
+  this.panoramicUploadProgress = 0;
+
+  // 模拟文件上传进度
+  const uploadInterval = setInterval(() => {
+    this.panoramicUploadProgress += 10;
+    if (this.panoramicUploadProgress >= 100) {
+      this.panoramicUploadProgress = 100;
+      clearInterval(uploadInterval);
+
+      // 上传完成,开始合成
+      this.synthesizePanoramicView(files);
+    }
+  }, 300);
+}
+
+private synthesizePanoramicView(files: File[]): void {
+  console.log('🔧 开始合成全景图...');
+
+  this.isUploadingPanoramicFiles = false;
+  this.isSynthesizingPanoramic = true;
+  this.panoramicSynthesisProgress = 0;
+
+  // 模拟合成进度
+  const synthesisInterval = setInterval(() => {
+    this.panoramicSynthesisProgress += 5;
+    if (this.panoramicSynthesisProgress >= 100) {
+      this.panoramicSynthesisProgress = 100;
+      clearInterval(synthesisInterval);
+
+      // 合成完成
+      this.completePanoramicSynthesis(files);
+    }
+  }, 500);
+}
+```
+
+**KR Panel集成**:
+- 支持多角度图片合成
+- 自动识别空间名称
+- 生成3D漫游场景
+- 支持VR模式预览
+
+#### 3.2.3 完成合成
+```typescript
+// project-detail.ts lines 4290-4328
+private completePanoramicSynthesis(files: File[]): void {
+  this.isSynthesizingPanoramic = false;
+
+  // 创建全景图合成记录
+  const synthesis: PanoramicSynthesis = {
+    id: `panoramic-${Date.now()}`,
+    name: `全景图_${new Date().toLocaleDateString()}`,
+    createdAt: new Date(),
+    spaces: files.map((file, index) => ({
+      id: `space-${index}`,
+      name: this.extractSpaceName(file.name),
+      imageUrl: URL.createObjectURL(file),
+      angle: index * 60 // 假设每60度一个角度
+    })),
+    previewUrl: 'https://example.com/panoramic/preview',
+    downloadUrl: 'https://example.com/panoramic/download',
+    shareLink: '',
+    fileSize: files.reduce((sum, f) => sum + f.size, 0),
+    status: 'completed'
+  };
+
+  // 添加到历史记录
+  this.panoramicSynthesisHistory.push(synthesis);
+
+  // 生成分享链接
+  this.generatePanoramicShareLink(synthesis);
+
+  console.log('✅ 全景图合成完成:', synthesis);
+
+  alert(`✅ 全景图合成完成!\n\n已生成${synthesis.spaces.length}个空间的全景图\n文件大小: ${this.formatFileSize(synthesis.fileSize)}`);
+}
+```
+
+**全景图数据结构**:
+```typescript
+interface PanoramicSynthesis {
+  id: string;
+  name: string;
+  createdAt: Date;
+  spaces: Array<{
+    id: string;
+    name: string;              // 空间名称:客厅-角度1
+    imageUrl: string;
+    angle: number;             // 拍摄角度
+  }>;
+  previewUrl: string;          // 预览链接
+  downloadUrl: string;         // 下载链接
+  shareLink: string;           // 分享链接
+  fileSize: number;
+  status: 'processing' | 'completed' | 'failed';
+}
+```
+
+### 3.3 自动生成分享链接
+
+```typescript
+// project-detail.ts lines 4330-4360
+private generatePanoramicShareLink(synthesis: PanoramicSynthesis): void {
+  // 生成唯一分享链接
+  const linkId = btoa(`panoramic-${synthesis.id}-${Date.now()}`);
+  const shareLink = `https://vr.example.com/view/${linkId}`;
+
+  synthesis.shareLink = shareLink;
+
+  console.log('🔗 已生成分享链接:', shareLink);
+
+  // 自动复制到剪贴板
+  this.copyToClipboard(shareLink);
+
+  // 通知客服发送给客户
+  this.notifyCustomerServiceForPanoramicShare(synthesis);
+
+  alert(`✅ 分享链接已生成并复制到剪贴板!\n\n${shareLink}\n\n客服已收到通知,可发送给客户。`);
+}
+
+private notifyCustomerServiceForPanoramicShare(synthesis: PanoramicSynthesis): void {
+  const notification = {
+    type: 'panoramic-ready',
+    projectId: this.projectId,
+    projectName: this.project?.name || '未命名项目',
+    shareLink: synthesis.shareLink,
+    spaceCount: synthesis.spaces.length,
+    message: `项目"${this.project?.name}"的全景图已合成完成,请发送给客户查看`
+  };
+
+  console.log('📧 已通知客服发送全景图:', notification);
+}
+```
+
+**分享链接特点**:
+- 唯一性标识
+- 有效期控制(可选)
+- 访问统计
+- VR模式支持
+
+## 4. 客户评价模块
+
+### 4.1 功能特点
+- 多维度评分系统
+- 评价链接自动生成
+- 30天有效期
+- 数据统计分析
+
+### 4.2 评价链接生成
+
+#### 4.2.1 生成评价令牌
+```typescript
+// 生成客户评价链接
+generateReviewLink(): void {
+  console.log('📋 生成客户评价链接');
+
+  // 生成唯一评价令牌
+  const token = this.generateUniqueToken();
+
+  // 创建评价链接记录
+  const reviewLink: CustomerReviewLink = {
+    id: `review-link-${Date.now()}`,
+    projectId: this.projectId,
+    token: token,
+    link: `https://review.example.com/${token}`,
+    createdAt: new Date(),
+    expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后过期
+    status: 'active',
+    submittedAt: undefined
+  };
+
+  this.customerReviewLink = reviewLink;
+
+  // 复制到剪贴板
+  this.copyToClipboard(reviewLink.link);
+
+  // 通知客服
+  this.notifyCustomerServiceForReview(reviewLink);
+
+  console.log('✅ 评价链接已生成:', reviewLink);
+
+  alert(`✅ 评价链接已生成并复制到剪贴板!\n\n${reviewLink.link}\n\n有效期: 30天\n客服已收到通知,可发送给客户。`);
+}
+```
+
+**评价链接结构**:
+```typescript
+interface CustomerReviewLink {
+  id: string;
+  projectId: string;
+  token: string;                // 唯一令牌
+  link: string;                 // 完整链接
+  createdAt: Date;
+  expiresAt: Date;              // 过期时间
+  status: 'active' | 'submitted' | 'expired';
+  submittedAt?: Date;
+}
+```
+
+#### 4.2.2 生成唯一令牌
+```typescript
+private generateUniqueToken(): string {
+  const timestamp = Date.now().toString(36);
+  const randomStr = Math.random().toString(36).substring(2, 15);
+  const projectIdHash = btoa(this.projectId).substring(0, 8);
+
+  return `${timestamp}-${randomStr}-${projectIdHash}`;
+}
+```
+
+### 4.3 评价数据结构
+
+```typescript
+interface CustomerReview {
+  id: string;
+  projectId: string;
+  submittedAt: Date;
+
+  // 多维度评分 (1-5星)
+  ratings: {
+    overall: number;            // 整体满意度
+    timeliness: number;         // 及时性
+    quality: number;            // 设计质量
+    communication: number;      // 沟通效率
+    professionalism: number;    // 专业程度
+  };
+
+  // 文字评价
+  comments: {
+    strengths: string;          // 优点
+    improvements: string;       // 改进建议
+    additional: string;         // 其他意见
+  };
+
+  // 推荐意愿
+  wouldRecommend: boolean;
+
+  // 附加信息
+  contact?: string;
+  permitPublish: boolean;       // 是否允许公开
+}
+```
+
+### 4.4 评价提交处理
+
+```typescript
+// 处理客户提交的评价
+onReviewSubmitted(reviewData: CustomerReview): void {
+  console.log('📝 收到客户评价:', reviewData);
+
+  // 保存评价数据
+  this.customerReviews.push(reviewData);
+
+  // 更新评价链接状态
+  if (this.customerReviewLink) {
+    this.customerReviewLink.status = 'submitted';
+    this.customerReviewLink.submittedAt = new Date();
+  }
+
+  // 计算平均分
+  this.calculateAverageRatings();
+
+  // 通知相关人员
+  this.notifyReviewReceived(reviewData);
+
+  console.log('✅ 客户评价已保存');
+
+  alert('✅ 感谢客户的宝贵评价!\n\n评价数据已保存并通知相关人员。');
+}
+```
+
+### 4.5 评价数据分析
+
+```typescript
+// 计算平均评分
+private calculateAverageRatings(): void {
+  if (this.customerReviews.length === 0) {
+    this.averageRatings = {
+      overall: 0,
+      timeliness: 0,
+      quality: 0,
+      communication: 0,
+      professionalism: 0
+    };
+    return;
+  }
+
+  const sum = this.customerReviews.reduce((acc, review) => ({
+    overall: acc.overall + review.ratings.overall,
+    timeliness: acc.timeliness + review.ratings.timeliness,
+    quality: acc.quality + review.ratings.quality,
+    communication: acc.communication + review.ratings.communication,
+    professionalism: acc.professionalism + review.ratings.professionalism
+  }), {
+    overall: 0,
+    timeliness: 0,
+    quality: 0,
+    communication: 0,
+    professionalism: 0
+  });
+
+  const count = this.customerReviews.length;
+
+  this.averageRatings = {
+    overall: Math.round((sum.overall / count) * 10) / 10,
+    timeliness: Math.round((sum.timeliness / count) * 10) / 10,
+    quality: Math.round((sum.quality / count) * 10) / 10,
+    communication: Math.round((sum.communication / count) * 10) / 10,
+    professionalism: Math.round((sum.professionalism / count) * 10) / 10
+  };
+
+  console.log('📊 平均评分已更新:', this.averageRatings);
+}
+```
+
+## 5. 投诉处理模块
+
+### 5.1 功能特点
+- 人工创建投诉
+- 关键词自动抓取
+- 智能标注问题类型
+- 处理进度跟踪
+
+### 5.2 人工创建投诉
+
+#### 5.2.1 创建投诉记录
+```typescript
+// 人工创建投诉记录
+createComplaintManually(): void {
+  console.log('📝 人工创建投诉记录');
+
+  // 验证权限
+  if (!this.isTeamLeaderView() && !this.isCustomerServiceView()) {
+    alert('⚠️ 仅组长和客服可以创建投诉记录');
+    return;
+  }
+
+  // 打开投诉创建表单
+  this.showComplaintForm = true;
+  this.complaintFormData = {
+    source: 'manual',
+    stage: '',
+    reason: '',
+    description: '',
+    severity: 'medium',
+    tags: []
+  };
+}
+```
+
+#### 5.2.2 提交投诉
+```typescript
+// 提交投诉记录
+submitComplaint(): void {
+  if (!this.complaintFormData.reason || !this.complaintFormData.description) {
+    alert('请填写投诉原因和详细描述');
+    return;
+  }
+
+  const complaint: ComplaintRecord = {
+    id: `complaint-${Date.now()}`,
+    projectId: this.projectId,
+    source: this.complaintFormData.source,
+    stage: this.complaintFormData.stage || '未指定',
+    reason: this.complaintFormData.reason,
+    description: this.complaintFormData.description,
+    severity: this.complaintFormData.severity,
+    tags: this.complaintFormData.tags,
+    status: '待处理',
+    createdAt: new Date(),
+    createdBy: this.getCurrentUserName(),
+    assignedTo: undefined,
+    resolvedAt: undefined,
+    resolution: undefined
+  };
+
+  // 添加到投诉列表
+  this.complaints.push(complaint);
+
+  // 通知相关处理人员
+  this.notifyComplaintHandler(complaint);
+
+  // 关闭表单
+  this.showComplaintForm = false;
+
+  console.log('✅ 投诉记录已创建:', complaint);
+
+  alert('✅ 投诉记录已创建!\n\n相关人员已收到通知。');
+}
+```
+
+**投诉数据结构**:
+```typescript
+interface ComplaintRecord {
+  id: string;
+  projectId: string;
+  source: 'manual' | 'keyword-detection';    // 来源
+  stage: string;                              // 投诉环节
+  reason: string;                             // 投诉原因
+  description: string;                        // 详细描述
+  severity: 'low' | 'medium' | 'high';       // 严重程度
+  tags: string[];                             // 问题标签
+  status: '待处理' | '处理中' | '已解决' | '已关闭';
+  createdAt: Date;
+  createdBy: string;
+  assignedTo?: string;                        // 分配给
+  resolvedAt?: Date;
+  resolution?: string;                        // 解决方案
+  attachments?: Array<{
+    id: string;
+    name: string;
+    url: string;
+  }>;
+}
+```
+
+### 5.3 关键词自动监控
+
+#### 5.3.1 设置关键词监测
+```typescript
+// 启动关键词监测
+setupKeywordMonitoring(): void {
+  console.log('🔍 设置关键词监测');
+
+  // 打开监控设置面板
+  this.showKeywordMonitoringSettings = true;
+
+  // 初始化默认关键词
+  if (this.monitoringKeywords.length === 0) {
+    this.monitoringKeywords = [
+      '不满意',
+      '投诉',
+      '退款',
+      '差评',
+      '质量问题',
+      '延期',
+      '态度差'
+    ];
+  }
+
+  console.log('📋 当前监控关键词:', this.monitoringKeywords);
+}
+```
+
+#### 5.3.2 关键词检测
+```typescript
+// 检测消息中的关键词
+private detectKeywords(message: string): string[] {
+  const detectedKeywords: string[] = [];
+
+  this.monitoringKeywords.forEach(keyword => {
+    if (message.includes(keyword)) {
+      detectedKeywords.push(keyword);
+    }
+  });
+
+  return detectedKeywords;
+}
+```
+
+#### 5.3.3 自动创建投诉
+```typescript
+// 检测到关键词后自动创建投诉
+onKeywordDetected(message: string, keyword: string): void {
+  console.log(`🚨 检测到关键词: ${keyword}`);
+
+  // 智能分析投诉严重程度
+  const severity = this.assessComplaintSeverity(keyword);
+
+  // 智能识别投诉环节
+  const stage = this.identifyComplaintStage(message);
+
+  // 智能标注问题类型
+  const tags = this.generateComplaintTags(message, keyword);
+
+  // 自动创建投诉记录
+  const complaint: ComplaintRecord = {
+    id: `complaint-auto-${Date.now()}`,
+    projectId: this.projectId,
+    source: 'keyword-detection',
+    stage: stage,
+    reason: `检测到关键词: ${keyword}`,
+    description: message,
+    severity: severity,
+    tags: tags,
+    status: '待处理',
+    createdAt: new Date(),
+    createdBy: '系统自动',
+    assignedTo: undefined,
+    resolvedAt: undefined,
+    resolution: undefined
+  };
+
+  this.complaints.push(complaint);
+
+  // 立即通知处理人员
+  this.notifyUrgentComplaint(complaint);
+
+  console.log('✅ 已自动创建投诉记录:', complaint);
+
+  alert(`🚨 检测到客户投诉关键词: ${keyword}\n\n已自动创建投诉记录并通知相关人员。`);
+}
+```
+
+**智能分析方法**:
+```typescript
+// 评估投诉严重程度
+private assessComplaintSeverity(keyword: string): 'low' | 'medium' | 'high' {
+  const highSeverityKeywords = ['退款', '投诉', '差评'];
+  const mediumSeverityKeywords = ['不满意', '质量问题', '延期'];
+
+  if (highSeverityKeywords.includes(keyword)) return 'high';
+  if (mediumSeverityKeywords.includes(keyword)) return 'medium';
+  return 'low';
+}
+
+// 识别投诉环节
+private identifyComplaintStage(message: string): string {
+  if (message.includes('需求') || message.includes('沟通')) return '需求沟通';
+  if (message.includes('方案') || message.includes('设计')) return '方案确认';
+  if (message.includes('建模') || message.includes('模型')) return '建模';
+  if (message.includes('软装') || message.includes('家具')) return '软装';
+  if (message.includes('渲染') || message.includes('效果图')) return '渲染';
+  if (message.includes('交付') || message.includes('延期')) return '交付';
+  return '未识别';
+}
+
+// 生成问题标签
+private generateComplaintTags(message: string, keyword: string): string[] {
+  const tags: string[] = [];
+
+  // 根据消息内容添加标签
+  if (message.includes('需求') || message.includes('理解')) tags.push('需求理解');
+  if (message.includes('质量') || message.includes('效果')) tags.push('设计质量');
+  if (message.includes('延期') || message.includes('时间')) tags.push('交付延期');
+  if (message.includes('态度') || message.includes('服务')) tags.push('服务态度');
+  if (message.includes('价格') || message.includes('费用')) tags.push('价格问题');
+
+  // 添加关键词作为标签
+  tags.push(keyword);
+
+  return [...new Set(tags)]; // 去重
+}
+```
+
+### 5.4 投诉处理流程
+
+```typescript
+// 处理投诉
+handleComplaint(complaintId: string, resolution: string): void {
+  const complaint = this.complaints.find(c => c.id === complaintId);
+  if (!complaint) return;
+
+  complaint.status = '已解决';
+  complaint.resolvedAt = new Date();
+  complaint.resolution = resolution;
+
+  // 通知客户和相关人员
+  this.notifyComplaintResolved(complaint);
+
+  console.log('✅ 投诉已处理:', complaint);
+
+  alert('✅ 投诉处理完成!\n\n已通知客户和相关人员。');
+}
+```
+
+## 6. 项目复盘模块
+
+### 6.1 功能特点
+- 三大核心板块(SOP执行数据、经验复盘、优化建议)
+- 数据可视化展示
+- 自动生成复盘报告
+- 导出为PDF/Excel
+
+### 6.2 SOP执行数据
+
+#### 6.2.1 数据收集
+```typescript
+// 收集SOP执行数据
+collectSOPExecutionData(): any {
+  return {
+    requirementCommunications: this.countRequirementCommunications(),
+    revisionCount: this.countRevisions(),
+    deliveryCycleCompliance: this.checkDeliveryCycleCompliance(),
+    customerSatisfaction: this.getCustomerSatisfactionScore(),
+    stageDetails: this.getStageExecutionDetails()
+  };
+}
+```
+
+#### 6.2.2 阶段执行详情
+```typescript
+// 获取各阶段执行详情
+private getStageExecutionDetails(): Array<{
+  stage: string;
+  plannedDuration: number;
+  actualDuration: number;
+  status: 'on-time' | 'delayed' | 'ahead';
+  score: number;
+}> {
+  return [
+    {
+      stage: '需求沟通',
+      plannedDuration: 2,
+      actualDuration: 2,
+      status: 'on-time',
+      score: 95
+    },
+    {
+      stage: '方案确认',
+      plannedDuration: 3,
+      actualDuration: 4,
+      status: 'delayed',
+      score: 85
+    },
+    {
+      stage: '建模',
+      plannedDuration: 5,
+      actualDuration: 4,
+      status: 'ahead',
+      score: 92
+    },
+    {
+      stage: '软装',
+      plannedDuration: 3,
+      actualDuration: 3,
+      status: 'on-time',
+      score: 90
+    },
+    {
+      stage: '渲染',
+      plannedDuration: 4,
+      actualDuration: 5,
+      status: 'delayed',
+      score: 88
+    }
+  ];
+}
+```
+
+### 6.3 经验复盘
+
+#### 6.3.1 自动提取信息
+```typescript
+// 提取经验复盘数据
+extractExperienceSummary(): any {
+  return {
+    customerNeeds: this.extractCustomerNeeds(),
+    customerConcerns: this.extractCustomerConcerns(),
+    complaintPoints: this.extractComplaintPoints(),
+    projectHighlights: this.extractProjectHighlights(),
+    keyConversations: this.extractKeyConversations()
+  };
+}
+```
+
+#### 6.3.2 提取客户需求
+```typescript
+private extractCustomerNeeds(): string[] {
+  // 从需求沟通记录中提取
+  return [
+    '客户希望整体风格偏现代简约',
+    '客户重视收纳空间的设计',
+    '客户要求使用环保材料',
+    '客户希望采光效果良好'
+  ];
+}
+```
+
+### 6.4 优化建议
+
+#### 6.4.1 生成优化建议
+```typescript
+// 生成优化建议
+generateOptimizationSuggestions(): any[] {
+  const suggestions = [];
+
+  // 基于数据分析生成建议
+  const sopData = this.collectSOPExecutionData();
+
+  // 建议1:需求沟通优化
+  if (sopData.requirementCommunications > 5) {
+    suggestions.push({
+      priority: 'high',
+      priorityText: '高',
+      category: '需求沟通',
+      problem: '需求沟通次数过多(6次),影响项目效率',
+      dataSupport: `需求沟通次数: ${sopData.requirementCommunications}次,标准为3-4次`,
+      solution: '建议在首次沟通时使用标准化需求采集表,确保需求收集的完整性',
+      actionPlan: [
+        '制定标准需求采集表模板',
+        '培训设计师使用标准表单',
+        '要求首次沟通必须完成80%需求确认'
+      ],
+      expectedImprovement: '减少30%的需求沟通次数',
+      referenceCase: '参考项目#1234在使用标准表后沟通次数从6次降至3次',
+      accepted: false
+    });
+  }
+
+  // 建议2:渲染阶段优化
+  const renderingStage = sopData.stageDetails.find((s: any) => s.stage === '渲染');
+  if (renderingStage && renderingStage.status === 'delayed') {
+    suggestions.push({
+      priority: 'medium',
+      priorityText: '中',
+      category: '渲染效率',
+      problem: '渲染阶段超期1天,影响整体交付时间',
+      dataSupport: `计划4天,实际5天,超期率25%`,
+      solution: '建议提前进行渲染设备性能检查,并预留缓冲时间',
+      actionPlan: [
+        '每月检查渲染设备性能',
+        '建模完成后立即启动预渲染',
+        '渲染阶段预留20%缓冲时间'
+      ],
+      expectedImprovement: '降低渲染超期率至10%以下',
+      referenceCase: '团队B采用预渲染机制后超期率从30%降至8%',
+      accepted: false
+    });
+  }
+
+  return suggestions;
+}
+```
+
+**优化建议结构**:
+```typescript
+interface OptimizationSuggestion {
+  priority: 'high' | 'medium' | 'low';
+  priorityText: string;
+  category: string;                     // 类别
+  problem: string;                      // 问题描述
+  dataSupport: string;                  // 数据支撑
+  solution: string;                     // 解决方案
+  actionPlan: string[];                 // 行动计划
+  expectedImprovement: string;          // 预期提升
+  referenceCase?: string;               // 参考案例
+  accepted: boolean;                    // 是否已采纳
+  acceptedAt?: Date;
+}
+```
+
+### 6.5 复盘报告生成
+
+#### 6.5.1 生成报告
+```typescript
+// 生成完整复盘报告
+generateReviewReport(): void {
+  console.log('📊 生成项目复盘报告');
+
+  this.isGeneratingReview = true;
+
+  // 模拟生成进度
+  let progress = 0;
+  const interval = setInterval(() => {
+    progress += 20;
+    if (progress >= 100) {
+      clearInterval(interval);
+      this.completeReviewReportGeneration();
+    }
+  }, 500);
+}
+
+private completeReviewReportGeneration(): void {
+  // 收集所有数据
+  const reportData = {
+    projectInfo: {
+      name: this.project?.name || '未命名项目',
+      id: this.projectId,
+      startDate: this.project?.createdAt,
+      endDate: new Date()
+    },
+    sopData: this.collectSOPExecutionData(),
+    experience: this.extractExperienceSummary(),
+    suggestions: this.generateOptimizationSuggestions(),
+    statistics: {
+      overallScore: this.calculateOverallScore(),
+      strengths: this.getProjectStrengths(),
+      weaknesses: this.getProjectWeaknesses()
+    }
+  };
+
+  // 保存报告
+  this.reviewReport = reportData;
+  this.isGeneratingReview = false;
+
+  console.log('✅ 复盘报告生成完成:', reportData);
+
+  alert('✅ 项目复盘报告已生成!\n\n您可以查看详情或导出报告。');
+}
+```
+
+#### 6.5.2 导出报告
+```typescript
+// 导出复盘报告
+exportReviewReport(format: 'pdf' | 'excel'): void {
+  if (!this.reviewReport) {
+    alert('请先生成复盘报告');
+    return;
+  }
+
+  console.log(`📤 导出复盘报告 (${format})`);
+
+  if (format === 'excel') {
+    this.exportAsExcel(this.reviewReport);
+  } else {
+    this.exportAsPDF(this.reviewReport);
+  }
+}
+
+private exportAsExcel(data: any): void {
+  // 转换为CSV格式
+  let csvContent = '\uFEFF'; // UTF-8 BOM
+
+  // 项目概况
+  csvContent += '=== 项目概况 ===\n';
+  csvContent += `项目名称,${data.projectInfo.name}\n`;
+  csvContent += `项目ID,${data.projectInfo.id}\n`;
+  csvContent += `总耗时,${this.calculateProjectDuration()}天\n\n`;
+
+  // SOP执行数据
+  csvContent += '=== SOP执行数据 ===\n';
+  csvContent += '阶段,计划时长,实际时长,状态,评分\n';
+  data.sopData.stageDetails.forEach((stage: any) => {
+    csvContent += `${stage.stage},${stage.plannedDuration},${stage.actualDuration},${stage.status},${stage.score}\n`;
+  });
+
+  // 优化建议
+  csvContent += '\n=== 优化建议 ===\n';
+  csvContent += '优先级,类别,问题,建议,预期提升\n';
+  data.suggestions.forEach((s: any) => {
+    csvContent += `${s.priorityText},${s.category},"${s.problem}","${s.solution}",${s.expectedImprovement}\n`;
+  });
+
+  // 创建下载
+  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+  const link = document.createElement('a');
+  const url = URL.createObjectURL(blob);
+
+  link.href = url;
+  link.download = `项目复盘报告_${data.projectInfo.name}_${this.formatDate(new Date())}.csv`;
+  link.click();
+
+  URL.revokeObjectURL(url);
+
+  console.log('✅ 报告已导出为Excel');
+  alert('✅ 报告已导出!\n\n文件已下载到您的下载文件夹。');
+}
+```
+
+## 7. 权限控制
+
+### 7.1 角色权限矩阵
+
+| 操作 | 客服 | 设计师 | 组长 | 技术 | 财务 |
+|-----|------|--------|------|------|------|
+| 查看售后板块 | ✅ | ✅ | ✅ | ✅ | ✅ |
+| 启动自动结算 | ❌ | ❌ | ❌ | ✅ | ❌ |
+| 上传支付凭证 | ✅ | ❌ | ✅ | ❌ | ✅ |
+| 发送图片给客户 | ✅ | ❌ | ✅ | ❌ | ❌ |
+| 合成全景图 | ❌ | ❌ | ❌ | ✅ | ❌ |
+| 生成评价链接 | ✅ | ❌ | ✅ | ❌ | ❌ |
+| 创建投诉记录 | ✅ | ❌ | ✅ | ❌ | ❌ |
+| 处理投诉 | ✅ | ❌ | ✅ | ❌ | ❌ |
+| 生成复盘报告 | ❌ | ❌ | ✅ | ✅ | ❌ |
+| 导出复盘报告 | ❌ | ❌ | ✅ | ✅ | ✅ |
+
+### 7.2 权限检查实现
+
+```typescript
+// 检查尾款结算权限
+canInitiateSettlement(): boolean {
+  return this.isTechnicalView();
+}
+
+// 检查全景图合成权限
+canSynthesizePanoramic(): boolean {
+  return this.isTechnicalView();
+}
+
+// 检查投诉处理权限
+canHandleComplaints(): boolean {
+  return this.isTeamLeaderView() || this.isCustomerServiceView();
+}
+
+// 检查复盘报告权限
+canGenerateReviewReport(): boolean {
+  return this.isTeamLeaderView() || this.isTechnicalView();
+}
+```
+
+## 8. 数据流转
+
+### 8.1 售后流程总览
+
+```mermaid
+sequenceDiagram
+    participant Tech as 技术
+    participant System as 系统
+    participant Payment as 支付网关
+    participant CS as 客服
+    participant Customer as 客户
+
+    Tech->>System: 启动自动结算
+    System->>Payment: 激活支付监听
+    System->>CS: 通知跟进尾款
+    CS->>Customer: 发送支付请求
+    Customer->>Payment: 完成支付
+    Payment->>System: 支付通知
+    System->>System: 解锁渲染大图
+    System->>CS: 通知发送大图
+    CS->>Customer: 发送渲染大图
+    System->>CS: 生成评价链接
+    CS->>Customer: 发送评价链接
+    Customer->>System: 提交评价
+    System->>Tech: 生成复盘报告
+```
+
+### 8.2 数据同步机制
+
+```typescript
+// 售后数据同步到项目
+private syncAfterCareDataToProject(): void {
+  if (!this.project) return;
+
+  this.project.afterCare = {
+    settlement: this.settlementRecord,
+    panoramic: this.panoramicSynthesisHistory,
+    reviews: this.customerReviews,
+    complaints: this.complaints,
+    reviewReport: this.reviewReport
+  };
+
+  // 同步到服务器
+  this.projectService.updateProject(this.project).subscribe({
+    next: (result) => {
+      console.log('✅ 售后数据已同步到项目');
+    },
+    error: (error) => {
+      console.error('❌ 售后数据同步失败:', error);
+    }
+  });
+}
+```
+
+## 9. 异常处理
+
+### 9.1 支付监听失败
+
+```typescript
+// 支付监听连接失败处理
+private handlePaymentMonitoringError(error: any): void {
+  console.error('支付监听连接失败:', error);
+
+  // 降级为手动模式
+  this.miniprogramPaymentStatus = 'error';
+
+  alert(`⚠️ 支付自动监听失败\n\n请使用"上传支付凭证"功能手动确认支付。`);
+
+  // 显示手动上传入口
+  this.showManualPaymentVoucherUpload = true;
+}
+```
+
+### 9.2 全景图合成失败
+
+```typescript
+// 全景图合成失败处理
+private handlePanoramicSynthesisError(error: any): void {
+  console.error('全景图合成失败:', error);
+
+  this.isSynthesizingPanoramic = false;
+
+  let errorMessage = '全景图合成失败';
+
+  if (error.code === 'INSUFFICIENT_IMAGES') {
+    errorMessage = '图片数量不足,至少需要6张图片';
+  } else if (error.code === 'INVALID_FORMAT') {
+    errorMessage = '图片格式不支持,请使用JPG或PNG格式';
+  }
+
+  alert(`❌ ${errorMessage}\n\n请检查后重试。`);
+}
+```
+
+### 9.3 评价链接过期
+
+```typescript
+// 检查评价链接是否过期
+checkReviewLinkExpiry(linkId: string): boolean {
+  const link = this.customerReviewLinks.find(l => l.id === linkId);
+
+  if (!link) return true;
+
+  if (link.status === 'expired') return true;
+
+  // 检查是否超过有效期
+  if (new Date() > link.expiresAt) {
+    link.status = 'expired';
+    return true;
+  }
+
+  return false;
+}
+
+// 重新生成过期的评价链接
+regenerateReviewLink(oldLinkId: string): void {
+  const oldLink = this.customerReviewLinks.find(l => l.id === oldLinkId);
+
+  if (!oldLink) return;
+
+  // 将旧链接标记为过期
+  oldLink.status = 'expired';
+
+  // 生成新链接
+  this.generateReviewLink();
+
+  alert('✅ 已重新生成评价链接!\n\n旧链接已失效,请使用新链接。');
+}
+```
+
+## 10. 性能优化
+
+### 10.1 报告生成优化
+
+```typescript
+// 使用Worker生成大型报告
+private generateReportWithWorker(data: any): void {
+  if (typeof Worker !== 'undefined') {
+    const worker = new Worker(new URL('./report-generator.worker', import.meta.url));
+
+    worker.onmessage = ({ data }) => {
+      console.log('报告生成完成:', data);
+      this.reviewReport = data.report;
+      this.isGeneratingReview = false;
+    };
+
+    worker.postMessage({ type: 'generate', data });
+  } else {
+    // 降级为同步生成
+    this.generateReportSync(data);
+  }
+}
+```
+
+### 10.2 图片压缩
+
+```typescript
+// 压缩全景图用于预览
+private compressImageForPreview(file: File): Promise<Blob> {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+
+    reader.onload = (e) => {
+      const img = new Image();
+      img.onload = () => {
+        const canvas = document.createElement('canvas');
+        const ctx = canvas.getContext('2d');
+
+        // 压缩到最大1920px
+        const maxDimension = 1920;
+        const scale = Math.min(maxDimension / img.width, maxDimension / img.height, 1);
+
+        canvas.width = img.width * scale;
+        canvas.height = img.height * scale;
+
+        ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
+
+        canvas.toBlob((blob) => {
+          if (blob) {
+            resolve(blob);
+          } else {
+            reject(new Error('压缩失败'));
+          }
+        }, 'image/jpeg', 0.8);
+      };
+
+      img.src = e.target?.result as string;
+    };
+
+    reader.readAsDataURL(file);
+  });
+}
+```
+
+### 10.3 数据缓存
+
+```typescript
+// 缓存复盘报告数据
+private cacheReviewReport(report: any): void {
+  try {
+    localStorage.setItem(
+      `review-report-${this.projectId}`,
+      JSON.stringify(report)
+    );
+    console.log('✅ 复盘报告已缓存');
+  } catch (error) {
+    console.warn('缓存失败:', error);
+  }
+}
+
+// 加载缓存的报告
+private loadCachedReviewReport(): any | null {
+  try {
+    const cached = localStorage.getItem(`review-report-${this.projectId}`);
+    if (cached) {
+      return JSON.parse(cached);
+    }
+  } catch (error) {
+    console.warn('加载缓存失败:', error);
+  }
+  return null;
+}
+```
+
+## 11. 测试用例
+
+### 11.1 尾款结算测试
+
+```typescript
+describe('Final Payment Settlement', () => {
+  it('should initiate auto settlement by technical user', () => {
+    component.roleContext = 'technical';
+    spyOn(component, 'isAllDeliveryCompleted').and.returnValue(true);
+
+    component.initiateAutoSettlement();
+
+    expect(component.miniprogramPaymentStatus).toBe('active');
+    expect(component.settlementRecord).toBeDefined();
+  });
+
+  it('should reject non-technical users', () => {
+    component.roleContext = 'designer';
+    spyOn(window, 'alert');
+
+    component.initiateAutoSettlement();
+
+    expect(window.alert).toHaveBeenCalledWith(jasmine.stringContaining('仅技术人员'));
+  });
+
+  it('should unlock images after payment received', () => {
+    component.renderLargeImages = [
+      { id: '1', name: 'img1.jpg', url: 'blob:1', locked: true },
+      { id: '2', name: 'img2.jpg', url: 'blob:2', locked: true }
+    ];
+
+    component.onPaymentReceived({ amount: 75000, method: 'wechat' });
+
+    expect(component.renderLargeImages.every(img => !img.locked)).toBe(true);
+  });
+});
+```
+
+### 11.2 投诉处理测试
+
+```typescript
+describe('Complaint Handling', () => {
+  it('should create complaint manually', () => {
+    component.roleContext = 'team-leader';
+    component.complaintFormData = {
+      source: 'manual',
+      stage: '渲染',
+      reason: '质量问题',
+      description: '渲染效果不符合预期',
+      severity: 'medium',
+      tags: ['设计质量']
+    };
+
+    component.submitComplaint();
+
+    expect(component.complaints.length).toBeGreaterThan(0);
+  });
+
+  it('should detect keywords and create complaint', () => {
+    const message = '我对渲染效果很不满意,要求退款';
+
+    component.monitoringKeywords = ['不满意', '退款'];
+
+    component.onKeywordDetected(message, '不满意');
+
+    expect(component.complaints.length).toBeGreaterThan(0);
+    expect(component.complaints[0].severity).toBe('high');
+  });
+});
+```
+
+### 11.3 复盘报告测试
+
+```typescript
+describe('Review Report Generation', () => {
+  it('should generate complete review report', () => {
+    component.generateReviewReport();
+
+    // Wait for generation
+    setTimeout(() => {
+      expect(component.reviewReport).toBeDefined();
+      expect(component.reviewReport.sopData).toBeDefined();
+      expect(component.reviewReport.experience).toBeDefined();
+      expect(component.reviewReport.suggestions.length).toBeGreaterThan(0);
+    }, 3000);
+  });
+
+  it('should export report as Excel', () => {
+    component.reviewReport = mockReviewReport;
+
+    spyOn(document, 'createElement').and.callThrough();
+
+    component.exportReviewReport('excel');
+
+    expect(document.createElement).toHaveBeenCalledWith('a');
+  });
+});
+```
+
+---
+
+**文档版本**:v1.0.0
+**创建日期**:2025-10-16
+**最后更新**:2025-10-16
+**维护人**:产品团队
+
+**相关文档**:
+- [AFTERCARE-FEATURES-README.md](/home/ryan/workspace/nova/yss-project/src/app/pages/designer/project-detail/AFTERCARE-FEATURES-README.md) - 售后模块功能实现说明

+ 1297 - 0
docs/prd/项目-订单分配.md

@@ -0,0 +1,1297 @@
+# 项目管理 - 订单分配阶段 PRD
+
+## 1. 功能概述
+
+### 1.1 阶段定位
+订单分配阶段是项目管理流程的第一个环节,主要负责将客户咨询转化为正式项目订单,并完成设计师团队的初步分配。该阶段是连接客服端和设计师端的关键桥梁。
+
+### 1.2 核心目标
+- 完成客户信息的结构化录入和同步
+- 确定项目报价和付款条件
+- 匹配并分配合适的设计师资源
+- 建立项目基础档案,为后续环节提供数据支撑
+
+### 1.3 涉及角色
+- **客服人员**:负责创建订单、录入客户信息、初步需求沟通
+- **设计师**:接收订单分配、查看项目基础信息
+- **组长**:查看团队订单分配情况、协调设计师资源
+
+## 2. 核心功能模块
+
+### 2.1 客户信息管理
+
+#### 2.1.1 信息展示卡片
+**位置**:订单分配阶段左侧面板
+
+**展示内容**:
+```typescript
+interface CustomerInfoDisplay {
+  // 基础信息
+  name: string;           // 客户姓名
+  phone: string;          // 联系电话
+  wechat?: string;        // 微信号
+  customerType: string;   // 客户类型:新客户/老客户/VIP客户
+  source: string;         // 来源:小程序/官网咨询/推荐介绍
+  remark?: string;        // 备注信息
+
+  // 状态信息
+  syncStatus: 'syncing' | 'synced' | 'error'; // 同步状态
+  lastSyncTime?: Date;    // 最后同步时间
+
+  // 需求标签
+  demandType?: string;    // 需求类型:价格敏感/质量敏感/综合要求
+  followUpStatus?: string; // 跟进状态:待报价/待确认需求/已失联
+  preferenceTags?: string[]; // 偏好标签数组
+}
+```
+
+**数据来源**:
+1. **客服端同步**:通过路由查询参数 `syncData` 传递客户信息
+   ```typescript
+   // 客服端跳转示例
+   router.navigate(['/designer/project-detail', projectId], {
+     queryParams: {
+       syncData: JSON.stringify({
+         customerInfo: {...},
+         requirementInfo: {...},
+         preferenceTags: [...]
+       })
+     }
+   });
+   ```
+
+2. **实时同步机制**:
+   - 每30秒自动同步一次客户信息
+   - 显示同步状态指示器(同步中/已同步/同步失败)
+   - 支持手动触发同步
+
+**交互特性**:
+- 卡片可展开/收起(`isCustomerInfoExpanded`)
+- 展开时显示完整客户信息和标签
+- 收起时仅显示客户姓名和联系方式
+- 同步状态实时更新,显示"刚刚/X分钟前/X小时前"
+
+#### 2.1.2 客户搜索功能
+**适用场景**:手动创建订单时快速选择已有客户
+
+**搜索逻辑**:
+```typescript
+searchCustomer(): void {
+  // 至少输入2个字符才触发搜索
+  if (this.customerSearchKeyword.trim().length >= 2) {
+    // 模糊搜索客户姓名、手机号、微信号
+    this.customerSearchResults = this.customerService.search({
+      keyword: this.customerSearchKeyword,
+      fields: ['name', 'phone', 'wechat']
+    });
+  }
+}
+```
+
+**搜索结果展示**:
+- 下拉列表形式
+- 每项显示:客户姓名、电话(脱敏)、客户类型、来源
+- 点击选择后自动填充表单
+
+### 2.2 核心需求表单
+
+#### 2.2.1 必填项配置
+**表单定义**:
+```typescript
+orderCreationForm = this.fb.group({
+  orderAmount: ['', [Validators.required, Validators.min(0)]],
+  smallImageDeliveryTime: ['', Validators.required],
+  decorationType: ['', Validators.required],
+  requirementReason: ['', Validators.required],
+  isMultiDesigner: [false]
+});
+```
+
+**字段详解**:
+
+| 字段名 | 类型 | 验证规则 | 说明 | UI组件 |
+|-------|------|---------|------|--------|
+| `orderAmount` | number | required, min(0) | 订单金额,单位:元 | 数字输入框,支持千分位格式化 |
+| `smallImageDeliveryTime` | Date | required | 小图交付时间 | 日期选择器,限制最早日期为今天 |
+| `decorationType` | string | required | 装修类型:全包/半包/清包/软装 | 下拉选择框 |
+| `requirementReason` | string | required | 需求原因:新房装修/旧房改造/局部翻新 | 单选框组 |
+| `isMultiDesigner` | boolean | - | 是否需要多设计师协作 | 复选框 |
+
+**表单验证提示**:
+- 实时验证:失焦时触发
+- 错误提示:红色边框 + 底部错误文字
+- 提交验证:点击"分配订单"按钮时调用 `markAllAsTouched()` 显示所有错误
+
+#### 2.2.2 可选信息表单
+**折叠面板设计**:
+```html
+<div class="optional-info-section">
+  <div class="section-header" (click)="isOptionalFormExpanded = !isOptionalFormExpanded">
+    <span>可选信息</span>
+    <span class="toggle-icon">{{ isOptionalFormExpanded ? '▼' : '▶' }}</span>
+  </div>
+  @if (isOptionalFormExpanded) {
+    <div class="section-content">
+      <!-- 可选字段表单 -->
+    </div>
+  }
+</div>
+```
+
+**可选字段**:
+```typescript
+optionalForm = this.fb.group({
+  largeImageDeliveryTime: [''],      // 大图交付时间
+  spaceRequirements: [''],           // 空间需求描述
+  designAngles: [''],                // 设计角度要求
+  specialAreaHandling: [''],         // 特殊区域处理说明
+  materialRequirements: [''],        // 材质要求
+  lightingRequirements: ['']         // 光照需求
+});
+```
+
+### 2.3 报价明细组件
+
+#### 2.3.1 组件集成
+**组件标签**:
+```html
+<app-quotation-details
+  [initialData]="quotationData"
+  [readonly]="isReadOnly()"
+  (dataChange)="onQuotationDataChange($event)">
+</app-quotation-details>
+```
+
+**数据结构**:
+```typescript
+interface QuotationData {
+  items: Array<{
+    id: string;
+    room: string;           // 空间名称:客餐厅/主卧/次卧/厨房/卫生间
+    amount: number;         // 金额
+    description?: string;   // 描述
+  }>;
+  totalAmount: number;      // 总金额
+  materialCost: number;     // 材料费
+  laborCost: number;        // 人工费
+  designFee: number;        // 设计费
+  managementFee: number;    // 管理费
+}
+```
+
+#### 2.3.2 组件功能
+1. **报价项管理**:
+   - 添加报价项:按空间/按项目添加
+   - 删除报价项:确认后删除
+   - 编辑金额:实时更新总金额
+
+2. **AI辅助生成**(可选功能):
+   ```typescript
+   generateQuotationDetails(): void {
+     // 基于项目信息自动生成报价明细
+     const rooms = ['客餐厅', '主卧', '次卧', '厨房', '卫生间'];
+     this.quotationDetails = rooms.map((room, index) => ({
+       id: `quote_${index + 1}`,
+       room: room,
+       amount: Math.floor(Math.random() * 1000) + 300,
+       description: `${room}装修设计费用`
+     }));
+     this.orderAmount = this.quotationDetails.reduce((total, item) => total + item.amount, 0);
+   }
+   ```
+
+3. **金额自动汇总**:
+   - 实时计算总金额
+   - 费用分类占比显示
+   - 支持导出报价单(PDF/Excel)
+
+### 2.4 设计师指派组件
+
+#### 2.4.1 组件集成
+**组件标签**:
+```html
+<app-designer-assignment
+  [projectData]="projectData"
+  [readonly]="isReadOnly()"
+  (assignmentChange)="onDesignerAssignmentChange($event)"
+  (designerClick)="onDesignerClick($event)">
+</app-designer-assignment>
+```
+
+**数据结构**:
+```typescript
+interface DesignerAssignmentData {
+  selectedDesigners: Designer[];
+  teamId: string;
+  teamName: string;
+  leaderId: string;
+  assignmentDate: Date;
+  expectedStartDate: Date;
+}
+
+interface Designer {
+  id: string;
+  name: string;
+  avatar: string;
+  teamId: string;
+  teamName: string;
+  isTeamLeader: boolean;
+  status: 'idle' | 'busy' | 'unavailable';
+  currentProjects: number;
+  skillMatch: number;        // 技能匹配度 0-100
+  recentOrders: number;      // 近期订单数
+  idleDays: number;          // 闲置天数
+  workload: number;          // 工作负荷 0-100
+  reviewDates: string[];     // 对图评审日期列表
+}
+```
+
+#### 2.4.2 设计师选择逻辑
+**智能推荐算法**:
+```typescript
+calculateDesignerScore(designer: Designer): number {
+  let score = 0;
+
+  // 1. 技能匹配度(权重40%)
+  score += designer.skillMatch * 0.4;
+
+  // 2. 工作负荷(权重30%,负荷越低分数越高)
+  score += (100 - designer.workload) * 0.3;
+
+  // 3. 闲置时间(权重20%,闲置越久分数越高)
+  score += Math.min(designer.idleDays * 2, 100) * 0.2;
+
+  // 4. 近期接单数(权重10%,接单越少分数越高)
+  score += Math.max(0, 100 - designer.recentOrders * 10) * 0.1;
+
+  return score;
+}
+```
+
+**设计师列表展示**:
+- 卡片网格布局,每行3-4个设计师卡片
+- 卡片信息:头像、姓名、团队、状态标签、技能匹配度进度条
+- 状态颜色:
+  - `idle` 空闲 - 绿色
+  - `busy` 繁忙 - 橙色
+  - `unavailable` 不可用 - 灰色
+- 点击卡片可查看设计师详细日历
+
+#### 2.4.3 设计师日历弹窗
+**触发条件**:点击设计师卡片时
+
+**弹窗内容**:
+```html
+<app-designer-calendar
+  [designers]="[selectedDesigner]"
+  [groups]="calendarGroups"
+  [selectedDate]="selectedCalendarDate"
+  (designerSelected)="onCalendarDesignerSelected($event)"
+  (assignmentRequested)="onCalendarAssignmentRequested($event)">
+</app-designer-calendar>
+```
+
+**日历功能**:
+- 月视图展示设计师日程
+- 标记对图评审日期(红点)
+- 显示已分配项目时间段
+- 计算下一个可用日期
+- 支持按日期筛选空闲设计师
+
+### 2.5 下单时间自动生成
+
+**实现逻辑**:
+```typescript
+ngOnInit(): void {
+  // 自动生成下单时间
+  this.orderTime = new Date().toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  });
+}
+```
+
+**显示格式**:`2025-10-16 14:30:25`
+
+**用途**:
+- 记录订单创建的准确时间
+- 作为项目开始时间的参考
+- 用于计算项目周期和延期预警
+
+## 3. 数据流转
+
+### 3.1 客服端同步流程
+
+```mermaid
+sequenceDiagram
+    participant CS as 客服端
+    participant Route as 路由
+    participant PD as 项目详情页
+    participant Form as 订单表单
+
+    CS->>Route: navigate with syncData
+    Route->>PD: queryParams.syncData
+    PD->>PD: parseJSON(syncData)
+    PD->>Form: patchValue(customerInfo)
+    PD->>PD: syncRequirementKeyInfo(requirementInfo)
+    PD->>PD: 更新 projectData
+    PD->>PD: 触发 cdr.detectChanges()
+    PD-->>CS: 同步完成
+```
+
+**关键代码实现**:
+```typescript
+// project-detail.ts lines 741-816
+this.route.queryParamMap.subscribe({
+  next: (qp) => {
+    const syncDataParam = qp.get('syncData');
+    if (syncDataParam) {
+      try {
+        const syncData = JSON.parse(syncDataParam);
+
+        // 设置同步状态
+        this.isSyncingCustomerInfo = true;
+
+        // 存储订单分配数据用于显示
+        this.orderCreationData = syncData;
+
+        // 同步客户信息到表单
+        if (syncData.customerInfo) {
+          this.customerForm.patchValue({
+            name: syncData.customerInfo.name || '',
+            phone: syncData.customerInfo.phone || '',
+            wechat: syncData.customerInfo.wechat || '',
+            customerType: syncData.customerInfo.customerType || '新客户',
+            source: syncData.customerInfo.source || '小程序',
+            remark: syncData.customerInfo.remark || ''
+          });
+        }
+
+        // 同步需求信息
+        if (syncData.requirementInfo) {
+          this.syncRequirementKeyInfo(syncData.requirementInfo);
+        }
+
+        // 同步偏好标签
+        if (syncData.preferenceTags) {
+          this.project.customerTags = syncData.preferenceTags;
+        }
+
+        // 模拟同步完成
+        setTimeout(() => {
+          this.isSyncingCustomerInfo = false;
+          this.lastSyncTime = new Date();
+          this.cdr.detectChanges();
+        }, 1500);
+
+      } catch (error) {
+        console.error('解析同步数据失败:', error);
+        this.isSyncingCustomerInfo = false;
+      }
+    }
+  }
+});
+```
+
+### 3.2 订单创建流程
+
+```mermaid
+flowchart TD
+    A[客服/设计师填写表单] --> B{表单验证}
+    B -->|验证失败| C[显示错误提示]
+    C --> A
+    B -->|验证成功| D[调用 createOrder]
+    D --> E[整合表单数据]
+    E --> F[整合报价数据]
+    F --> G[整合设计师分配数据]
+    G --> H[调用 ProjectService.createProject]
+    H --> I{API响应}
+    I -->|成功| J[显示成功提示]
+    J --> K[推进到需求沟通阶段]
+    K --> L[展开需求沟通面板]
+    L --> M[滚动到需求沟通区域]
+    I -->|失败| N[显示错误信息]
+```
+
+**关键方法实现**:
+```typescript
+// project-detail.ts lines 4783-4808
+createOrder(): void {
+  if (!this.canCreateOrder()) {
+    // 标记所有字段为已触摸,以显示验证错误
+    this.orderCreationForm.markAllAsTouched();
+    return;
+  }
+
+  const orderData = {
+    ...this.orderCreationForm.value,
+    ...this.optionalForm.value,
+    customerInfo: this.orderCreationData?.customerInfo,
+    quotationData: this.quotationData,
+    designerAssignment: this.designerAssignmentData
+  };
+
+  console.log('分配订单:', orderData);
+
+  // 调用 ProjectService 创建项目
+  this.projectService.createProject(orderData).subscribe({
+    next: (result) => {
+      if (result.success) {
+        alert('订单分配成功!');
+        // 订单分配成功后自动切换到下一环节
+        this.advanceToNextStage('订单分配');
+      }
+    },
+    error: (error) => {
+      console.error('订单分配失败:', error);
+      alert('订单分配失败,请重试');
+    }
+  });
+}
+```
+
+### 3.3 阶段推进机制
+
+**推进触发条件**:
+- 订单分配完成(必填项填写 + 报价确认 + 设计师分配)
+- 点击"分配订单"按钮并验证通过
+- 或者通过 `onProjectCreated` 事件自动推进
+
+**推进逻辑**:
+```typescript
+// project-detail.ts lines 1391-1423
+advanceToNextStage(afterStage: ProjectStage): void {
+  const idx = this.stageOrder.indexOf(afterStage);
+  if (idx >= 0 && idx < this.stageOrder.length - 1) {
+    const next = this.stageOrder[idx + 1];
+
+    // 更新项目阶段
+    this.updateProjectStage(next);
+
+    // 更新展开状态,折叠当前、展开下一阶段
+    this.expandedStages[afterStage] = false;
+    this.expandedStages[next] = true;
+
+    // 更新板块展开状态
+    const nextSection = this.getSectionKeyForStage(next);
+    this.expandedSection = nextSection;
+
+    // 触发变更检测以更新导航栏颜色
+    this.cdr.detectChanges();
+  }
+}
+
+// project-detail.ts lines 3038-3069
+onProjectCreated(projectData: any): void {
+  console.log('项目创建完成:', projectData);
+  this.projectData = projectData;
+
+  // 团队分配已在子组件中完成并触发该事件:推进到需求沟通阶段
+  this.updateProjectStage('需求沟通');
+
+  // 更新项目对象的当前阶段,确保四大板块状态正确显示
+  if (this.project) {
+    this.project.currentStage = '需求沟通';
+  }
+
+  // 展开需求沟通阶段,收起订单分配阶段
+  this.expandedStages['需求沟通'] = true;
+  this.expandedStages['订单分配'] = false;
+
+  // 自动展开确认需求板块
+  this.expandedSection = 'requirements';
+
+  // 强制触发变更检测,确保UI更新
+  this.cdr.detectChanges();
+
+  // 延迟滚动到需求沟通阶段,确保DOM更新完成
+  setTimeout(() => {
+    this.scrollToStage('需求沟通');
+    this.cdr.detectChanges();
+  }, 100);
+}
+```
+
+## 4. 权限控制
+
+### 4.1 角色权限矩阵
+
+| 操作 | 客服 | 设计师 | 组长 | 技术 |
+|-----|------|--------|------|------|
+| 查看订单分配阶段 | ✅ | ✅ | ✅ | ✅ |
+| 创建订单 | ✅ | ❌ | ✅ | ❌ |
+| 编辑客户信息 | ✅ | ❌ | ✅ | ❌ |
+| 填写报价明细 | ✅ | ❌ | ✅ | ❌ |
+| 选择设计师 | ✅ | ❌ | ✅ | ❌ |
+| 接收订单通知 | ❌ | ✅ | ✅ | ❌ |
+| 查看设计师日历 | ✅ | ✅ | ✅ | ❌ |
+| 推进到下一阶段 | ✅ | ❌ | ✅ | ❌ |
+
+### 4.2 权限检查方法
+
+```typescript
+// project-detail.ts lines 890-936
+private detectRoleContextFromUrl(): 'customer-service' | 'designer' | 'team-leader' | 'technical' {
+  const url = this.router.url || '';
+
+  // 首先检查查询参数中的role
+  const queryParams = this.route.snapshot.queryParamMap;
+  const roleParam = queryParams.get('roleName');
+  if (roleParam === 'customer-service') {
+    return 'customer-service';
+  }
+  if (roleParam === 'technical') {
+    return 'technical';
+  }
+
+  // 如果没有role查询参数,则根据URL路径判断
+  if (url.includes('/customer-service/')) return 'customer-service';
+  if (url.includes('/team-leader/')) return 'team-leader';
+  if (url.includes('/technical/')) return 'technical';
+  return 'designer';
+}
+
+isDesignerView(): boolean { return this.roleContext === 'designer'; }
+isTeamLeaderView(): boolean { return this.roleContext === 'team-leader'; }
+isCustomerServiceView(): boolean { return this.roleContext === 'customer-service'; }
+isTechnicalView(): boolean { return this.roleContext === 'technical'; }
+isReadOnly(): boolean { return this.isCustomerServiceView(); }
+
+canEditSection(sectionKey: SectionKey): boolean {
+  if (this.isCustomerServiceView()) {
+    return sectionKey === 'order' || sectionKey === 'requirements' || sectionKey === 'aftercare';
+  }
+  return true; // 设计师和组长可以编辑所有板块
+}
+
+canEditStage(stage: ProjectStage): boolean {
+  if (this.isCustomerServiceView()) {
+    const editableStages: ProjectStage[] = [
+      '订单分配', '需求沟通', '方案确认', // 订单分配和确认需求板块
+      '尾款结算', '客户评价', '投诉处理' // 售后板块
+    ];
+    return editableStages.includes(stage);
+  }
+  return true; // 设计师和组长可以编辑所有阶段
+}
+```
+
+### 4.3 UI权限控制
+
+**模板权限指令**:
+```html
+<!-- 只读模式:客服查看设计师视角 -->
+@if (isReadOnly()) {
+  <div class="readonly-banner">
+    当前为只读模式,您可以查看项目信息但无法编辑
+  </div>
+}
+
+<!-- 编辑权限检查:表单禁用状态 -->
+<app-quotation-details
+  [readonly]="!canEditSection('order')"
+  ...>
+</app-quotation-details>
+
+<!-- 按钮显示控制:只有客服和组长可以看到分配订单按钮 -->
+@if (canEditSection('order')) {
+  <button
+    class="btn-primary"
+    [disabled]="!canCreateOrder()"
+    (click)="createOrder()">
+    分配订单
+  </button>
+}
+```
+
+## 5. 关键交互设计
+
+### 5.1 表单验证交互
+
+**实时验证**:
+```typescript
+// 失焦时触发验证
+<input
+  formControlName="orderAmount"
+  (blur)="orderCreationForm.get('orderAmount')?.markAsTouched()"
+  ...>
+
+// 错误信息显示
+@if (orderCreationForm.get('orderAmount')?.invalid &&
+      orderCreationForm.get('orderAmount')?.touched) {
+  <div class="error-message">
+    @if (orderCreationForm.get('orderAmount')?.errors?.['required']) {
+      订单金额不能为空
+    }
+    @if (orderCreationForm.get('orderAmount')?.errors?.['min']) {
+      订单金额不能小于0
+    }
+  </div>
+}
+```
+
+**提交验证**:
+```typescript
+createOrder(): void {
+  if (!this.canCreateOrder()) {
+    // 标记所有字段为已触摸,显示所有验证错误
+    this.orderCreationForm.markAllAsTouched();
+
+    // 滚动到第一个错误字段
+    const firstError = document.querySelector('.error-message');
+    if (firstError) {
+      firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
+    }
+    return;
+  }
+  // ... 继续提交逻辑
+}
+```
+
+### 5.2 按钮状态控制
+
+**分配订单按钮状态**:
+```typescript
+canCreateOrder(): boolean {
+  // 检查必填表单是否有效
+  const formValid = this.orderCreationForm?.valid;
+
+  // 检查是否已选择设计师
+  const designerAssigned = this.designerAssignmentData?.selectedDesigners?.length > 0;
+
+  // 检查是否已填写报价明细
+  const quotationFilled = this.quotationData?.items?.length > 0 && this.quotationData?.totalAmount > 0;
+
+  return formValid && designerAssigned && quotationFilled;
+}
+```
+
+**按钮样式**:
+```html
+<button
+  class="btn-primary"
+  [class.disabled]="!canCreateOrder()"
+  [disabled]="!canCreateOrder()"
+  (click)="createOrder()">
+  @if (isSubmitting) {
+    <span class="spinner"></span>
+    分配中...
+  } @else {
+    分配订单
+  }
+</button>
+```
+
+### 5.3 阶段推进动画
+
+**滚动动画**:
+```typescript
+// project-detail.ts lines 2253-2260
+scrollToStage(stage: ProjectStage): void {
+  const anchor = this.stageToAnchor(stage);
+  const el = document.getElementById(anchor);
+  if (el) {
+    el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+  }
+}
+```
+
+**展开动画**:
+```scss
+.stage-content {
+  transition: max-height 0.3s ease-in-out, opacity 0.2s ease-in-out;
+
+  &.collapsed {
+    max-height: 0;
+    opacity: 0;
+    overflow: hidden;
+  }
+
+  &.expanded {
+    max-height: 5000px;
+    opacity: 1;
+  }
+}
+```
+
+## 6. 组件依赖关系
+
+### 6.1 父子组件通信
+
+```mermaid
+graph TD
+    A[ProjectDetail 父组件] --> B[QuotationDetailsComponent]
+    A --> C[DesignerAssignmentComponent]
+    A --> D[DesignerCalendarComponent]
+
+    B -->|dataChange| A
+    C -->|assignmentChange| A
+    C -->|designerClick| A
+    D -->|designerSelected| A
+    D -->|assignmentRequested| A
+```
+
+### 6.2 Input/Output 接口
+
+**QuotationDetailsComponent**:
+```typescript
+@Component({
+  selector: 'app-quotation-details',
+  ...
+})
+export class QuotationDetailsComponent {
+  @Input() initialData?: QuotationData;
+  @Input() readonly: boolean = false;
+  @Output() dataChange = new EventEmitter<QuotationData>();
+
+  // 当报价数据变化时触发
+  onDataChange(): void {
+    this.dataChange.emit(this.quotationData);
+  }
+}
+```
+
+**DesignerAssignmentComponent**:
+```typescript
+@Component({
+  selector: 'app-designer-assignment',
+  ...
+})
+export class DesignerAssignmentComponent {
+  @Input() projectData?: any;
+  @Input() readonly: boolean = false;
+  @Output() assignmentChange = new EventEmitter<DesignerAssignmentData>();
+  @Output() designerClick = new EventEmitter<Designer>();
+
+  // 当设计师分配变化时触发
+  onAssignmentChange(): void {
+    this.assignmentChange.emit(this.assignmentData);
+  }
+
+  // 当点击设计师卡片时触发
+  onDesignerCardClick(designer: Designer): void {
+    this.designerClick.emit(designer);
+  }
+}
+```
+
+**父组件处理**:
+```typescript
+// project-detail.ts lines 2843-2873
+onQuotationDataChange(data: QuotationData): void {
+  this.quotationData = { ...data };
+  this.orderAmount = data.totalAmount || 0;
+}
+
+onDesignerAssignmentChange(data: DesignerAssignmentData): void {
+  this.designerAssignmentData = { ...data };
+}
+
+onDesignerClick(designer: AssignmentDesigner): void {
+  // 映射为日历组件需要的数据格式
+  const mapped = this.mapAssignmentDesignerToCalendar(designer);
+  this.calendarDesigners = [mapped];
+  this.calendarGroups = [{
+    id: designer.teamId,
+    name: designer.teamName,
+    leaderId: designer.id,
+    memberIds: [designer.id]
+  }];
+  this.selectedCalendarDate = new Date();
+  this.showDesignerCalendar = true;
+}
+```
+
+## 7. API集成
+
+### 7.1 项目创建接口
+
+**接口地址**:`POST /api/projects`
+
+**请求参数**:
+```typescript
+interface CreateProjectRequest {
+  // 客户信息
+  customerId: string;
+  customerName: string;
+  customerPhone: string;
+  customerWechat?: string;
+  customerType: string;
+  customerSource: string;
+  customerRemark?: string;
+
+  // 订单信息
+  orderAmount: number;
+  smallImageDeliveryTime: Date;
+  largeImageDeliveryTime?: Date;
+  decorationType: string;
+  requirementReason: string;
+  isMultiDesigner: boolean;
+
+  // 空间需求(可选)
+  spaceRequirements?: string;
+  designAngles?: string;
+  specialAreaHandling?: string;
+  materialRequirements?: string;
+  lightingRequirements?: string;
+
+  // 报价明细
+  quotation: {
+    items: Array<{
+      room: string;
+      amount: number;
+      description?: string;
+    }>;
+    totalAmount: number;
+    materialCost: number;
+    laborCost: number;
+    designFee: number;
+    managementFee: number;
+  };
+
+  // 设计师分配
+  assignment: {
+    designerIds: string[];
+    teamId: string;
+    leaderId: string;
+    assignmentDate: Date;
+    expectedStartDate: Date;
+  };
+
+  // 偏好标签
+  preferenceTags?: string[];
+}
+```
+
+**响应数据**:
+```typescript
+interface CreateProjectResponse {
+  success: boolean;
+  message: string;
+  projectId: string;
+  project: {
+    id: string;
+    name: string;
+    currentStage: ProjectStage;
+    createdAt: Date;
+    assignedDesigners: string[];
+  };
+}
+```
+
+### 7.2 设计师列表接口
+
+**接口地址**:`GET /api/designers/available`
+
+**查询参数**:
+```typescript
+interface DesignerQueryParams {
+  projectType?: string;      // 项目类型,用于技能匹配
+  requiredSkills?: string[]; // 必需技能
+  startDate?: Date;          // 项目预计开始日期
+  teamId?: string;           // 指定团队ID
+  sortBy?: 'skillMatch' | 'workload' | 'idleDays'; // 排序方式
+}
+```
+
+**响应数据**:
+```typescript
+interface DesignerListResponse {
+  success: boolean;
+  designers: Designer[];
+  recommendedDesignerId?: string; // AI推荐的最佳设计师
+}
+```
+
+## 8. 异常处理
+
+### 8.1 表单验证失败
+```typescript
+createOrder(): void {
+  if (!this.canCreateOrder()) {
+    // 1. 标记所有字段为已触摸
+    this.orderCreationForm.markAllAsTouched();
+
+    // 2. 收集所有错误信息
+    const errors: string[] = [];
+    Object.keys(this.orderCreationForm.controls).forEach(key => {
+      const control = this.orderCreationForm.get(key);
+      if (control?.invalid) {
+        errors.push(this.getFieldLabel(key));
+      }
+    });
+
+    // 3. 显示错误提示
+    alert(`请完善以下必填项:\n${errors.join('\n')}`);
+
+    // 4. 滚动到第一个错误字段
+    const firstError = document.querySelector('.error-message');
+    firstError?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+    return;
+  }
+  // ... 继续提交
+}
+```
+
+### 8.2 API调用失败
+```typescript
+this.projectService.createProject(orderData).subscribe({
+  next: (result) => {
+    if (result.success) {
+      alert('订单分配成功!');
+      this.advanceToNextStage('订单分配');
+    } else {
+      // 服务端返回失败
+      alert(`订单分配失败:${result.message || '未知错误'}`);
+    }
+  },
+  error: (error) => {
+    // 网络错误或服务端异常
+    console.error('订单分配失败:', error);
+
+    let errorMessage = '订单分配失败,请重试';
+
+    if (error.status === 400) {
+      errorMessage = '请求参数有误,请检查表单填写';
+    } else if (error.status === 401) {
+      errorMessage = '未登录或登录已过期,请重新登录';
+    } else if (error.status === 403) {
+      errorMessage = '没有权限执行此操作';
+    } else if (error.status === 500) {
+      errorMessage = '服务器错误,请稍后重试';
+    }
+
+    alert(errorMessage);
+  }
+});
+```
+
+### 8.3 设计师资源不足
+```typescript
+onDesignerAssignmentChange(data: DesignerAssignmentData): void {
+  if (!data.selectedDesigners || data.selectedDesigners.length === 0) {
+    // 没有选择设计师
+    this.showWarning('请至少选择一位设计师');
+    return;
+  }
+
+  // 检查设计师是否可用
+  const unavailableDesigners = data.selectedDesigners.filter(d => d.status === 'unavailable');
+  if (unavailableDesigners.length > 0) {
+    const names = unavailableDesigners.map(d => d.name).join('、');
+    this.showWarning(`以下设计师当前不可用:${names}\n请重新选择`);
+    return;
+  }
+
+  // 检查工作负荷
+  const overloadedDesigners = data.selectedDesigners.filter(d => d.workload > 90);
+  if (overloadedDesigners.length > 0) {
+    const names = overloadedDesigners.map(d => d.name).join('、');
+    const confirmed = confirm(`以下设计师工作负荷较高(>90%):${names}\n确定要继续分配吗?`);
+    if (!confirmed) {
+      return;
+    }
+  }
+
+  this.designerAssignmentData = { ...data };
+}
+```
+
+### 8.4 数据同步失败
+```typescript
+// 客服端同步数据解析失败
+this.route.queryParamMap.subscribe({
+  next: (qp) => {
+    const syncDataParam = qp.get('syncData');
+    if (syncDataParam) {
+      try {
+        const syncData = JSON.parse(syncDataParam);
+        // ... 同步逻辑
+      } catch (error) {
+        console.error('解析同步数据失败:', error);
+        this.isSyncingCustomerInfo = false;
+
+        // 显示错误提示
+        alert('客户信息同步失败,请手动填写表单');
+
+        // 回退到手动模式
+        this.orderCreationMethod = 'manual';
+      }
+    }
+  }
+});
+```
+
+## 9. 性能优化
+
+### 9.1 变更检测优化
+```typescript
+// 使用 OnPush 策略的子组件
+@Component({
+  selector: 'app-quotation-details',
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  ...
+})
+
+// 父组件手动触发变更检测
+onQuotationDataChange(data: QuotationData): void {
+  this.quotationData = { ...data }; // 不可变更新
+  this.orderAmount = data.totalAmount || 0;
+  this.cdr.markForCheck(); // 标记需要检查
+}
+```
+
+### 9.2 懒加载子组件
+```typescript
+// 只有展开订单分配阶段时才加载子组件
+@if (expandedSection === 'order' || getSectionStatus('order') === 'active') {
+  <div class="order-assignment-section">
+    <app-quotation-details ...></app-quotation-details>
+    <app-designer-assignment ...></app-designer-assignment>
+  </div>
+}
+```
+
+### 9.3 防抖处理
+```typescript
+// 客户搜索防抖
+private searchDebounce = new Subject<string>();
+
+ngOnInit(): void {
+  this.searchDebounce
+    .pipe(debounceTime(300), distinctUntilChanged())
+    .subscribe(keyword => {
+      this.performSearch(keyword);
+    });
+}
+
+onSearchInputChange(keyword: string): void {
+  this.searchDebounce.next(keyword);
+}
+```
+
+## 10. 测试用例
+
+### 10.1 单元测试
+
+**表单验证测试**:
+```typescript
+describe('OrderCreationForm Validation', () => {
+  it('should require orderAmount', () => {
+    const control = component.orderCreationForm.get('orderAmount');
+    control?.setValue('');
+    expect(control?.valid).toBeFalsy();
+    expect(control?.errors?.['required']).toBeTruthy();
+  });
+
+  it('should reject negative orderAmount', () => {
+    const control = component.orderCreationForm.get('orderAmount');
+    control?.setValue(-100);
+    expect(control?.valid).toBeFalsy();
+    expect(control?.errors?.['min']).toBeTruthy();
+  });
+
+  it('should accept valid orderAmount', () => {
+    const control = component.orderCreationForm.get('orderAmount');
+    control?.setValue(5000);
+    expect(control?.valid).toBeTruthy();
+  });
+});
+```
+
+**权限控制测试**:
+```typescript
+describe('Permission Control', () => {
+  it('should allow customer-service to edit order section', () => {
+    component.roleContext = 'customer-service';
+    expect(component.canEditSection('order')).toBeTruthy();
+  });
+
+  it('should not allow designer to edit order section', () => {
+    component.roleContext = 'designer';
+    expect(component.canEditSection('order')).toBeFalsy();
+  });
+
+  it('should allow team-leader to edit all sections', () => {
+    component.roleContext = 'team-leader';
+    expect(component.canEditSection('order')).toBeTruthy();
+    expect(component.canEditSection('requirements')).toBeTruthy();
+    expect(component.canEditSection('delivery')).toBeTruthy();
+    expect(component.canEditSection('aftercare')).toBeTruthy();
+  });
+});
+```
+
+### 10.2 集成测试
+
+**订单创建流程测试**:
+```typescript
+describe('Order Creation Flow', () => {
+  it('should complete full order creation flow', async () => {
+    // 1. 填写客户信息
+    component.customerForm.patchValue({
+      name: '测试客户',
+      phone: '13800138000',
+      customerType: '新客户'
+    });
+
+    // 2. 填写必填项
+    component.orderCreationForm.patchValue({
+      orderAmount: 10000,
+      smallImageDeliveryTime: new Date(),
+      decorationType: '全包',
+      requirementReason: '新房装修'
+    });
+
+    // 3. 添加报价明细
+    component.quotationData = {
+      items: [{ id: '1', room: '客厅', amount: 5000 }],
+      totalAmount: 10000,
+      materialCost: 6000,
+      laborCost: 3000,
+      designFee: 1000,
+      managementFee: 0
+    };
+
+    // 4. 分配设计师
+    component.designerAssignmentData = {
+      selectedDesigners: [mockDesigner],
+      teamId: 'team1',
+      teamName: '设计一组',
+      leaderId: 'designer1',
+      assignmentDate: new Date(),
+      expectedStartDate: new Date()
+    };
+
+    // 5. 验证可以提交
+    expect(component.canCreateOrder()).toBeTruthy();
+
+    // 6. 提交订单
+    spyOn(component.projectService, 'createProject').and.returnValue(
+      of({ success: true, projectId: 'proj-001' })
+    );
+
+    component.createOrder();
+
+    expect(component.projectService.createProject).toHaveBeenCalled();
+  });
+});
+```
+
+### 10.3 E2E测试
+
+**完整用户流程测试**:
+```typescript
+describe('Order Assignment E2E', () => {
+  it('should complete order assignment as customer-service', async () => {
+    // 1. 客服登录
+    await page.goto('/customer-service/login');
+    await page.fill('#username', 'cs_user');
+    await page.fill('#password', 'password');
+    await page.click('button[type="submit"]');
+
+    // 2. 创建咨询订单
+    await page.goto('/customer-service/consultation-order/create');
+    await page.fill('#customerName', '张三');
+    await page.fill('#customerPhone', '13800138000');
+    await page.click('button[text="创建订单"]');
+
+    // 3. 跳转到项目详情页
+    await page.waitForURL(/\/designer\/project-detail/);
+
+    // 4. 填写订单分配表单
+    await page.fill('#orderAmount', '10000');
+    await page.fill('#smallImageDeliveryTime', '2025-11-01');
+    await page.selectOption('#decorationType', '全包');
+    await page.check('#requirementReason[value="新房装修"]');
+
+    // 5. 添加报价明细
+    await page.click('button[text="添加报价项"]');
+    await page.fill('.quotation-item:last-child #room', '客厅');
+    await page.fill('.quotation-item:last-child #amount', '5000');
+
+    // 6. 选择设计师
+    await page.click('.designer-card:first-child');
+
+    // 7. 提交订单
+    await page.click('button[text="分配订单"]');
+
+    // 8. 验证成功跳转到需求沟通阶段
+    await expect(page.locator('.stage-nav-item.active')).toContainText('确认需求');
+  });
+});
+```
+
+## 11. 附录
+
+### 11.1 字段标签映射
+```typescript
+private fieldLabelMap: Record<string, string> = {
+  'orderAmount': '订单金额',
+  'smallImageDeliveryTime': '小图交付时间',
+  'decorationType': '装修类型',
+  'requirementReason': '需求原因',
+  'isMultiDesigner': '多设计师协作',
+  'largeImageDeliveryTime': '大图交付时间',
+  'spaceRequirements': '空间需求',
+  'designAngles': '设计角度',
+  'specialAreaHandling': '特殊区域处理',
+  'materialRequirements': '材质要求',
+  'lightingRequirements': '光照需求'
+};
+
+private getFieldLabel(fieldName: string): string {
+  return this.fieldLabelMap[fieldName] || fieldName;
+}
+```
+
+### 11.2 阶段锚点映射
+```typescript
+// project-detail.ts lines 2237-2251
+stageToAnchor(stage: ProjectStage): string {
+  const map: Record<ProjectStage, string> = {
+    '订单分配': 'order',
+    '需求沟通': 'requirements-talk',
+    '方案确认': 'proposal-confirm',
+    '建模': 'modeling',
+    '软装': 'softdecor',
+    '渲染': 'render',
+    '后期': 'postprocess',
+    '尾款结算': 'settlement',
+    '客户评价': 'customer-review',
+    '投诉处理': 'complaint'
+  };
+  return `stage-${map[stage] || 'unknown'}`;
+}
+```
+
+### 11.3 设计师状态颜色映射
+```scss
+.designer-card {
+  &.status-idle {
+    border-left: 4px solid #10b981; // 绿色 - 空闲
+  }
+
+  &.status-busy {
+    border-left: 4px solid #f59e0b; // 橙色 - 繁忙
+  }
+
+  &.status-unavailable {
+    border-left: 4px solid #9ca3af; // 灰色 - 不可用
+    opacity: 0.6;
+    cursor: not-allowed;
+  }
+}
+```
+
+---
+
+**文档版本**:v1.0.0
+**创建日期**:2025-10-16
+**最后更新**:2025-10-16
+**维护人**:产品团队

+ 2080 - 0
docs/prd/项目-需求确认.md

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

+ 19 - 11
docs/task/20251015-wxwork-project.md

@@ -90,18 +90,26 @@
             claude-sonnet:  9.7k input, 146.9k output, 6.3m cache read, 2.0m cache write ($11.79)
 
 # FAQ:订单页面细化
-请您参考./docs/data/quotation.md重新设计和开发stage-order页面,特别是订单确认这部分要参考src/a
-  pp/pages/designer/project-detail/project-detail.ts和src/app/pages/designer/project-detail/pro
-  ject-detail.html及相关页面组件的交互.项目信息填写分类应该是家装\工装,价格表应该是客户直接快速
-  选场景,下面价格表就出来了,可以直接用,创建订单,只有要调整的再人工调整.还有分配设计师,要按照pro
-  ject-detail.ts中点开后选择项目组在选择组员的方式,注意项目组使用Departmnet表- Department
+
+1. 为了进一步将src/app/pages/designer/project-detail/project-detail.ts所有功能界面交互,迁移到./src/modules/project子页面内,请您先分析原有逻辑.再查看./rules/schemas.md,充分准备好交互逻辑,再开始开发
+
+2.请您参考./docs/data/quotation.md重新设计和开发stage-order页面
+
+特别是订单确认这部分要参考src/app/pages/designer/project-detail/project-detail.ts和src/app/pages/designer/project-detail/project-detail.html及相关页面组件的交互.
+项目信息填写分类应该是家装\工装,
+价格表应该是客户直接快速选场景,下面价格表就出来了,可以直接用,创建订单,只有要调整的再人工调整.
+
+注意:分配设计师,要按照project-detail.ts中点开后选择项目组在选择组和组员的方式,注意项目组使用Departmnet表
+- Department
   项目组(部门)
     - name String 项目组名称
     - type "project" 项目组
     - leader Pointer<Profile> 组长
-    - company Pointer<Company> 指向当前帐套.为了进一步将src/app/pages/designer/project-detail/p
-  roject-detail.ts所有功能界面交互,迁移到./src/modules/project子页面内,请您先分析原有逻辑.再查
-  看./rules/schemas.md,充分准备好交互逻辑,再开始开发,注意使用div-scss而不是Ionic.请您完成更完整
-  更可用的所有开发任务.并检查下aftercare
-  requirements两个阶段Angular报错递归调用等细节问题,delivery中出现的场景来自于order阶段报价中的
-  场景要数据动态加载,每个场景都需要建模\软装\渲染\后期阶段.您不用询问直接继续,直到完成所有. 
+    - company Pointer<Company> 指向当前帐套.
+
+
+3.并检查下aftercare requirements两个阶段Angular报错递归调用等细节问题
+
+4.delivery中出现的场景来自于order阶段报价中的场景要数据动态加载,每个场景都需要建模\软装\渲染\后期阶段.您不用询问直接继续,直到完成所有. 
+
+注意使用div-scss而不是Ionic.请您合理规划整个计划任务,完成更完整更可用的项目管理模块./src/modules/project.

+ 7 - 7
rules/schemas.md

@@ -58,7 +58,7 @@ TABLE(Profile, "Profile\n员工档案表") {
     FIELD(mobile, String)
     FIELD(company, Pointer→Company)
     FIELD(userId, String)
-    FIELD(role, String)
+    FIELD(roleName, String)
     FIELD(data, Object)
     FIELD(isDeleted, Boolean)
 }
@@ -324,7 +324,7 @@ GroupChat "n" --> "1" Project : 关联项目(可选)
 | mobile | String | 否 | 手机号 | "13800138000" |
 | company | Pointer | 是 | 所属企业 | → Company |
 | userId | String | 否 | 企微UserID | "zhangsan" |
-| role | String | 是 | 员工角色 | "客服" / "组员" / "组长" |
+| roleName | String | 是 | 员工角色 | "客服" / "组员" / "组长" |
 | data | Object | 否 | 扩展数据 | { avatar, department, skills, ... } |
 | isDeleted | Boolean | 否 | 软删除标记 | false |
 | createdAt | Date | 自动 | 创建时间 | 2024-01-01T00:00:00.000Z |
@@ -369,20 +369,20 @@ const profile = await query.get(profileId);
 // 查询设计师列表
 const designerQuery = new Parse.Query("Profile");
 designerQuery.equalTo("company", companyId);
-designerQuery.equalTo("role", "组员");
+designerQuery.equalTo("roleName", "组员");
 designerQuery.notEqualTo("isDeleted", true);
 const designers = await designerQuery.find();
 
 // 查询客服人员
 const csQuery = new Parse.Query("Profile");
-csQuery.equalTo("role", "客服");
+csQuery.equalTo("roleName", "客服");
 csQuery.equalTo("company", companyId);
 ```
 
 **索引建议**:
 - `company + isDeleted`
 - `userId + company`
-- `role + company`
+- `roleName + company`
 - `mobile`
 
 ---
@@ -614,7 +614,7 @@ const projects = (await pgQuery2.find()).map(pg => pg.get("project"));
       "endTime": "2024-10-01T10:30:00.000Z",
       "duration": 1.5,
       "status": "completed",
-      "operator": { "id": "prof002", "name": "王客服", "role": "客服" }
+      "operator": { "id": "prof002", "name": "王客服", "roleName": "客服" }
     }
   ],
   "workflow": {
@@ -1385,7 +1385,7 @@ for (const contact of contacts) {
 
 ✅ **统一规范**: Profile(员工)、ContactInfo(客户)统一管理
 ✅ **灵活关联**: Project ←→ GroupChat 支持简单和复杂场景
-✅ **角色清晰**: role 字段区分客服/组员/组长
+✅ **角色清晰**: roleName 字段区分客服/组员/组长
 ✅ **扩展性强**: data Object 字段支持灵活扩展
 ✅ **多租户**: company 字段确保租户隔离
 ✅ **软删除**: isDeleted 字段保护数据安全

+ 425 - 0
src/modules/project/config/quotation-rules.ts

@@ -0,0 +1,425 @@
+/**
+ * 映三色项目报价规则配置
+ *
+ * 来源: docs/data/quotation.md
+ *
+ * 三级报价体系:
+ * - 一级报价: 老客户报价
+ * - 二级报价: 中端组报价
+ * - 三级报价: 高端组报价
+ */
+
+export interface QuotationRule {
+  priceLevel: string;
+  projectType: '家装' | '工装' | '建筑类';
+  renderType?: '静态单张' | '360全景';
+  spaceType?: string;
+  styleLevel?: string;
+  businessType?: string;
+  spaceCategory?: string;
+  basePrice: number;
+}
+
+/**
+ * 风格分类
+ */
+export const STYLE_LEVELS = {
+  基础风格组: ['现代', '北欧', '简约', '后现代', '工业风'],
+  中级风格组: ['轻奢', '日式', '新中式'],
+  高级风格组: ['简欧', '古典中式', '欧式', '美式', '简美', '轻法'],
+  顶级风格组: ['古典欧式', '古典美式', '纯法式']
+};
+
+/**
+ * 空间类型
+ */
+export const SPACE_TYPES = {
+  家装: ['平层', '跃层', '挑空'],
+  工装: ['门厅空间', '封闭空间', '辅助空间']
+};
+
+/**
+ * 业态类型
+ */
+export const BUSINESS_TYPES = ['办公空间', '商业空间', '娱乐空间', '酒店餐厅', '公共空间'];
+
+/**
+ * 建筑类型
+ */
+export const ARCHITECTURE_TYPES = ['门头', '小型单体', '大型单体', '鸟瞰'];
+
+/**
+ * 家装区预设房间列表
+ */
+export const HOME_DEFAULT_ROOMS = ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'];
+
+/**
+ * 完整报价表
+ */
+export const QUOTATION_PRICE_TABLE = {
+  // ==================== 一级报价(老客户) ====================
+  一级: {
+    家装: {
+      静态单张: {
+        平层: {
+          基础风格组: 300,
+          中级风格组: 350,
+          高级风格组: 400,
+          顶级风格组: 500
+        },
+        跃层: {
+          基础风格组: 400,
+          中级风格组: 450,
+          高级风格组: 500,
+          顶级风格组: 600
+        },
+        挑空: {
+          基础风格组: 600,
+          中级风格组: 700,
+          高级风格组: 800,
+          顶级风格组: 900
+        },
+        卧室: 300
+      },
+      '360全景': {
+        平层: {
+          基础风格组: 650,
+          中级风格组: 700,
+          高级风格组: 750,
+          顶级风格组: 850
+        },
+        跃层: {
+          基础风格组: 700,
+          中级风格组: 750,
+          高级风格组: 850,
+          顶级风格组: 1000
+        },
+        挑空: {
+          基础风格组: 1100,
+          中级风格组: 1200,
+          高级风格组: 1300,
+          顶级风格组: 1400
+        },
+        卧室: 400
+      }
+    },
+    工装: {
+      静态单张: {
+        办公空间: { 门厅空间: 600, 封闭空间: 400 },
+        商业空间: { 门厅空间: 600, 封闭空间: 500 },
+        娱乐空间: { 门厅空间: 600, 封闭空间: 500 },
+        酒店餐厅: { 门厅空间: 600, 封闭空间: 500 },
+        公共空间: { 门厅空间: 600, 封闭空间: 600 }
+      }
+    },
+    建筑类: {
+      门头: 700,
+      小型单体: 1000,
+      大型单体: 1500,
+      鸟瞰: 2000
+    }
+  },
+
+  // ==================== 二级报价(中端组) ====================
+  二级: {
+    家装: {
+      静态单张: {
+        平层: {
+          基础风格组: 400,
+          中级风格组: 450,
+          高级风格组: 500,
+          顶级风格组: 600
+        },
+        跃层: {
+          基础风格组: 500,
+          中级风格组: 550,
+          高级风格组: 600,
+          顶级风格组: 700
+        },
+        挑空: {
+          基础风格组: 700,
+          中级风格组: 800,
+          高级风格组: 800,
+          顶级风格组: 900
+        },
+        卧室: 400
+      },
+      '360全景': {
+        平层: {
+          基础风格组: 800,
+          中级风格组: 850,
+          高级风格组: 900,
+          顶级风格组: 1000
+        },
+        跃层: {
+          基础风格组: 900,
+          中级风格组: 950,
+          高级风格组: 1000,
+          顶级风格组: 1100
+        },
+        挑空: {
+          基础风格组: 1200,
+          中级风格组: 1300,
+          高级风格组: 1400,
+          顶级风格组: 1500
+        },
+        卧室: 600
+      }
+    },
+    工装: {
+      静态单张: {
+        办公空间: { 门厅空间: 700, 封闭空间: 500 },
+        商业空间: { 门厅空间: 700, 封闭空间: 600 },
+        娱乐空间: { 门厅空间: 700, 封闭空间: 600 },
+        酒店餐厅: { 门厅空间: 700, 封闭空间: 600 },
+        公共空间: { 门厅空间: 700, 封闭空间: 700 }
+      }
+    },
+    建筑类: {
+      门头: 800,
+      小型单体: 1100,
+      大型单体: 1600,
+      鸟瞰: 2100
+    }
+  },
+
+  // ==================== 三级报价(高端组) ====================
+  三级: {
+    家装: {
+      静态单张: {
+        平层: {
+          基础风格组: 600,
+          中级风格组: 650,
+          高级风格组: 700,
+          顶级风格组: 800
+        },
+        跃层: {
+          基础风格组: 700,
+          中级风格组: 750,
+          高级风格组: 800,
+          顶级风格组: 900
+        },
+        挑空: {
+          基础风格组: 900,
+          中级风格组: 1000,
+          高级风格组: 1000,
+          顶级风格组: 1200
+        },
+        卧室: 600
+      },
+      '360全景': {
+        平层: {
+          基础风格组: 1200,
+          中级风格组: 1250,
+          高级风格组: 1400,
+          顶级风格组: 1500
+        },
+        跃层: {
+          基础风格组: 1300,
+          中级风格组: 1400,
+          高级风格组: 1500,
+          顶级风格组: 1700
+        },
+        挑空: {
+          基础风格组: 1800,
+          中级风格组: 1850,
+          高级风格组: 1900,
+          顶级风格组: 2000
+        },
+        卧室: 800
+      }
+    },
+    工装: {
+      静态单张: {
+        办公空间: { 门厅空间: 900, 封闭空间: 700 },
+        商业空间: { 门厅空间: 900, 封闭空间: 800 },
+        娱乐空间: { 门厅空间: 900, 封闭空间: 800 },
+        酒店餐厅: { 门厅空间: 900, 封闭空间: 800 },
+        公共空间: { 门厅空间: 900, 封闭空间: 900 }
+      }
+    },
+    建筑类: {
+      门头: 1000,
+      小型单体: 1300,
+      大型单体: 1800,
+      鸟瞰: 2300
+    }
+  }
+};
+
+/**
+ * 加价规则配置
+ */
+export const ADJUSTMENT_RULES = {
+  // 家装加价规则
+  家装: {
+    extraFunction: {
+      label: '功能区加价',
+      amount: 100,
+      description: '每增加一个功能区+100元'
+    },
+    complexity: {
+      label: '造型复杂度',
+      min: 100,
+      max: 200,
+      description: '立面和吊顶造型复杂:+100至200元'
+    },
+    design: {
+      label: '设计服务',
+      multiplier: 2,
+      description: '需要我们设计(非提供参考图):原价×2'
+    }
+  },
+
+  // 工装加价规则
+  工装: {
+    extraFunction: {
+      label: '功能区加价',
+      amount: 400,
+      description: '每增加一个功能区+400元'
+    },
+    complexity: {
+      label: '造型复杂度',
+      min: 100,
+      max: 200,
+      description: '立面和吊顶造型复杂:+100至200元'
+    },
+    design: {
+      label: '设计服务',
+      multiplier: 2,
+      description: '需要我们设计:原价×2'
+    },
+    panoramic: {
+      label: '全景渲染',
+      multiplier: 2,
+      description: '需要全景渲染:原价×2'
+    }
+  },
+
+  // 建筑类加价规则
+  建筑类: {
+    complexity: {
+      label: '造型复杂度',
+      min: 100,
+      max: 200,
+      description: '根据造型复杂度:+100至200元'
+    },
+    design: {
+      label: '设计服务',
+      multiplier: 2,
+      description: '需要我们设计:原价×2'
+    }
+  }
+};
+
+/**
+ * 获取基础价格
+ */
+export function getBasePrice(
+  priceLevel: string,
+  projectType: '家装' | '工装' | '建筑类',
+  renderType: string,
+  spaceType: string,
+  styleLevel?: string,
+  businessType?: string,
+  architectureType?: string
+): number {
+  try {
+    const table = QUOTATION_PRICE_TABLE as any;
+
+    if (projectType === '家装') {
+      if (spaceType === '卧室') {
+        return table[priceLevel]?.[projectType]?.[renderType]?.['卧室'] || 0;
+      }
+      return table[priceLevel]?.[projectType]?.[renderType]?.[spaceType]?.[styleLevel || ''] || 0;
+    }
+
+    if (projectType === '工装') {
+      return table[priceLevel]?.[projectType]?.[renderType]?.[businessType || '']?.[spaceType] || 0;
+    }
+
+    if (projectType === '建筑类') {
+      return table[priceLevel]?.[projectType]?.[architectureType || ''] || 0;
+    }
+
+    return 0;
+  } catch (err) {
+    console.error('获取基础价格失败:', err);
+    return 0;
+  }
+}
+
+/**
+ * 计算最终价格(含加价规则)
+ */
+export function calculateFinalPrice(
+  basePrice: number,
+  projectType: '家装' | '工装' | '建筑类',
+  adjustments: {
+    extraFunction?: number; // 功能区数量
+    complexity?: number; // 造型复杂度加价金额
+    design?: boolean; // 是否需要设计服务
+    panoramic?: boolean; // 是否全景渲染(仅工装)
+  }
+): number {
+  let price = basePrice;
+  const rules = ADJUSTMENT_RULES[projectType];
+
+  // 功能区加价
+  if (adjustments.extraFunction && rules.extraFunction) {
+    price += (rules.extraFunction as any).amount * adjustments.extraFunction;
+  }
+
+  // 造型复杂度加价
+  if (adjustments.complexity) {
+    price += adjustments.complexity;
+  }
+
+  // 设计服务倍增
+  if (adjustments.design && rules.design) {
+    price *= (rules.design as any).multiplier;
+  }
+
+  // 全景渲染倍增(仅工装)
+  if (projectType === '工装' && adjustments.panoramic && (rules as any).panoramic) {
+    price *= (rules as any).panoramic.multiplier;
+  }
+
+  return price;
+}
+
+/**
+ * 生成默认工序配置
+ */
+export function getDefaultProcesses(projectType: '家装' | '工装', finalPrice: number) {
+  return {
+    modeling: {
+      enabled: false,
+      price: 0,
+      unit: '㎡',
+      quantity: 0,
+      description: '3D建模'
+    },
+    softDecor: {
+      enabled: false,
+      price: 0,
+      unit: '㎡',
+      quantity: 0,
+      description: '软装设计'
+    },
+    rendering: {
+      enabled: true,
+      price: finalPrice,
+      unit: '张',
+      quantity: 1,
+      description: '效果图渲染'
+    },
+    postProcess: {
+      enabled: true,
+      price: Math.round(finalPrice * 0.2),
+      unit: '张',
+      quantity: 1,
+      description: '后期处理'
+    }
+  };
+}

+ 48 - 128
src/modules/project/pages/project-detail/stages/stage-aftercare.component.ts

@@ -2,11 +2,7 @@ import { Component, OnInit, Input } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { IonicModule } from '@ionic/angular';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
-import { ProjectUploadService } from '../../../services/upload.service';
-import { ProjectAIService } from '../../../services/ai.service';
-import { WxworkSDKService } from '../../../services/wxwork-sdk.service';
 
 const Parse = FmodeParse.with('nova');
 
@@ -23,7 +19,7 @@ const Parse = FmodeParse.with('nova');
 @Component({
   selector: 'app-stage-aftercare',
   standalone: true,
-  imports: [CommonModule, IonicModule, FormsModule],
+  imports: [CommonModule, FormsModule],
   templateUrl: './stage-aftercare.component.html',
   styleUrls: ['./stage-aftercare.component.scss']
 })
@@ -37,9 +33,6 @@ export class StageAftercareComponent implements OnInit {
   cid: string = '';
   projectId: string = '';
 
-  // 服务注入
-  wxwork: WxworkSDKService | null = null;
-
   // 尾款信息
   finalPayment = {
     totalAmount: 0,
@@ -91,10 +84,7 @@ export class StageAftercareComponent implements OnInit {
   saving: boolean = false;
 
   constructor(
-    private route: ActivatedRoute,
-    private uploadService: ProjectUploadService,
-    private aiService: ProjectAIService,
-    private wxworkService: WxworkSDKService
+    private route: ActivatedRoute
   ) {}
 
   async ngOnInit() {
@@ -114,15 +104,17 @@ export class StageAftercareComponent implements OnInit {
       this.loading = true;
 
       if (!this.project && this.projectId) {
-        const query = new Parse.Query('Project');
+        const query = Parse.Query.from('Project');
         query.include('customer', 'assignee');
         this.project = await query.get(this.projectId);
         this.customer = this.project.get('customer');
       }
 
       if (!this.currentUser && this.cid) {
-        await this.wxworkService.initialize(this.cid, 'crm');
-        this.currentUser = await this.wxworkService.getCurrentUser();
+        // 动态导入WxworkSDK避免循环依赖
+        const { WxworkSDK } = await import('fmode-ng/core');
+        const wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
+        this.currentUser = await wxwork.getCurrentUser();
 
         const role = this.currentUser?.get('roleName') || '';
         this.canEdit = ['客服', '组长', '管理员'].includes(role);
@@ -173,14 +165,14 @@ export class StageAftercareComponent implements OnInit {
     const file = event.target.files[0];
     if (!file) return;
 
-    // 验证文件类型
-    if (!this.uploadService.validateFileType(file, ['image/*'])) {
+    // 简单的文件类型验证
+    if (!file.type.startsWith('image/')) {
       alert('请上传图片文件');
       return;
     }
 
     // 验证文件大小 (10MB)
-    if (!this.uploadService.validateFileSize(file, 10)) {
+    if (file.size > 10 * 1024 * 1024) {
       alert('图片大小不能超过10MB');
       return;
     }
@@ -188,57 +180,23 @@ export class StageAftercareComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 上传文件到Parse Server
-      const url = await this.uploadService.uploadFile(file, {
-        compress: true,
-        onProgress: (progress: number) => {
-          console.log('上传进度:', progress);
-        }
+      // 直接使用Parse File上传
+      const parseFile = new Parse.File(file.name, file);
+      await parseFile.save();
+      const url = parseFile.url();
+
+      // 暂时不使用OCR,需要手动输入金额和支付方式
+      this.finalPayment.paymentVouchers.push({
+        url: url,
+        amount: 0,
+        paymentTime: new Date(),
+        paymentMethod: '待确认',
+        ocrResult: { note: '请手动核对金额和支付方式' }
       });
 
-      // 使用AI进行OCR识别
-      try {
-        const ocrResult = await this.aiService.recognizePaymentVoucher(url);
-
-        this.finalPayment.paymentVouchers.push({
-          url: url,
-          amount: ocrResult.amount || 0,
-          paymentTime: ocrResult.paymentTime ? new Date(ocrResult.paymentTime) : new Date(),
-          paymentMethod: ocrResult.paymentMethod || '未识别',
-          ocrResult: ocrResult
-        });
-
-        // 更新已支付金额
-        this.finalPayment.paidAmount += ocrResult.amount || 0;
-        this.finalPayment.remainingAmount = this.finalPayment.totalAmount - this.finalPayment.paidAmount;
-
-        // 更新状态
-        if (this.finalPayment.remainingAmount <= 0) {
-          this.finalPayment.status = 'completed';
-        } else if (this.finalPayment.paidAmount > 0) {
-          this.finalPayment.status = 'partial';
-        }
-
-        await this.saveDraft();
-
-        alert(`OCR识别成功!\n金额: ¥${ocrResult.amount}\n方式: ${ocrResult.paymentMethod}`);
-
-      } catch (ocrError) {
-        // OCR失败,仍然保存图片,但需要手动输入
-        console.error('OCR识别失败:', ocrError);
-
-        this.finalPayment.paymentVouchers.push({
-          url: url,
-          amount: 0,
-          paymentTime: new Date(),
-          paymentMethod: '待确认',
-          ocrResult: { error: 'OCR识别失败,请手动确认' }
-        });
+      await this.saveDraft();
 
-        await this.saveDraft();
-
-        alert('凭证已上传,但OCR识别失败,请手动核对金额和支付方式');
-      }
+      alert('凭证已上传,请手动核对金额和支付方式');
 
     } catch (error: any) {
       console.error('上传失败:', error);
@@ -285,23 +243,35 @@ export class StageAftercareComponent implements OnInit {
       // 准备项目数据
       const projectData = {
         title: this.project.get('title') || '',
-        type: this.project.get('type') || '',
+        type: this.project.get('projectType') || '',
         duration: this.calculateProjectDuration(),
         customerRating: this.customerFeedback.rating,
         challenges: this.extractChallenges()
       };
 
-      // 调用AI服务生成复盘
-      const result = await this.aiService.generateProjectRetrospective(
-        projectData,
-        {
-          onProgress: (content) => {
-            console.log('生成进度:', content.length);
-          }
-        }
-      );
-
-      this.projectRetrospective = result;
+      // 使用mock数据生成复盘(暂时不调用AI服务)
+      this.projectRetrospective = {
+        generated: true,
+        summary: `项目${projectData.title}整体执行顺利,历时${projectData.duration}天完成。客户满意度${projectData.customerRating}星,团队协作良好。`,
+        highlights: [
+          '设计方案获得客户高度认可',
+          '按时完成所有交付物',
+          '团队协作高效,沟通顺畅'
+        ],
+        challenges: projectData.challenges.length > 0
+          ? projectData.challenges
+          : ['部分细节调整较多', '个别环节时间紧张'],
+        lessons: [
+          '需要更早介入前期沟通',
+          '方案确认需要更充分讨论',
+          '时间管理需要更精细化'
+        ],
+        recommendations: [
+          '建立快速响应机制',
+          '增加可视化沟通工具',
+          '完善项目管理流程'
+        ]
+      };
 
       await this.saveDraft();
 
@@ -347,56 +317,6 @@ export class StageAftercareComponent implements OnInit {
     return challenges;
   }
 
-  /**
-   * 构建复盘提示词
-   */
-  buildRetrospectivePrompt(): string {
-    let prompt = `作为项目经理,请对以下项目进行复盘总结:\n\n`;
-
-    prompt += `项目名称: ${this.project?.get('title')}\n`;
-    prompt += `项目类型: ${this.project?.get('type')}\n`;
-    prompt += `客户评价: ${this.customerFeedback.rating}星\n\n`;
-
-    prompt += `请从以下几个方面进行总结:\n`;
-    prompt += `1. 项目亮点和成功经验\n`;
-    prompt += `2. 遇到的挑战和问题\n`;
-    prompt += `3. 经验教训和改进建议\n`;
-    prompt += `4. 未来项目的建议\n`;
-
-    return prompt;
-  }
-
-  /**
-   * 调用AI服务
-   */
-  async callAIService(prompt: string): Promise<string> {
-    // TODO: 实现实际的AI服务调用
-    await new Promise(resolve => setTimeout(resolve, 2000));
-
-    return JSON.stringify({
-      summary: '项目整体执行顺利,客户满意度高,团队协作良好。',
-      highlights: ['设计方案获得客户高度认可', '按时完成所有交付物', '团队协作高效'],
-      challenges: ['CAD图纸延期', '部分软装调整较多'],
-      lessons: ['需要更早介入CAD图纸环节', '软装方案需要更充分沟通'],
-      recommendations: ['建立CAD图纸快速审核机制', '增加软装方案可视化工具']
-    });
-  }
-
-  /**
-   * 解析复盘响应
-   */
-  parseRetrospectiveResponse(response: string): any {
-    try {
-      const parsed = JSON.parse(response);
-      return {
-        generated: true,
-        ...parsed
-      };
-    } catch (err) {
-      console.error('解析失败:', err);
-      return null;
-    }
-  }
 
   /**
    * 归档项目

+ 18 - 13
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -2,9 +2,7 @@ import { Component, OnInit, Input } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { IonicModule } from '@ionic/angular';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
-import { WxworkSDK } from 'fmode-ng/core';
 
 const Parse = FmodeParse.with('nova');
 
@@ -12,7 +10,7 @@ const Parse = FmodeParse.with('nova');
  * 交付执行阶段组件
  *
  * 功能:
- * 1. 按空间+工序上传交付物
+ * 1. 按空间+工序上传交付物(从order阶段quotation动态加载)
  * 2. 质量自查清单
  * 3. 组长审核与问题反馈
  * 4. 发起交付流程
@@ -20,7 +18,7 @@ const Parse = FmodeParse.with('nova');
 @Component({
   selector: 'app-stage-delivery',
   standalone: true,
-  imports: [CommonModule, IonicModule, FormsModule],
+  imports: [CommonModule, FormsModule],
   templateUrl: './stage-delivery.component.html',
   styleUrls: ['./stage-delivery.component.scss']
 })
@@ -34,9 +32,6 @@ export class StageDeliveryComponent implements OnInit {
   cid: string = '';
   projectId: string = '';
 
-  // 企微SDK
-  wxwork: WxworkSDK | null = null;
-
   // 用户角色
   role: string = '';
   isDesigner: boolean = false;
@@ -141,7 +136,7 @@ export class StageDeliveryComponent implements OnInit {
 
       // 如果没有传入project,从路由参数加载
       if (!this.project && this.projectId) {
-        const query = new Parse.Query('Project');
+        const query = Parse.Query.from('Project');
         query.include('customer', 'assignee');
         this.project = await query.get(this.projectId);
         this.customer = this.project.get('customer');
@@ -149,8 +144,9 @@ export class StageDeliveryComponent implements OnInit {
 
       // 如果没有传入currentUser,加载当前用户
       if (!this.currentUser && this.cid) {
-        this.wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
-        this.currentUser = await this.wxwork.getCurrentUser();
+        const { WxworkSDK } = await import('fmode-ng/core');
+        const wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
+        this.currentUser = await wxwork.getCurrentUser();
       }
 
       // 设置用户角色和权限
@@ -226,16 +222,24 @@ export class StageDeliveryComponent implements OnInit {
     const file = event.target.files[0];
     if (!file) return;
 
+    // 验证文件大小 (50MB)
+    if (file.size > 50 * 1024 * 1024) {
+      alert('文件大小不能超过50MB');
+      return;
+    }
+
     try {
       this.uploading = true;
 
-      // TODO: 实现文件上传到Parse Server或云存储
-      const mockUrl = URL.createObjectURL(file);
+      // 使用Parse File上传
+      const parseFile = new Parse.File(file.name, file);
+      await parseFile.save();
+      const url = parseFile.url();
 
       const deliverable = this.getDeliverable(spaceName, processType);
       if (deliverable) {
         deliverable.files.push({
-          url: mockUrl,
+          url: url,
           name: file.name,
           type: file.type.startsWith('image/') ? 'image' : file.type.startsWith('video/') ? 'video' : 'document',
           uploadTime: new Date(),
@@ -246,6 +250,7 @@ export class StageDeliveryComponent implements OnInit {
         });
 
         await this.saveDraft();
+        alert('上传成功');
       }
 
     } catch (err) {

+ 414 - 120
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -24,26 +24,20 @@
       <div class="card-content">
         <div class="info-list">
           <div class="info-item">
-            <div class="info-text">
-              <p class="info-label">客户姓名</p>
-              <h4 class="info-value">{{ customer?.get('name') }}</h4>
-            </div>
+            <p class="info-label">客户姓名</p>
+            <h4 class="info-value">{{ customer?.get('name') }}</h4>
           </div>
           <div class="info-item">
-            <div class="info-text">
-              <p class="info-label">来源渠道</p>
-              <h4 class="info-value">{{ customer?.get('source') }}</h4>
-            </div>
+            <p class="info-label">来源渠道</p>
+            <h4 class="info-value">{{ customer?.get('source') || '未知' }}</h4>
           </div>
           @if (customer?.get('data')?.preferences) {
             <div class="info-item">
-              <div class="info-text">
-                <p class="info-label">风格偏好</p>
-                <div class="preference-tags">
-                  @for (style of customer?.get('data')?.preferences?.style || []; track style) {
-                    <span class="badge badge-tertiary">{{ style }}</span>
-                  }
-                </div>
+              <p class="info-label">风格偏好</p>
+              <div class="preference-tags">
+                @for (style of customer?.get('data')?.preferences?.style || []; track style) {
+                  <span class="badge badge-tertiary">{{ style }}</span>
+                }
               </div>
             </div>
           }
@@ -56,7 +50,7 @@
       <div class="card-header">
         <h3 class="card-title">
           <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M416 221.25V416a48 48 0 01-48 48H144a48 48 0 01-48-48V96a48 48 0 0148-48h98.75a32 32 0 0122.62 9.37l141.26 141.26a32 32 0 019.37 22.62z"/><path fill="currentColor" d="M256 56v120a32 32 0 0032 32h120"/>
+            <path fill="currentColor" d="M416 221.25V416a48 48 0 01-48 48H144a48 48 0 01-48-48V96a48 48 0 0148-48h98.75a32 32 0 0122.62 9.37l141.26 141.26a32 32 0 019.37 22.62z"/>
           </svg>
           项目基本信息
         </h3>
@@ -74,17 +68,46 @@
               placeholder="请输入项目名称" />
           </div>
 
-          <!-- 项目类型 -->
+          <!-- 项目类型(家装/工装) -->
           <div class="form-group">
             <label class="form-label">项目类型 <span class="required">*</span></label>
             <select
               class="form-select"
-              [(ngModel)]="projectInfo.type"
+              [(ngModel)]="projectInfo.projectType"
+              (change)="onProjectTypeChange()"
               [disabled]="!canEdit">
               <option value="">请选择项目类型</option>
-              <option value="整屋设计">整屋设计</option>
-              <option value="局部改造">局部改造</option>
-              <option value="软装设计">软装设计</option>
+              <option value="家装">家装</option>
+              <option value="工装">工装</option>
+            </select>
+          </div>
+
+          <!-- 渲染类型 -->
+          @if (projectInfo.projectType === '家装') {
+            <div class="form-group">
+              <label class="form-label">渲染类型 <span class="required">*</span></label>
+              <select
+                class="form-select"
+                [(ngModel)]="projectInfo.renderType"
+                (change)="onHomeSceneChange()"
+                [disabled]="!canEdit">
+                <option value="">请选择渲染类型</option>
+                <option value="静态单张">静态单张</option>
+                <option value="360全景">360全景</option>
+              </select>
+            </div>
+          }
+
+          <!-- 报价等级 -->
+          <div class="form-group">
+            <label class="form-label">报价等级</label>
+            <select
+              class="form-select"
+              [(ngModel)]="projectInfo.priceLevel"
+              [disabled]="!canEdit">
+              <option value="一级">一级报价(老客户)</option>
+              <option value="二级">二级报价(中端组)</option>
+              <option value="三级">三级报价(高端组)</option>
             </select>
           </div>
 
@@ -95,8 +118,7 @@
               class="form-input"
               type="date"
               [(ngModel)]="projectInfo.deadline"
-              [disabled]="!canEdit"
-              placeholder="请选择交付期限" />
+              [disabled]="!canEdit" />
           </div>
 
           <!-- 项目描述 -->
@@ -113,140 +135,412 @@
       </div>
     </div>
 
-    <!-- 3. 报价明细 -->
-    <div class="card quotation-card">
-      <div class="card-header">
-        <h3 class="card-title">
-          <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-            <rect x="112" y="128" width="288" height="320" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
-            <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M176 128V80a32 32 0 0132-32h96a32 32 0 0132 32v48"/>
-            <path fill="currentColor" d="M320 304h-32v-24a8 8 0 00-8-8h-48a8 8 0 00-8 8v24h-32a8 8 0 00-8 8v16a8 8 0 008 8h32v24a8 8 0 008 8h48a8 8 0 008-8v-24h32a8 8 0 008-8v-16a8 8 0 00-8-8z"/>
-          </svg>
-          报价明细
-        </h3>
-        <p class="card-subtitle">按空间和工序配置价格</p>
-      </div>
-      <div class="card-content">
-        @for (space of quotation.spaces; track space.name) {
-          <div class="space-section">
-            <div class="space-header">
-              <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                <path fill="currentColor" d="M440 432H72a40 40 0 01-40-40V120a40 40 0 0140-40h75.89a40 40 0 0122.19 6.72l27.84 18.56a40 40 0 0022.19 6.72H440a40 40 0 0140 40v240a40 40 0 01-40 40z"/>
-              </svg>
-              <h3>{{ space.name }}</h3>
+    <!-- 3. 场景选择(家装) -->
+    @if (projectInfo.projectType === '家装' && projectInfo.renderType) {
+      <div class="card scene-card">
+        <div class="card-header">
+          <h3 class="card-title">
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M261.56 101.28a8 8 0 00-11.06 0L66.4 277.15a8 8 0 00-2.47 5.79L63.9 448a32 32 0 0032 32H192a16 16 0 0016-16V328a8 8 0 018-8h80a8 8 0 018 8v136a16 16 0 0016 16h96.06a32 32 0 0032-32V282.94a8 8 0 00-2.47-5.79z"/>
+              <path fill="currentColor" d="M490.91 244.15l-74.8-71.56V64a16 16 0 00-16-16h-48a16 16 0 00-16 16v48l-57.92-55.38C272.77 51.14 264.71 48 256 48c-8.68 0-16.72 3.14-22.14 8.63l-212.7 203.5c-6.22 6-7 15.87-1.34 22.37A16 16 0 0043 284.12l213.3-204.2a8 8 0 0111.47 0l213.34 204.16a16 16 0 0023.22-.75c5.57-6.5 4.93-16.36-1.42-22.18z"/>
+            </svg>
+            场景快速选择
+          </h3>
+          <p class="card-subtitle">选择空间类型和风格,快速生成报价</p>
+        </div>
+        <div class="card-content">
+          <div class="form-list">
+            <!-- 空间类型 -->
+            <div class="form-group">
+              <label class="form-label">空间类型 <span class="required">*</span></label>
+              <div class="radio-group">
+                @for (spaceType of ['平层', '跃层', '挑空']; track spaceType) {
+                  <label class="radio-label">
+                    <input
+                      type="radio"
+                      name="spaceType"
+                      [value]="spaceType"
+                      [(ngModel)]="homeScenes.spaceType"
+                      (change)="onHomeSceneChange()"
+                      [disabled]="!canEdit" />
+                    <span class="radio-text">{{ spaceType }}</span>
+                  </label>
+                }
+              </div>
             </div>
 
-            <div class="process-grid">
-              @for (processType of processTypes; track processType.key) {
-                <div
-                  class="process-item"
-                  [class.enabled]="isProcessEnabled(space, processType.key)">
-                  <div class="process-header" (click)="canEdit && toggleProcess(space, processType.key)">
-                    <label class="checkbox-wrapper">
+            <!-- 风格等级 -->
+            <div class="form-group">
+              <label class="form-label">风格等级 <span class="required">*</span></label>
+              <div class="radio-group">
+                @for (styleLevel of Object.keys(styleLevels); track styleLevel) {
+                  <label class="radio-label">
+                    <input
+                      type="radio"
+                      name="styleLevel"
+                      [value]="styleLevel"
+                      [(ngModel)]="homeScenes.styleLevel"
+                      (change)="onHomeSceneChange()"
+                      [disabled]="!canEdit" />
+                    <span class="radio-text">{{ styleLevel }}</span>
+                  </label>
+                }
+              </div>
+            </div>
+          </div>
+
+          <!-- 房间选择 -->
+          @if (homeScenes.rooms.length > 0) {
+            <div class="room-selection">
+              <h4 class="section-title">选择房间</h4>
+              <div class="room-grid">
+                @for (room of homeScenes.rooms; track room.name) {
+                  <div class="room-item" [class.selected]="room.selected">
+                    <label class="room-checkbox">
                       <input
                         type="checkbox"
-                        class="checkbox-input"
-                        [checked]="isProcessEnabled(space, processType.key)"
+                        [(ngModel)]="room.selected"
                         [disabled]="!canEdit" />
                       <span class="checkbox-custom"></span>
+                      <div class="room-info">
+                        <h5>{{ room.name }}</h5>
+                        <p class="base-price">基础价: ¥{{ room.basePrice }}</p>
+                      </div>
                     </label>
-                    <span class="badge" [attr.data-color]="processType.color">
-                      {{ processType.name }}
-                    </span>
-                  </div>
 
-                  @if (isProcessEnabled(space, processType.key)) {
-                    <div class="process-inputs">
-                      <div class="input-group">
-                        <label class="input-label">单价</label>
-                        <div class="input-with-note">
+                    <!-- 加价选项 -->
+                    @if (room.selected && canEdit) {
+                      <div class="room-adjustments">
+                        <div class="adjustment-item">
+                          <label class="adjustment-label">功能区加价</label>
                           <input
-                            class="input-field"
                             type="number"
-                            [ngModel]="getProcessPrice(space, processType.key)"
-                            (ngModelChange)="setProcessPrice(space, processType.key, $event); onProcessChange()"
-                            [disabled]="!canEdit"
+                            class="adjustment-input"
+                            [(ngModel)]="room.adjustments.extraFunction"
+                            min="0"
+                            step="100"
                             placeholder="0" />
-                          <span class="input-note">元/{{ getProcessUnit(space, processType.key) }}</span>
+                          <span class="adjustment-unit">元</span>
                         </div>
-                      </div>
 
-                      <div class="input-group">
-                        <label class="input-label">数量</label>
-                        <div class="input-with-note">
+                        <div class="adjustment-item">
+                          <label class="adjustment-label">造型复杂度</label>
                           <input
-                            class="input-field"
                             type="number"
-                            [ngModel]="getProcessQuantity(space, processType.key)"
-                            (ngModelChange)="setProcessQuantity(space, processType.key, $event); onProcessChange()"
-                            [disabled]="!canEdit"
+                            class="adjustment-input"
+                            [(ngModel)]="room.adjustments.complexity"
+                            min="0"
+                            max="200"
+                            step="50"
                             placeholder="0" />
-                          <span class="input-note">{{ getProcessUnit(space, processType.key) }}</span>
+                          <span class="adjustment-unit">元</span>
+                        </div>
+
+                        <div class="adjustment-item">
+                          <label class="adjustment-checkbox">
+                            <input
+                              type="checkbox"
+                              [(ngModel)]="room.adjustments.design" />
+                            <span class="checkbox-text">需要设计服务(×2)</span>
+                          </label>
                         </div>
                       </div>
+                    }
+                  </div>
+                }
+              </div>
+
+              <button
+                class="btn btn-primary"
+                (click)="generateQuotation()"
+                [disabled]="!canEdit || !homeScenes.rooms.some(r => r.selected)">
+                生成报价表
+              </button>
+            </div>
+          }
+        </div>
+      </div>
+    }
 
-                      <div class="process-subtotal">
-                        小计: ¥{{ calculateProcessSubtotal(space, processType.key).toFixed(2) }}
+    <!-- 3. 场景选择(工装) -->
+    @if (projectInfo.projectType === '工装') {
+      <div class="card scene-card">
+        <div class="card-header">
+          <h3 class="card-title">
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M408 64H104a56.16 56.16 0 00-56 56v192a56.16 56.16 0 0056 56h40v72h32v-72h160v72h32v-72h40a56.16 56.16 0 0056-56V120a56.16 56.16 0 00-56-56zm24 248a24 24 0 01-24 24H104a24 24 0 01-24-24V120a24 24 0 0124-24h304a24 24 0 0124 24z"/>
+            </svg>
+            场景快速选择
+          </h3>
+          <p class="card-subtitle">选择业态类型,快速生成报价</p>
+        </div>
+        <div class="card-content">
+          <!-- 业态类型 -->
+          <div class="form-group">
+            <label class="form-label">业态类型 <span class="required">*</span></label>
+            <select
+              class="form-select"
+              [(ngModel)]="commercialScenes.businessType"
+              (change)="onCommercialTypeChange()"
+              [disabled]="!canEdit">
+              <option value="">请选择业态类型</option>
+              @for (type of businessTypes; track type) {
+                <option [value]="type">{{ type }}</option>
+              }
+            </select>
+          </div>
+
+          <!-- 空间选择 -->
+          @if (commercialScenes.spaces.length > 0) {
+            <div class="space-selection">
+              <h4 class="section-title">选择空间</h4>
+              <div class="space-grid">
+                @for (space of commercialScenes.spaces; track space.name; let idx = $index) {
+                  <div class="space-item" [class.selected]="space.selected">
+                    <label class="space-checkbox">
+                      <input
+                        type="checkbox"
+                        [(ngModel)]="space.selected"
+                        [disabled]="!canEdit" />
+                      <span class="checkbox-custom"></span>
+                      <div class="space-info">
+                        <h5>{{ space.name }}</h5>
+                        <p class="space-type">{{ space.type }}</p>
+                        <p class="base-price">基础价: ¥{{ space.basePrice }}</p>
                       </div>
+                    </label>
+
+                    <!-- 加价选项 -->
+                    @if (space.selected && canEdit) {
+                      <div class="space-adjustments">
+                        <div class="adjustment-item">
+                          <label class="adjustment-label">功能区加价</label>
+                          <input
+                            type="number"
+                            class="adjustment-input"
+                            [(ngModel)]="space.adjustments.extraFunction"
+                            min="0"
+                            step="400"
+                            placeholder="0" />
+                          <span class="adjustment-unit">元</span>
+                        </div>
+
+                        <div class="adjustment-item">
+                          <label class="adjustment-label">造型复杂度</label>
+                          <input
+                            type="number"
+                            class="adjustment-input"
+                            [(ngModel)]="space.adjustments.complexity"
+                            min="0"
+                            max="200"
+                            step="50"
+                            placeholder="0" />
+                          <span class="adjustment-unit">元</span>
+                        </div>
+
+                        <div class="adjustment-item">
+                          <label class="adjustment-checkbox">
+                            <input
+                              type="checkbox"
+                              [(ngModel)]="space.adjustments.design" />
+                            <span class="checkbox-text">需要设计服务(×2)</span>
+                          </label>
+                        </div>
+
+                        <div class="adjustment-item">
+                          <label class="adjustment-checkbox">
+                            <input
+                              type="checkbox"
+                              [(ngModel)]="space.adjustments.panoramic" />
+                            <span class="checkbox-text">全景渲染(×2)</span>
+                          </label>
+                        </div>
+                      </div>
+                    }
+                  </div>
+                }
+              </div>
+
+              <button
+                class="btn btn-primary"
+                (click)="generateQuotation()"
+                [disabled]="!canEdit || !commercialScenes.spaces.some(s => s.selected)">
+                生成报价表
+              </button>
+            </div>
+          }
+        </div>
+      </div>
+    }
+
+    <!-- 4. 报价明细 -->
+    @if (quotation.spaces.length > 0) {
+      <div class="card quotation-card">
+        <div class="card-header">
+          <h3 class="card-title">
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <rect x="112" y="128" width="288" height="320" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/>
+              <path fill="currentColor" d="M320 304h-32v-24a8 8 0 00-8-8h-48a8 8 0 00-8 8v24h-32a8 8 0 00-8 8v16a8 8 0 008 8h32v24a8 8 0 008 8h48a8 8 0 008-8v-24h32a8 8 0 008-8v-16a8 8 0 00-8-8z"/>
+            </svg>
+            报价明细
+          </h3>
+          <p class="card-subtitle">可手动调整工序和价格</p>
+        </div>
+        <div class="card-content">
+          @for (space of quotation.spaces; track space.name) {
+            <div class="space-section">
+              <div class="space-header">
+                <h3>{{ space.name }}</h3>
+              </div>
+
+              <div class="process-grid">
+                @for (processType of processTypes; track processType.key) {
+                  <div
+                    class="process-item"
+                    [class.enabled]="isProcessEnabled(space, processType.key)">
+                    <div class="process-header" (click)="canEdit && toggleProcess(space, processType.key)">
+                      <label class="checkbox-wrapper">
+                        <input
+                          type="checkbox"
+                          class="checkbox-input"
+                          [checked]="isProcessEnabled(space, processType.key)"
+                          [disabled]="!canEdit" />
+                        <span class="checkbox-custom"></span>
+                      </label>
+                      <span class="badge" [attr.data-color]="processType.color">
+                        {{ processType.name }}
+                      </span>
                     </div>
-                  }
-                </div>
-              }
+
+                    @if (isProcessEnabled(space, processType.key)) {
+                      <div class="process-inputs">
+                        <div class="input-group">
+                          <label class="input-label">单价</label>
+                          <div class="input-with-note">
+                            <input
+                              class="input-field"
+                              type="number"
+                              [ngModel]="getProcessPrice(space, processType.key)"
+                              (ngModelChange)="setProcessPrice(space, processType.key, $event); onProcessChange()"
+                              [disabled]="!canEdit"
+                              placeholder="0" />
+                            <span class="input-note">元/{{ getProcessUnit(space, processType.key) }}</span>
+                          </div>
+                        </div>
+
+                        <div class="input-group">
+                          <label class="input-label">数量</label>
+                          <div class="input-with-note">
+                            <input
+                              class="input-field"
+                              type="number"
+                              [ngModel]="getProcessQuantity(space, processType.key)"
+                              (ngModelChange)="setProcessQuantity(space, processType.key, $event); onProcessChange()"
+                              [disabled]="!canEdit"
+                              placeholder="0" />
+                            <span class="input-note">{{ getProcessUnit(space, processType.key) }}</span>
+                          </div>
+                        </div>
+
+                        <div class="process-subtotal">
+                          小计: ¥{{ calculateProcessSubtotal(space, processType.key).toFixed(2) }}
+                        </div>
+                      </div>
+                    }
+                  </div>
+                }
+              </div>
             </div>
-          </div>
-        }
+          }
 
-        <!-- 总价 -->
-        <div class="total-section">
-          <div class="total-label">报价总额</div>
-          <div class="total-amount">¥{{ quotation.total.toFixed(2) }}</div>
+          <!-- 总价 -->
+          <div class="total-section">
+            <div class="total-label">报价总额</div>
+            <div class="total-amount">¥{{ quotation.total.toFixed(2) }}</div>
+          </div>
         </div>
       </div>
-    </div>
+    }
 
-    <!-- 4. 设计师分配 -->
+    <!-- 5. 设计师分配 -->
     <div class="card designer-card">
       <div class="card-header">
         <h3 class="card-title">
           <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M402 168c-2.93 40.67-33.1 72-66 72s-63.12-31.32-66-72c-3-42.31 26.37-72 66-72s69 30.46 66 72z"/><path fill="currentColor" d="M336 304c-65.17 0-127.84 32.37-143.54 95.41-2.08 8.34 3.15 16.59 11.72 16.59h263.65c8.57 0 13.77-8.25 11.72-16.59C463.85 335.36 401.18 304 336 304z"/><path fill="currentColor" d="M200 185.94c-2.34 32.48-26.72 58.06-53 58.06s-50.7-25.57-53-58.06C91.61 152.15 115.34 128 147 128s55.39 24.77 53 57.94z"/><path fill="currentColor" d="M206 306c-18.05-8.27-37.93-11.45-59-11.45-52 0-102.1 25.85-114.65 76.2-1.65 6.66 2.53 13.25 9.37 13.25H154"/>
+            <path fill="currentColor" d="M402 168c-2.93 40.67-33.1 72-66 72s-63.12-31.32-66-72c-3-42.31 26.37-72 66-72s69 30.46 66 72z"/>
+            <path fill="currentColor" d="M336 304c-65.17 0-127.84 32.37-143.54 95.41-2.08 8.34 3.15 16.59 11.72 16.59h263.65c8.57 0 13.77-8.25 11.72-16.59C463.85 335.36 401.18 304 336 304z"/>
           </svg>
           设计师分配
         </h3>
-        <p class="card-subtitle">选择负责的组员</p>
+        <p class="card-subtitle">先选择项目组,再选择组员</p>
       </div>
       <div class="card-content">
-        @if (designers.length === 0) {
-          <div class="empty-state">
-            <svg class="icon-large" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-              <path fill="currentColor" d="M258.9 48C141.92 46.42 46.42 141.92 48 258.9c1.56 112.19 92.91 203.54 205.1 205.1 117 1.6 212.48-93.9 210.88-210.88C462.44 140.91 371.09 49.56 258.9 48zM385.32 375.25a4 4 0 01-6.14-.32 124.27 124.27 0 00-32.35-29.59C321.37 329 289.11 320 256 320s-65.37 9-90.83 25.34a124.24 124.24 0 00-32.35 29.58 4 4 0 01-6.14.32A175.32 175.32 0 0180 259c-1.63-97.31 78.22-178.76 175.57-179S432 158.81 432 256a175.32 175.32 0 01-46.68 119.25z"/>
-            </svg>
-            <p>暂无可用设计师</p>
-          </div>
-        } @else {
-          <div class="designer-grid">
-            @for (designer of designers; track designer.id) {
-              <div
-                class="designer-item"
-                [class.selected]="selectedDesigner?.id === designer.id"
-                (click)="canEdit && selectDesigner(designer)">
-                <div class="designer-avatar">
-                  @if (designer.get('data')?.avatar) {
-                    <img [src]="designer.get('data').avatar" alt="设计师头像" />
-                  } @else {
-                    <svg class="icon avatar-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                      <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm0 60a60 60 0 11-60 60 60 60 0 0160-60zm0 336c-63.6 0-119.92-36.47-146.39-89.68C109.74 329.09 176.24 296 256 296s146.26 33.09 146.39 58.32C376.92 407.53 319.6 444 256 444z"/>
+        <!-- 项目组选择 -->
+        <div class="department-section">
+          <h4 class="section-title">选择项目组</h4>
+          @if (departments.length === 0) {
+            <div class="empty-state">
+              <p>暂无可用项目组</p>
+            </div>
+          } @else {
+            <div class="department-grid">
+              @for (dept of departments; track dept.id) {
+                <div
+                  class="department-item"
+                  [class.selected]="selectedDepartment?.id === dept.id"
+                  (click)="canEdit && selectDepartment(dept)">
+                  <h5>{{ dept.get('name') }}</h5>
+                  <p>组长: {{ dept.get('leader')?.get('name') || '未指定' }}</p>
+                  @if (selectedDepartment?.id === dept.id) {
+                    <svg class="icon selected-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                      <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm-38 312.38L137.4 280.8a24 24 0 0133.94-33.94l50.2 50.2 95.74-95.74a24 24 0 0133.94 33.94z"/>
                     </svg>
                   }
                 </div>
-                <div class="designer-info">
-                  <h4>{{ designer.get('name') }}</h4>
-                  <p>{{ getDesignerWorkload(designer) }}</p>
-                </div>
-                @if (selectedDesigner?.id === designer.id) {
-                  <svg class="icon selected-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-                    <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm-38 312.38L137.4 280.8a24 24 0 0133.94-33.94l50.2 50.2 95.74-95.74a24 24 0 0133.94 33.94z"/>
-                  </svg>
+              }
+            </div>
+          }
+        </div>
+
+        <!-- 组员选择 -->
+        @if (selectedDepartment) {
+          <div class="designer-section">
+            <h4 class="section-title">选择组员</h4>
+            @if (loadingMembers) {
+              <div class="loading-spinner">
+                <div class="spinner-sm"></div>
+                <p>加载组员中...</p>
+              </div>
+            } @else if (departmentMembers.length === 0) {
+              <div class="empty-state">
+                <p>该项目组暂无可用组员</p>
+              </div>
+            } @else {
+              <div class="designer-grid">
+                @for (designer of departmentMembers; track designer.id) {
+                  <div
+                    class="designer-item"
+                    [class.selected]="selectedDesigner?.id === designer.id"
+                    (click)="canEdit && selectDesigner(designer)">
+                    <div class="designer-avatar">
+                      @if (designer.get('data')?.avatar) {
+                        <img [src]="designer.get('data').avatar" alt="设计师头像" />
+                      } @else {
+                        <svg class="icon avatar-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                          <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm0 60a60 60 0 11-60 60 60 60 0 0160-60zm0 336c-63.6 0-119.92-36.47-146.39-89.68C109.74 329.09 176.24 296 256 296s146.26 33.09 146.39 58.32C376.92 407.53 319.6 444 256 444z"/>
+                        </svg>
+                      }
+                    </div>
+                    <div class="designer-info">
+                      <h4>{{ designer.get('name') }}</h4>
+                      <p>{{ getDesignerWorkload(designer) }}</p>
+                    </div>
+                    @if (selectedDesigner?.id === designer.id) {
+                      <svg class="icon selected-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                        <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm-38 312.38L137.4 280.8a24 24 0 0133.94-33.94l50.2 50.2 95.74-95.74a24 24 0 0133.94 33.94z"/>
+                      </svg>
+                    }
+                  </div>
                 }
               </div>
             }
@@ -255,7 +549,7 @@
       </div>
     </div>
 
-    <!-- 5. 操作按钮 -->
+    <!-- 6. 操作按钮 -->
     @if (canEdit) {
       <div class="action-buttons">
         <button

+ 468 - 0
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -629,6 +629,474 @@
     }
   }
 
+  // 场景选择卡片(家装/工装)
+  .scene-card {
+    .section-title {
+      font-size: 15px;
+      font-weight: 600;
+      color: var(--dark-color);
+      margin: 20px 0 12px;
+
+      &:first-child {
+        margin-top: 0;
+      }
+    }
+
+    // 单选框组
+    .radio-group {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 12px;
+      margin-top: 8px;
+
+      .radio-label {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        padding: 8px 16px;
+        background: var(--light-color);
+        border: 2px solid transparent;
+        border-radius: 8px;
+        cursor: pointer;
+        transition: all 0.3s;
+        user-select: none;
+
+        &:hover {
+          background: var(--light-shade);
+        }
+
+        input[type="radio"] {
+          cursor: pointer;
+
+          &:checked {
+            + .radio-text {
+              font-weight: 600;
+              color: var(--primary-color);
+            }
+          }
+
+          &:disabled {
+            cursor: not-allowed;
+          }
+        }
+
+        .radio-text {
+          font-size: 14px;
+          color: var(--dark-color);
+          transition: all 0.3s;
+        }
+      }
+    }
+
+    // 房间选择(家装)
+    .room-selection {
+      margin-top: 24px;
+
+      .room-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+        gap: 16px;
+        margin-bottom: 20px;
+      }
+
+      .room-item {
+        border: 2px solid var(--light-shade);
+        border-radius: 8px;
+        padding: 14px;
+        background: var(--white);
+        transition: all 0.3s;
+
+        &.selected {
+          border-color: var(--primary-color);
+          background: rgba(var(--primary-rgb), 0.05);
+          box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.15);
+        }
+
+        .room-checkbox {
+          display: flex;
+          align-items: flex-start;
+          gap: 12px;
+          cursor: pointer;
+          user-select: none;
+
+          input[type="checkbox"] {
+            width: 20px;
+            height: 20px;
+            margin-top: 2px;
+            cursor: pointer;
+            flex-shrink: 0;
+
+            &:disabled {
+              cursor: not-allowed;
+            }
+          }
+
+          .checkbox-custom {
+            // 预留自定义复选框样式
+          }
+
+          .room-info {
+            flex: 1;
+
+            h5 {
+              margin: 0 0 6px;
+              font-size: 16px;
+              font-weight: 600;
+              color: var(--dark-color);
+            }
+
+            .base-price {
+              margin: 0;
+              font-size: 14px;
+              color: var(--success-color);
+              font-weight: 600;
+            }
+          }
+        }
+
+        .room-adjustments {
+          margin-top: 12px;
+          padding-top: 12px;
+          border-top: 1px solid var(--light-shade);
+          display: flex;
+          flex-direction: column;
+          gap: 10px;
+        }
+      }
+    }
+
+    // 空间选择(工装)
+    .space-selection {
+      margin-top: 24px;
+
+      .space-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+        gap: 16px;
+        margin-bottom: 20px;
+      }
+
+      .space-item {
+        border: 2px solid var(--light-shade);
+        border-radius: 8px;
+        padding: 14px;
+        background: var(--white);
+        transition: all 0.3s;
+
+        &.selected {
+          border-color: var(--primary-color);
+          background: rgba(var(--primary-rgb), 0.05);
+          box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.15);
+        }
+
+        .space-checkbox {
+          display: flex;
+          align-items: flex-start;
+          gap: 12px;
+          cursor: pointer;
+          user-select: none;
+
+          input[type="checkbox"] {
+            width: 20px;
+            height: 20px;
+            margin-top: 2px;
+            cursor: pointer;
+            flex-shrink: 0;
+
+            &:disabled {
+              cursor: not-allowed;
+            }
+          }
+
+          .space-info {
+            flex: 1;
+
+            h5 {
+              margin: 0 0 4px;
+              font-size: 16px;
+              font-weight: 600;
+              color: var(--dark-color);
+            }
+
+            .space-type {
+              margin: 0 0 6px;
+              font-size: 12px;
+              color: var(--medium-color);
+            }
+
+            .base-price {
+              margin: 0;
+              font-size: 14px;
+              color: var(--success-color);
+              font-weight: 600;
+            }
+          }
+        }
+
+        .space-adjustments {
+          margin-top: 12px;
+          padding-top: 12px;
+          border-top: 1px solid var(--light-shade);
+          display: flex;
+          flex-direction: column;
+          gap: 10px;
+        }
+      }
+    }
+
+    // 加价选项
+    .adjustment-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .adjustment-label {
+        font-size: 13px;
+        color: var(--medium-color);
+        min-width: 90px;
+        flex-shrink: 0;
+      }
+
+      .adjustment-input {
+        flex: 1;
+        padding: 6px 10px;
+        border: 1px solid var(--light-shade);
+        border-radius: 6px;
+        font-size: 13px;
+        background: var(--light-color);
+        transition: all 0.3s;
+
+        &:focus {
+          outline: none;
+          border-color: var(--primary-color);
+          background: var(--white);
+        }
+
+        // 隐藏 number input 的上下箭头
+        &[type="number"] {
+          -moz-appearance: textfield;
+
+          &::-webkit-outer-spin-button,
+          &::-webkit-inner-spin-button {
+            -webkit-appearance: none;
+            margin: 0;
+          }
+        }
+      }
+
+      .adjustment-unit {
+        font-size: 13px;
+        color: var(--medium-color);
+        flex-shrink: 0;
+      }
+
+      .adjustment-checkbox {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        cursor: pointer;
+        user-select: none;
+
+        input[type="checkbox"] {
+          width: 16px;
+          height: 16px;
+          cursor: pointer;
+        }
+
+        .checkbox-text {
+          font-size: 13px;
+          color: var(--dark-color);
+        }
+      }
+    }
+  }
+
+  // 设计师分配 - 项目组和组员选择
+  .designer-card {
+    .section-title {
+      font-size: 15px;
+      font-weight: 600;
+      color: var(--dark-color);
+      margin: 0 0 12px;
+
+      &:not(:first-child) {
+        margin-top: 24px;
+      }
+    }
+
+    .loading-spinner {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 12px;
+      padding: 40px 20px;
+
+      .spinner-sm {
+        width: 24px;
+        height: 24px;
+        border: 3px solid var(--light-shade);
+        border-top-color: var(--primary-color);
+        border-radius: 50%;
+        animation: spin 0.8s linear infinite;
+      }
+
+      p {
+        margin: 0;
+        font-size: 14px;
+        color: var(--medium-color);
+      }
+    }
+
+    // 项目组选择
+    .department-section {
+      margin-bottom: 24px;
+
+      .department-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+        gap: 12px;
+      }
+
+      .department-item {
+        position: relative;
+        padding: 14px;
+        border: 2px solid var(--light-shade);
+        border-radius: 8px;
+        background: var(--white);
+        cursor: pointer;
+        transition: all 0.3s;
+
+        &:hover {
+          border-color: var(--primary-color);
+          background: rgba(var(--primary-rgb), 0.03);
+          transform: translateY(-2px);
+        }
+
+        &.selected {
+          border-color: var(--primary-color);
+          background: rgba(var(--primary-rgb), 0.08);
+          box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.2);
+        }
+
+        h5 {
+          margin: 0 0 6px;
+          font-size: 15px;
+          font-weight: 600;
+          color: var(--dark-color);
+          padding-right: 24px;
+        }
+
+        p {
+          margin: 0;
+          font-size: 13px;
+          color: var(--medium-color);
+        }
+
+        .selected-icon {
+          position: absolute;
+          top: 10px;
+          right: 10px;
+          width: 22px;
+          height: 22px;
+          color: var(--primary-color);
+        }
+      }
+    }
+
+    // 组员选择
+    .designer-section {
+      .designer-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+        gap: 12px;
+      }
+
+      .designer-item {
+        position: relative;
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        padding: 12px;
+        border: 2px solid var(--light-shade);
+        border-radius: 8px;
+        background: var(--white);
+        cursor: pointer;
+        transition: all 0.3s;
+
+        &:hover {
+          border-color: var(--primary-color);
+          background: rgba(var(--primary-rgb), 0.03);
+          transform: translateX(2px);
+        }
+
+        &.selected {
+          border-color: var(--primary-color);
+          background: rgba(var(--primary-rgb), 0.08);
+          box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.2);
+
+          .designer-info h4 {
+            color: var(--primary-color);
+          }
+        }
+
+        .designer-avatar {
+          width: 48px;
+          height: 48px;
+          border-radius: 50%;
+          overflow: hidden;
+          background: var(--light-shade);
+          flex-shrink: 0;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+
+          .avatar-icon {
+            width: 28px;
+            height: 28px;
+            color: var(--medium-color);
+          }
+        }
+
+        .designer-info {
+          flex: 1;
+          min-width: 0;
+
+          h4 {
+            margin: 0 0 4px;
+            font-size: 15px;
+            font-weight: 600;
+            color: var(--dark-color);
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            transition: color 0.3s;
+          }
+
+          p {
+            margin: 0;
+            font-size: 12px;
+            color: var(--medium-color);
+          }
+        }
+
+        .selected-icon {
+          position: absolute;
+          top: 8px;
+          right: 8px;
+          width: 20px;
+          height: 20px;
+          color: var(--primary-color);
+        }
+      }
+    }
+  }
+
   // 操作按钮
   .action-buttons {
     display: grid;

+ 364 - 108
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -2,8 +2,18 @@ import { Component, OnInit, Input } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { IonicModule } from '@ionic/angular';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
+import {
+  QUOTATION_PRICE_TABLE,
+  STYLE_LEVELS,
+  SPACE_TYPES,
+  BUSINESS_TYPES,
+  HOME_DEFAULT_ROOMS,
+  ADJUSTMENT_RULES,
+  getBasePrice,
+  calculateFinalPrice,
+  getDefaultProcesses
+} from '../../../config/quotation-rules';
 
 const Parse = FmodeParse.with('nova');
 
@@ -11,16 +21,15 @@ const Parse = FmodeParse.with('nova');
  * 订单分配阶段组件
  *
  * 功能:
- * 1. 显示客户基础信息
- * 2. 编辑项目基本信息(项目名称、类型、交付期限)
- * 3. 编辑报价明细(按空间+工序类型)
- * 4. 分配设计师(组员)
- * 5. 提交审批
+ * 1. 家装/工装项目类型选择
+ * 2. 快速场景选择,自动生成报价表(基于quotation.md规则)
+ * 3. 项目组(Department)→ 组员(Profile)两级设计师分配
+ * 4. 提交审批
  */
 @Component({
   selector: 'app-stage-order',
   standalone: true,
-  imports: [CommonModule, IonicModule, FormsModule],
+  imports: [CommonModule, FormsModule],
   templateUrl: './stage-order.component.html',
   styleUrls: ['./stage-order.component.scss']
 })
@@ -33,60 +42,58 @@ export class StageOrderComponent implements OnInit {
   // 项目基本信息
   projectInfo = {
     title: '',
-    type: '整屋设计', // 整屋设计 | 局部改造 | 软装设计
+    projectType: '', // 家装 | 工装
+    renderType: '', // 静态单张 | 360全景
     deadline: '',
-    description: ''
+    description: '',
+    priceLevel: '一级' // 一级(老客户) | 二级(中端组) | 三级(高端组)
   };
 
-  // 报价明细(按空间+工序)
+  // 场景选择(家装)
+  homeScenes = {
+    spaceType: '', // 平层 | 跃层 | 挑空
+    styleLevel: '', // 基础风格组 | 中级风格组 | 高级风格组 | 顶级风格组
+    rooms: [] as Array<{
+      name: string; // 客厅、主卧、次卧等
+      selected: boolean;
+      basePrice: number;
+      adjustments: {
+        extraFunction: number; // 功能区加价
+        complexity: number; // 造型复杂度加价
+        design: boolean; // 是否需要设计服务(×2)
+      };
+    }>
+  };
+
+  // 场景选择(工装)
+  commercialScenes = {
+    businessType: '', // 办公空间 | 商业空间 | 娱乐空间 | 酒店餐厅 | 公共空间
+    spaces: [] as Array<{
+      name: string;
+      type: string; // 门厅空间 | 封闭空间
+      selected: boolean;
+      basePrice: number;
+      adjustments: {
+        extraFunction: number;
+        complexity: number;
+        design: boolean;
+        panoramic: boolean; // 是否全景渲染(×2)
+      };
+    }>
+  };
+
+  // 报价明细(最终生成)
   quotation = {
-    spaces: [
-      {
-        name: '客厅',
-        processes: {
-          modeling: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          softDecor: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          rendering: { enabled: false, price: 0, unit: '张', quantity: 0 },
-          postProcess: { enabled: false, price: 0, unit: '张', quantity: 0 }
-        }
-      },
-      {
-        name: '主卧',
-        processes: {
-          modeling: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          softDecor: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          rendering: { enabled: false, price: 0, unit: '张', quantity: 0 },
-          postProcess: { enabled: false, price: 0, unit: '张', quantity: 0 }
-        }
-      },
-      {
-        name: '次卧',
-        processes: {
-          modeling: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          softDecor: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          rendering: { enabled: false, price: 0, unit: '张', quantity: 0 },
-          postProcess: { enabled: false, price: 0, unit: '张', quantity: 0 }
-        }
-      },
-      {
-        name: '厨房',
-        processes: {
-          modeling: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          softDecor: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          rendering: { enabled: false, price: 0, unit: '张', quantity: 0 },
-          postProcess: { enabled: false, price: 0, unit: '张', quantity: 0 }
-        }
-      },
-      {
-        name: '卫生间',
-        processes: {
-          modeling: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          softDecor: { enabled: false, price: 0, unit: '㎡', quantity: 0 },
-          rendering: { enabled: false, price: 0, unit: '张', quantity: 0 },
-          postProcess: { enabled: false, price: 0, unit: '张', quantity: 0 }
-        }
-      }
-    ],
+    spaces: [] as Array<{
+      name: string;
+      processes: {
+        modeling: { enabled: boolean; price: number; unit: string; quantity: number };
+        softDecor: { enabled: boolean; price: number; unit: string; quantity: number };
+        rendering: { enabled: boolean; price: number; unit: string; quantity: number };
+        postProcess: { enabled: boolean; price: number; unit: string; quantity: number };
+      };
+      subtotal: number;
+    }>,
     total: 0
   };
 
@@ -98,23 +105,34 @@ export class StageOrderComponent implements OnInit {
     { key: 'postProcess', name: '后期', color: 'success' }
   ];
 
-  // 可选设计师列表
-  designers: FmodeObject[] = [];
+  // 项目组(Department)列表
+  departments: FmodeObject[] = [];
+  selectedDepartment: FmodeObject | null = null;
+
+  // 项目组成员(Profile)列表
+  departmentMembers: FmodeObject[] = [];
   selectedDesigner: FmodeObject | null = null;
 
   // 加载状态
   loading: boolean = true;
   saving: boolean = false;
+  loadingMembers: boolean = false;
 
   // 路由参数
   cid: string = '';
   projectId: string = '';
 
+  // 从配置导入报价表和规则
+  priceTable = QUOTATION_PRICE_TABLE;
+  styleLevels = STYLE_LEVELS;
+  spaceTypes = SPACE_TYPES;
+  businessTypes = BUSINESS_TYPES;
+  homeDefaultRooms = HOME_DEFAULT_ROOMS;
+  adjustmentRules = ADJUSTMENT_RULES;
+
   constructor(private route: ActivatedRoute) {}
 
   async ngOnInit() {
-    // 尝试从父组件获取数据(如果通过@Input传入)
-    // 否则从路由参数加载
     if (!this.project || !this.customer || !this.currentUser) {
       this.cid = this.route.parent?.snapshot.paramMap.get('cid') || '';
       this.projectId = this.route.parent?.snapshot.paramMap.get('projectId') || '';
@@ -130,16 +148,15 @@ export class StageOrderComponent implements OnInit {
     try {
       this.loading = true;
 
-      // 如果没有传入project,从路由参数加载
+      // 使用FmodeParse加载项目、客户、当前用户
       if (!this.project && this.projectId) {
-        const query = new Parse.Query('Project');
-        query.include('customer', 'assignee');
+        const query = Parse.Query.from('Project');
+        query.include('customer', 'assignee', 'department');
         this.project = await query.get(this.projectId);
         this.customer = this.project.get('customer');
         this.selectedDesigner = this.project.get('assignee');
       }
 
-      // 如果没有传入currentUser,加载当前用户
       if (!this.currentUser && this.cid) {
         const { WxworkSDK } = await import('fmode-ng/core');
         const wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
@@ -149,32 +166,50 @@ export class StageOrderComponent implements OnInit {
         this.canEdit = ['客服', '组员', '组长', '管理员'].includes(role);
       }
 
-      // 1. 加载项目基本信息
+      // 加载项目信息
       if (this.project) {
         this.projectInfo.title = this.project.get('title') || '';
-        this.projectInfo.type = this.project.get('type') || '整屋设计';
+        this.projectInfo.projectType = this.project.get('projectType') || '';
+        this.projectInfo.renderType = this.project.get('renderType') || '';
         this.projectInfo.deadline = this.project.get('deadline') || '';
         this.projectInfo.description = this.project.get('description') || '';
 
-        // 加载报价明细
         const data = this.project.get('data') || {};
         if (data.quotation) {
           this.quotation = data.quotation;
         }
 
-        // 加载已分配设计师
+        if (data.priceLevel) {
+          this.projectInfo.priceLevel = data.priceLevel;
+        }
+
+        // 加载场景选择数据
+        if (data.homeScenes) {
+          this.homeScenes = data.homeScenes;
+        }
+        if (data.commercialScenes) {
+          this.commercialScenes = data.commercialScenes;
+        }
+
+        // 加载已分配的项目组和设计师
+        const department = this.project.get('department');
+        if (department) {
+          this.selectedDepartment = department;
+          await this.loadDepartmentMembers(department.id);
+        }
+
         const assignee = this.project.get('assignee');
         if (assignee) {
           this.selectedDesigner = assignee;
         }
       }
 
-      // 2. 加载可用设计师列表
-      const query = new Parse.Query('Profile');
-      query.equalTo('roleName', '组员');
-      query.equalTo('isDeleted', false);
-      query.ascending('name');
-      this.designers = await query.find();
+      // 使用FmodeParse加载项目组列表(Department表)
+      const deptQuery = Parse.Query.from('Department');
+      deptQuery.equalTo('type', 'project');
+      deptQuery.notEqualTo('isDeleted', true);
+      deptQuery.ascending('name');
+      this.departments = await deptQuery.find();
 
     } catch (err) {
       console.error('加载失败:', err);
@@ -183,6 +218,173 @@ export class StageOrderComponent implements OnInit {
     }
   }
 
+  /**
+   * 项目类型改变,初始化场景数据
+   */
+  onProjectTypeChange() {
+    this.quotation.spaces = [];
+    this.quotation.total = 0;
+
+    if (this.projectInfo.projectType === '家装') {
+      this.homeScenes = {
+        spaceType: '',
+        styleLevel: '',
+        rooms: []
+      };
+      this.commercialScenes = { businessType: '', spaces: [] };
+    } else if (this.projectInfo.projectType === '工装') {
+      this.commercialScenes = {
+        businessType: '',
+        spaces: []
+      };
+      this.homeScenes = { spaceType: '', styleLevel: '', rooms: [] };
+    }
+  }
+
+  /**
+   * 家装场景:空间类型和风格等级改变,生成预设房间列表
+   */
+  onHomeSceneChange() {
+    if (!this.homeScenes.spaceType || !this.homeScenes.styleLevel) {
+      this.homeScenes.rooms = [];
+      return;
+    }
+
+    const renderType = this.projectInfo.renderType || '静态单张';
+
+    // 使用配置文件中的工具函数获取价格
+    this.homeScenes.rooms = this.homeDefaultRooms.map(name => {
+      const spaceType = name.includes('卧') ? '卧室' : this.homeScenes.spaceType;
+      const basePrice = getBasePrice(
+        this.projectInfo.priceLevel,
+        '家装',
+        renderType,
+        spaceType,
+        this.homeScenes.styleLevel
+      );
+
+      return {
+        name,
+        selected: false,
+        basePrice,
+        adjustments: {
+          extraFunction: 0,
+          complexity: 0,
+          design: false
+        }
+      };
+    });
+  }
+
+  /**
+   * 工装场景:业态类型改变,生成预设空间列表
+   */
+  onCommercialTypeChange() {
+    if (!this.commercialScenes.businessType) {
+      this.commercialScenes.spaces = [];
+      return;
+    }
+
+    const renderType = '静态单张';
+
+    this.commercialScenes.spaces = [
+      {
+        name: '门厅',
+        type: '门厅空间',
+        selected: false,
+        basePrice: getBasePrice(
+          this.projectInfo.priceLevel,
+          '工装',
+          renderType,
+          '门厅空间',
+          undefined,
+          this.commercialScenes.businessType
+        ),
+        adjustments: {
+          extraFunction: 0,
+          complexity: 0,
+          design: false,
+          panoramic: false
+        }
+      },
+      {
+        name: '封闭空间1',
+        type: '封闭空间',
+        selected: false,
+        basePrice: getBasePrice(
+          this.projectInfo.priceLevel,
+          '工装',
+          renderType,
+          '封闭空间',
+          undefined,
+          this.commercialScenes.businessType
+        ),
+        adjustments: {
+          extraFunction: 0,
+          complexity: 0,
+          design: false,
+          panoramic: false
+        }
+      }
+    ];
+  }
+
+  /**
+   * 生成最终报价明细
+   */
+  generateQuotation() {
+    this.quotation.spaces = [];
+
+    if (this.projectInfo.projectType === '家装') {
+      // 遍历选中的房间
+      for (const room of this.homeScenes.rooms.filter(r => r.selected)) {
+        // 使用配置文件中的价格计算函数
+        const finalPrice = calculateFinalPrice(
+          room.basePrice,
+          '家装',
+          {
+            extraFunction: room.adjustments.extraFunction,
+            complexity: room.adjustments.complexity,
+            design: room.adjustments.design
+          }
+        );
+
+        // 使用配置文件生成默认工序
+        const processes = getDefaultProcesses('家装', finalPrice);
+
+        this.quotation.spaces.push({
+          name: room.name,
+          processes: processes as any,
+          subtotal: finalPrice + processes.postProcess.price
+        });
+      }
+    } else if (this.projectInfo.projectType === '工装') {
+      // 遍历选中的空间
+      for (const space of this.commercialScenes.spaces.filter(s => s.selected)) {
+        const finalPrice = calculateFinalPrice(
+          space.basePrice,
+          '工装',
+          {
+            extraFunction: space.adjustments.extraFunction,
+            complexity: space.adjustments.complexity,
+            design: space.adjustments.design,
+            panoramic: space.adjustments.panoramic
+          }
+        );
+
+        const processes = getDefaultProcesses('工装', finalPrice);
+
+        this.quotation.spaces.push({
+          name: space.name,
+          processes: processes as any,
+          subtotal: finalPrice + processes.postProcess.price
+        });
+      }
+    }
+
+    this.calculateTotal();
+  }
+
   /**
    * 计算报价总额
    */
@@ -200,40 +402,80 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 切换工序启用状态
+   * 选择项目组(Department)
    */
-  toggleProcess(space: any, processKey: string) {
-    const process = space.processes[processKey];
-    process.enabled = !process.enabled;
-    if (!process.enabled) {
-      process.price = 0;
-      process.quantity = 0;
-    }
-    this.calculateTotal();
+  async selectDepartment(department: FmodeObject) {
+    if (!this.canEdit) return;
+
+    this.selectedDepartment = department;
+    this.selectedDesigner = null;
+    this.departmentMembers = [];
+
+    await this.loadDepartmentMembers(department.id);
   }
 
   /**
-   * 工序价格或数量变化
+   * 加载项目组成员
    */
-  onProcessChange() {
-    this.calculateTotal();
+  async loadDepartmentMembers(departmentId: string) {
+    try {
+      this.loadingMembers = true;
+
+      // 使用FmodeParse查询项目组的组员(Profile表,roleName为"组员")
+      const query = Parse.Query.from('Profile');
+      query.equalTo('roleName', '组员');
+      query.notEqualTo('isDeleted', true);
+      query.ascending('name');
+
+      // TODO: 根据实际数据结构过滤特定项目组的成员
+      // 如果Profile表有department字段,可以这样查询:
+      // const deptPointer = Parse.Object.from('Department', departmentId);
+      // query.equalTo('department', deptPointer);
+
+      this.departmentMembers = await query.find();
+    } catch (err) {
+      console.error('加载项目组成员失败:', err);
+    } finally {
+      this.loadingMembers = false;
+    }
   }
 
   /**
    * 选择设计师
    */
   selectDesigner(designer: FmodeObject) {
+    if (!this.canEdit) return;
     this.selectedDesigner = designer;
   }
 
   /**
-   * 获取设计师工作量统计
+   * 获取设计师工作量
    */
   getDesignerWorkload(designer: FmodeObject): string {
     // TODO: 查询该设计师当前进行中的项目数量
     return '3个项目';
   }
 
+  /**
+   * 切换工序启用状态
+   */
+  toggleProcess(space: any, processKey: string) {
+    const process = space.processes[processKey];
+    process.enabled = !process.enabled;
+    if (!process.enabled) {
+      process.price = 0;
+      process.quantity = 0;
+    }
+    this.calculateTotal();
+  }
+
+  /**
+   * 工序价格或数量变化
+   */
+  onProcessChange() {
+    this.calculateTotal();
+  }
+
   /**
    * 保存草稿
    */
@@ -243,18 +485,23 @@ export class StageOrderComponent implements OnInit {
     try {
       this.saving = true;
 
-      // 更新项目基本信息
       this.project.set('title', this.projectInfo.title);
-      this.project.set('type', this.projectInfo.type);
+      this.project.set('projectType', this.projectInfo.projectType);
+      this.project.set('renderType', this.projectInfo.renderType);
       this.project.set('deadline', this.projectInfo.deadline);
       this.project.set('description', this.projectInfo.description);
 
-      // 保存报价明细到 data 字段
       const data = this.project.get('data') || {};
       data.quotation = this.quotation;
+      data.priceLevel = this.projectInfo.priceLevel;
+      data.homeScenes = this.homeScenes;
+      data.commercialScenes = this.commercialScenes;
       this.project.set('data', data);
 
-      // 保存设计师分配
+      if (this.selectedDepartment) {
+        this.project.set('department', this.selectedDepartment.toPointer());
+      }
+
       if (this.selectedDesigner) {
         this.project.set('assignee', this.selectedDesigner.toPointer());
       }
@@ -276,12 +523,17 @@ export class StageOrderComponent implements OnInit {
   async submitForApproval() {
     if (!this.project || !this.canEdit) return;
 
-    // 验证必填项
+    // 验证
     if (!this.projectInfo.title.trim()) {
       alert('请填写项目名称');
       return;
     }
 
+    if (!this.projectInfo.projectType) {
+      alert('请选择项目类型');
+      return;
+    }
+
     if (!this.projectInfo.deadline) {
       alert('请选择交付期限');
       return;
@@ -292,6 +544,11 @@ export class StageOrderComponent implements OnInit {
       return;
     }
 
+    if (!this.selectedDepartment) {
+      alert('请选择项目组');
+      return;
+    }
+
     if (!this.selectedDesigner) {
       alert('请选择设计师');
       return;
@@ -300,14 +557,11 @@ export class StageOrderComponent implements OnInit {
     try {
       this.saving = true;
 
-      // 1. 保存项目信息
       await this.saveDraft();
 
-      // 2. 更新项目状态
       this.project.set('status', '待审核');
       this.project.set('currentStage', '订单分配');
 
-      // 3. 添加审批记录到 data 字段
       const data = this.project.get('data') || {};
       const approvalHistory = data.approvalHistory || [];
 
@@ -321,6 +575,10 @@ export class StageOrderComponent implements OnInit {
         submitTime: new Date(),
         status: 'pending',
         quotationTotal: this.quotation.total,
+        department: {
+          id: this.selectedDepartment.id,
+          name: this.selectedDepartment.get('name')
+        },
         assignee: {
           id: this.selectedDesigner.id,
           name: this.selectedDesigner.get('name')
@@ -332,8 +590,6 @@ export class StageOrderComponent implements OnInit {
 
       await this.project.save();
 
-      // 4. TODO: 发送企微消息通知组长审批
-
       alert('提交成功,等待组长审批');
 
     } catch (err) {
@@ -345,7 +601,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 检查工序是否启用
+   * 辅助方法:检查工序是否启用
    */
   isProcessEnabled(space: any, processKey: string): boolean {
     const process = (space.processes as any)[processKey];
@@ -353,7 +609,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 设置工序价格
+   * 辅助方法:设置工序价格
    */
   setProcessPrice(space: any, processKey: string, value: any): void {
     const process = (space.processes as any)[processKey];
@@ -363,7 +619,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 设置工序数量
+   * 辅助方法:设置工序数量
    */
   setProcessQuantity(space: any, processKey: string, value: any): void {
     const process = (space.processes as any)[processKey];
@@ -373,7 +629,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 获取工序价格
+   * 辅助方法:获取工序价格
    */
   getProcessPrice(space: any, processKey: string): number {
     const process = (space.processes as any)[processKey];
@@ -381,7 +637,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 获取工序数量
+   * 辅助方法:获取工序数量
    */
   getProcessQuantity(space: any, processKey: string): number {
     const process = (space.processes as any)[processKey];
@@ -389,7 +645,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 获取工序单位
+   * 辅助方法:获取工序单位
    */
   getProcessUnit(space: any, processKey: string): string {
     const process = (space.processes as any)[processKey];
@@ -397,7 +653,7 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 计算工序小计
+   * 辅助方法:计算工序小计
    */
   calculateProcessSubtotal(space: any, processKey: string): number {
     const process = (space.processes as any)[processKey];
@@ -405,12 +661,12 @@ export class StageOrderComponent implements OnInit {
   }
 
   /**
-   * 获取项目类型图标
+   * 辅助方法:获取项目类型图标
    */
   getProjectTypeIcon(type: string): string {
     const iconMap: any = {
-      '整屋设计': 'home-outline',
-      '局部改造': 'construct-outline',
+      '家装': 'home-outline',
+      '工装': 'business-outline',
       '软装设计': 'color-palette-outline'
     };
     return iconMap[type] || 'document-outline';

+ 40 - 49
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -100,11 +100,13 @@ export class StageRequirementsComponent implements OnInit {
   generating: boolean = false;
   saving: boolean = false;
 
+  // 使用懒加载方式注入服务,避免循环依赖
+  private uploadService?: ProjectUploadService;
+  private aiService?: ProjectAIService;
+  private wxworkService?: WxworkSDKService;
+
   constructor(
-    private route: ActivatedRoute,
-    private uploadService: ProjectUploadService,
-    private aiService: ProjectAIService,
-    private wxworkService: WxworkSDKService
+    private route: ActivatedRoute
   ) {}
 
   async ngOnInit() {
@@ -135,8 +137,10 @@ export class StageRequirementsComponent implements OnInit {
 
       // 如果没有传入currentUser,加载当前用户
       if (!this.currentUser && this.cid) {
-        await this.wxworkService.initialize(this.cid, 'crm');
-        this.currentUser = await this.wxworkService.getCurrentUser();
+        // 动态导入WxworkSDK避免循环依赖
+        const { WxworkSDK } = await import('fmode-ng/core');
+        const wxwork = new WxworkSDK({ cid: this.cid, appId: 'crm' });
+        this.currentUser = await wxwork.getCurrentUser();
 
         const role = this.currentUser?.get('roleName') || '';
         this.canEdit = ['客服', '组员', '组长', '管理员'].includes(role);
@@ -208,14 +212,14 @@ export class StageRequirementsComponent implements OnInit {
     const file = event.target.files[0];
     if (!file) return;
 
-    // 验证文件类型
-    if (!this.uploadService.validateFileType(file, ['image/*'])) {
+    // 简单的文件类型验证
+    if (!file.type.startsWith('image/')) {
       alert('请上传图片文件');
       return;
     }
 
     // 验证文件大小 (10MB)
-    if (!this.uploadService.validateFileSize(file, 10)) {
+    if (file.size > 10 * 1024 * 1024) {
       alert('图片大小不能超过10MB');
       return;
     }
@@ -223,15 +227,10 @@ export class StageRequirementsComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 上传文件到Parse Server
-      const url = await this.uploadService.uploadFile(file, {
-        compress: true,
-        maxWidth: 1920,
-        maxHeight: 1920,
-        onProgress: (progress: number) => {
-          console.log('上传进度:', progress);
-        }
-      });
+      // 直接使用Parse File上传
+      const parseFile = new Parse.File(file.name, file);
+      await parseFile.save();
+      const url = parseFile.url();
 
       this.referenceImages.push({
         url: url,
@@ -266,24 +265,15 @@ export class StageRequirementsComponent implements OnInit {
     if (!file) return;
 
     // 验证文件类型
-    const allowedTypes = [
-      'application/acad',
-      'application/x-acad',
-      'application/dxf',
-      'image/vnd.dwg',
-      'image/x-dwg',
-      'application/pdf'
-    ];
-    if (!allowedTypes.includes(file.type) &&
-        !file.name.endsWith('.dwg') &&
-        !file.name.endsWith('.dxf') &&
-        !file.name.endsWith('.pdf')) {
+    const allowedExtensions = ['.dwg', '.dxf', '.pdf'];
+    const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+    if (!allowedExtensions.includes(fileExtension)) {
       alert('请上传CAD文件(.dwg/.dxf)或PDF文件');
       return;
     }
 
     // 验证文件大小 (50MB)
-    if (!this.uploadService.validateFileSize(file, 50)) {
+    if (file.size > 50 * 1024 * 1024) {
       alert('文件大小不能超过50MB');
       return;
     }
@@ -291,12 +281,10 @@ export class StageRequirementsComponent implements OnInit {
     try {
       this.uploading = true;
 
-      // 上传文件到Parse Server
-      const url = await this.uploadService.uploadFile(file, {
-        onProgress: (progress: number) => {
-          console.log('上传进度:', progress);
-        }
-      });
+      // 直接使用Parse File上传
+      const parseFile = new Parse.File(file.name, file);
+      await parseFile.save();
+      const url = parseFile.url();
 
       this.cadFiles.push({
         url: url,
@@ -373,18 +361,21 @@ export class StageRequirementsComponent implements OnInit {
       // 收集参考图片URL
       const imageUrls = this.referenceImages.map(img => img.url);
 
-      // 调用AI服务生成方案
-      const result = await this.aiService.generateDesignSolution(
-        this.requirements,
-        {
-          images: imageUrls.length > 0 ? imageUrls : undefined,
-          onProgress: (content) => {
-            console.log('生成进度:', content.length);
-          }
-        }
-      );
-
-      this.aiSolution = result;
+      // TODO: 调用AI服务生成方案
+      // 暂时使用mock数据
+      this.aiSolution = {
+        generated: true,
+        content: '基于您的需求,我们为您设计了以下方案...',
+        spaces: this.requirements.spaces.map(space => ({
+          name: space.name,
+          styleDescription: `${this.requirements.stylePreference}风格设计`,
+          colorPalette: ['#FFFFFF', '#F5F5DC', '#8B4513'],
+          materials: ['实木', '大理石', '布艺'],
+          furnitureRecommendations: ['沙发', '茶几', '电视柜']
+        })),
+        estimatedCost: this.requirements.budget.max || 100000,
+        timeline: '预计30-45个工作日'
+      };
 
       // 保存到项目数据
       await this.saveDraft();