Просмотр исходного кода

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

0235711 5 месяцев назад
Родитель
Сommit
0488d05344
76 измененных файлов с 21819 добавлено и 2740 удалено
  1. 4 0
      .vscode/extensions.json
  2. 3 1
      src/app/app.routes.ts
  3. 47 0
      src/app/models/project.model.ts
  4. 220 0
      src/app/models/work-hour.model.ts
  5. 243 0
      src/app/pages/customer-service/consultation-order/components/designer-assignment/designer-assignment.component.html
  6. 567 0
      src/app/pages/customer-service/consultation-order/components/designer-assignment/designer-assignment.component.scss
  7. 350 0
      src/app/pages/customer-service/consultation-order/components/designer-assignment/designer-assignment.component.ts
  8. 317 0
      src/app/pages/customer-service/consultation-order/components/designer-calendar/designer-calendar.component.html
  9. 843 0
      src/app/pages/customer-service/consultation-order/components/designer-calendar/designer-calendar.component.scss
  10. 419 0
      src/app/pages/customer-service/consultation-order/components/designer-calendar/designer-calendar.component.ts
  11. 100 0
      src/app/pages/customer-service/consultation-order/components/order-form/order-form.component.html
  12. 176 0
      src/app/pages/customer-service/consultation-order/components/order-form/order-form.component.scss
  13. 113 0
      src/app/pages/customer-service/consultation-order/components/order-form/order-form.component.ts
  14. 167 0
      src/app/pages/customer-service/consultation-order/components/quotation-detail/quotation-detail.component.html
  15. 348 0
      src/app/pages/customer-service/consultation-order/components/quotation-detail/quotation-detail.component.scss
  16. 212 0
      src/app/pages/customer-service/consultation-order/components/quotation-detail/quotation-detail.component.ts
  17. 7 11
      src/app/pages/customer-service/dashboard/pages/after-sales/after-sales.component.html
  18. 200 25
      src/app/pages/customer-service/dashboard/pages/after-sales/after-sales.component.scss
  19. 2 0
      src/app/pages/customer-service/dashboard/pages/consultation-list/consultation-list.component.scss
  20. 1 1
      src/app/pages/designer/personal-board/personal-board.ts
  21. 290 0
      src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.html
  22. 711 0
      src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.scss
  23. 315 0
      src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.ts
  24. 121 0
      src/app/pages/designer/project-detail/components/designer-calendar/designer-calendar.component.html
  25. 450 0
      src/app/pages/designer/project-detail/components/designer-calendar/designer-calendar.component.scss
  26. 187 0
      src/app/pages/designer/project-detail/components/order-creation/order-creation.component.html
  27. 245 0
      src/app/pages/designer/project-detail/components/order-creation/order-creation.component.scss
  28. 319 0
      src/app/pages/designer/project-detail/components/order-creation/order-creation.component.ts
  29. 252 0
      src/app/pages/designer/project-detail/components/quotation-details/quotation-details.component.html
  30. 559 0
      src/app/pages/designer/project-detail/components/quotation-details/quotation-details.component.scss
  31. 326 0
      src/app/pages/designer/project-detail/components/quotation-details/quotation-details.component.ts
  32. 0 295
      src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav-styles.scss
  33. 0 14
      src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav.component.html
  34. 0 148
      src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav.component.scss
  35. 0 56
      src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav.component.ts
  36. 477 389
      src/app/pages/designer/project-detail/project-detail.html
  37. 1645 1354
      src/app/pages/designer/project-detail/project-detail.scss
  38. 564 189
      src/app/pages/designer/project-detail/project-detail.ts
  39. 88 0
      src/app/pages/finance/dashboard/dashboard.html
  40. 230 0
      src/app/pages/finance/dashboard/dashboard.scss
  41. 76 2
      src/app/pages/finance/dashboard/dashboard.ts
  42. 303 0
      src/app/pages/finance/quotation-approval/quotation-approval.component.html
  43. 582 0
      src/app/pages/finance/quotation-approval/quotation-approval.component.scss
  44. 274 0
      src/app/pages/finance/quotation-approval/quotation-approval.component.ts
  45. 351 0
      src/app/services/ai-quotation.service.ts
  46. 820 0
      src/app/services/auto-settlement.service.ts
  47. 314 0
      src/app/services/miniprogram-payment.service.ts
  48. 426 0
      src/app/services/notification.service.ts
  49. 410 0
      src/app/services/payment-voucher-recognition.service.ts
  50. 10 0
      src/app/services/project.service.ts
  51. 435 0
      src/app/services/quotation-approval.service.ts
  52. 340 0
      src/app/services/work-hour.service.ts
  53. 256 0
      src/app/shared/components/auto-settlement-config/auto-settlement-config.html
  54. 392 0
      src/app/shared/components/auto-settlement-config/auto-settlement-config.scss
  55. 260 0
      src/app/shared/components/auto-settlement-config/auto-settlement-config.ts
  56. 236 23
      src/app/shared/components/complaint-card/complaint-card.html
  57. 654 2
      src/app/shared/components/complaint-card/complaint-card.scss
  58. 353 11
      src/app/shared/components/complaint-card/complaint-card.ts
  59. 230 1
      src/app/shared/components/customer-review-card/customer-review-card.html
  60. 329 0
      src/app/shared/components/customer-review-card/customer-review-card.scss
  61. 276 7
      src/app/shared/components/customer-review-card/customer-review-card.ts
  62. 396 0
      src/app/shared/components/customer-review-form/customer-review-form.html
  63. 469 0
      src/app/shared/components/customer-review-form/customer-review-form.scss
  64. 227 0
      src/app/shared/components/customer-review-form/customer-review-form.ts
  65. 167 0
      src/app/shared/components/panoramic-synthesis-card/panoramic-synthesis-card.html
  66. 722 0
      src/app/shared/components/panoramic-synthesis-card/panoramic-synthesis-card.scss
  67. 243 0
      src/app/shared/components/panoramic-synthesis-card/panoramic-synthesis-card.ts
  68. 257 101
      src/app/shared/components/settlement-card/settlement-card.html
  69. 266 101
      src/app/shared/components/settlement-card/settlement-card.scss
  70. 354 4
      src/app/shared/components/settlement-card/settlement-card.ts
  71. 24 0
      src/app/shared/components/star-rating/star-rating.component.html
  72. 133 0
      src/app/shared/components/star-rating/star-rating.component.scss
  73. 104 0
      src/app/shared/components/star-rating/star-rating.component.ts
  74. 1 2
      src/index.html
  75. 8 3
      src/styles.scss
  76. 3 0
      temp.txt

+ 4 - 0
.vscode/extensions.json

@@ -2,6 +2,10 @@
   // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
   "recommendations": [
     "angular.ng-template",
+<<<<<<< HEAD
     "alibaba-cloud.tongyi-lingma"
+=======
+    "kingleo.qwen"
+>>>>>>> 00a914048f847f282b0a33b5db303fe71520fd32
   ]
 }

+ 3 - 1
src/app/app.routes.ts

@@ -36,6 +36,7 @@ import { Dashboard as FinanceDashboard } from './pages/finance/dashboard/dashboa
 import { ProjectRecords } from './pages/finance/project-records/project-records';
 import { Reconciliation } from './pages/finance/reconciliation/reconciliation';
 import { Reports } from './pages/finance/reports/reports';
+import { QuotationApprovalComponent } from './pages/finance/quotation-approval/quotation-approval.component';
 
 // 人事/行政页面
 import { HrLayout } from './pages/hr/hr-layout/hr-layout';
@@ -116,7 +117,8 @@ export const routes: Routes = [
       { path: 'dashboard', component: FinanceDashboard, title: '财务工作台' },
       { path: 'project-records', component: ProjectRecords, title: '项目流水' },
       { path: 'reconciliation', component: Reconciliation, title: '对账与结算' },
-      { path: 'reports', component: Reports, title: '财务报表' }
+      { path: 'reports', component: Reports, title: '财务报表' },
+      { path: 'quotation-approval', component: QuotationApprovalComponent, title: '报价审核' }
     ]
   },
 

+ 47 - 0
src/app/models/project.model.ts

@@ -191,4 +191,51 @@ export interface MatchingOrder {
   requiredSkills: string[];
   matchRate: number;
   customerLevel: '优质' | '普通';
+}
+
+// 全景合成相关接口
+
+// 空间图像
+export interface SpaceImage {
+  id: string;
+  name: string;
+  url: string;
+  thumbnailUrl: string;
+  type: 'living_room' | 'bedroom' | 'kitchen' | 'bathroom' | 'dining_room' | 'study' | 'balcony';
+  size: number;
+  width: number;
+  height: number;
+  format: 'jpg' | 'png' | 'webp';
+  uploadDate: Date;
+  isPrimary: boolean;
+}
+
+// 全景空间
+export interface PanoramicSpace {
+  id: string;
+  name: string;
+  type: 'living_room' | 'bedroom' | 'kitchen' | 'bathroom' | 'dining_room' | 'study' | 'balcony';
+  images: SpaceImage[];
+  thumbnailUrl: string;
+  createdAt: Date;
+  updatedAt: Date;
+  status: 'pending' | 'processing' | 'completed' | 'failed';
+  progress?: number;
+}
+
+// 全景合成
+export interface PanoramicSynthesis {
+  id: string;
+  projectId: string;
+  projectName: string;
+  spaces: PanoramicSpace[];
+  status: 'draft' | 'processing' | 'completed' | 'published';
+  createdAt: Date;
+  updatedAt: Date;
+  completedAt?: Date;
+  previewUrl?: string;
+  downloadUrl?: string;
+  quality: 'standard' | 'high' | 'ultra';
+  renderTime?: number;
+  fileSize?: number;
 }

+ 220 - 0
src/app/models/work-hour.model.ts

@@ -0,0 +1,220 @@
+// 工时统计模块模型定义
+
+// 项目阶段类型
+export type ProjectPhase = '建模' | '渲染' | '后期' | '软装' | '对图' | '修改';
+
+// 项目空间类型
+export type SpaceType = '客餐厅' | '卧室' | '厨房' | '卫生间' | '书房' | '阳台' | '玄关' | '别墅';
+
+// 项目风格类型
+export type ProjectStyle = '现代简约' | '新中式' | '北欧' | '工业风' | '轻奢' | '美式' | '欧式' | '日式';
+
+// 绩效等级
+export type PerformanceLevel = 'S' | 'A' | 'B' | 'C';
+
+// 工时记录状态
+export type WorkHourStatus = '工作中' | '停滞' | '等待' | '完成';
+
+// 难度系数配置
+export interface DifficultyCoefficient {
+  spaceType: SpaceType;
+  spaceCoefficient: number; // 空间复杂度系数
+  styleType: ProjectStyle;
+  styleCoefficient: number; // 风格难度系数
+}
+
+// 工时记录
+export interface WorkHourRecord {
+  id: string;
+  projectId: string;
+  projectName: string;
+  designerId: string;
+  designerName: string;
+  phase: ProjectPhase;
+  startTime: Date;
+  endTime?: Date;
+  actualHours: number; // 实际工作时间
+  status: WorkHourStatus;
+  pauseReason?: string; // 停滞原因(如"客户3天未反馈"、"等待渲染1天")
+  pauseHours?: number; // 停滞时间
+  spaceType: SpaceType;
+  projectStyle: ProjectStyle;
+  difficultyCoefficient: number; // 综合难度系数
+  effectiveHours: number; // 有效工时 = 实际工作时间 × 难度系数
+  notes?: string;
+}
+
+// 月度工时统计
+export interface MonthlyWorkHourStats {
+  designerId: string;
+  designerName: string;
+  month: string; // YYYY-MM格式
+  totalActualHours: number; // 总实际工时
+  totalEffectiveHours: number; // 总有效工时
+  totalPauseHours: number; // 总停滞时间
+  completedProjects: number; // 完成项目数
+  averageCustomerSatisfaction: number; // 平均客户满意度
+  performanceLevel: PerformanceLevel; // 绩效等级
+  onTimeCompletionRate: number; // 按时完成率
+  excellentWorkRate: number; // 优秀作品率
+  records: WorkHourRecord[]; // 详细记录
+}
+
+// 绩效等级标准
+export interface PerformanceCriteria {
+  level: PerformanceLevel;
+  timeCompletionRate: number; // 时间完成率阈值(如提前20%以上为S级)
+  customerSatisfactionMin: number; // 客户满意度最低要求
+  description: string;
+  bonusMultiplier: number; // 奖金系数
+}
+
+// 报价规则配置
+export interface PricingRule {
+  spaceType: SpaceType;
+  basePrice: number; // 基础价格(元/人天)
+  styleType: ProjectStyle;
+  styleMultiplier: number; // 风格系数
+  finalPrice: number; // 最终价格 = 基础价格 × 风格系数
+  serviceIncludes: string[]; // 包含服务内容
+}
+
+// 报价话术
+export interface PricingScript {
+  id: string;
+  category: string; // 话术分类(如"高端报价解释"、"价格构成说明")
+  title: string;
+  content: string;
+  applicableScenarios: string[]; // 适用场景
+  tags: string[];
+}
+
+// 自动报价结果
+export interface AutoQuotationResult {
+  id: string;
+  projectId?: string;
+  spaceType: SpaceType;
+  projectStyle: ProjectStyle;
+  estimatedWorkDays: number; // 预估工作天数
+  basePrice: number;
+  styleMultiplier: number;
+  finalPrice: number;
+  serviceIncludes: string[];
+  createdAt: Date;
+  createdBy: string; // 客服ID
+  adjustments?: PriceAdjustment[]; // 价格调整记录
+  status: 'draft' | 'sent' | 'approved' | 'rejected';
+}
+
+// 价格调整记录
+export interface PriceAdjustment {
+  id: string;
+  originalPrice: number;
+  adjustedPrice: number;
+  reason: string;
+  adjustedBy: string; // 调整人ID
+  adjustedAt: Date;
+  approvedBy?: string; // 审批人ID
+  approvedAt?: Date;
+}
+
+// 工时统计仪表板数据
+export interface WorkHourDashboardData {
+  totalDesigners: number;
+  totalProjects: number;
+  averageEffectiveHours: number;
+  performanceDistribution: {
+    S: number;
+    A: number;
+    B: number;
+    C: number;
+  };
+  monthlyTrends: {
+    month: string;
+    totalHours: number;
+    effectiveHours: number;
+    efficiency: number; // 效率 = 有效工时 / 总工时
+  }[];
+  topPerformers: {
+    designerId: string;
+    designerName: string;
+    effectiveHours: number;
+    performanceLevel: PerformanceLevel;
+    efficiency: number;
+  }[];
+}
+
+// 默认难度系数配置
+export const DEFAULT_DIFFICULTY_COEFFICIENTS: DifficultyCoefficient[] = [
+  // 空间类型系数
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '卧室', spaceCoefficient: 0.8, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '厨房', spaceCoefficient: 0.9, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '卫生间', spaceCoefficient: 0.7, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '书房', spaceCoefficient: 0.8, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '阳台', spaceCoefficient: 0.6, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '玄关', spaceCoefficient: 0.5, styleType: '现代简约', styleCoefficient: 1.0 },
+  { spaceType: '别墅', spaceCoefficient: 1.5, styleType: '现代简约', styleCoefficient: 1.0 },
+  
+  // 风格类型系数
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '新中式', styleCoefficient: 1.2 },
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '北欧', styleCoefficient: 1.0 },
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '工业风', styleCoefficient: 1.1 },
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '轻奢', styleCoefficient: 1.3 },
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '美式', styleCoefficient: 1.2 },
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '欧式', styleCoefficient: 1.4 },
+  { spaceType: '客餐厅', spaceCoefficient: 1.0, styleType: '日式', styleCoefficient: 1.1 }
+];
+
+// 默认绩效等级标准
+export const DEFAULT_PERFORMANCE_CRITERIA: PerformanceCriteria[] = [
+  {
+    level: 'S',
+    timeCompletionRate: 120, // 提前20%以上完成
+    customerSatisfactionMin: 5.0,
+    description: '提前20%以上完成,客户满意度5分',
+    bonusMultiplier: 1.5
+  },
+  {
+    level: 'A',
+    timeCompletionRate: 100, // 标准时间内完成
+    customerSatisfactionMin: 4.0,
+    description: '标准时间内完成,客户满意度4-5分',
+    bonusMultiplier: 1.2
+  },
+  {
+    level: 'B',
+    timeCompletionRate: 80, // 超时但客户满意度达标
+    customerSatisfactionMin: 4.0,
+    description: '超时但客户满意度4分以上',
+    bonusMultiplier: 1.0
+  },
+  {
+    level: 'C',
+    timeCompletionRate: 0, // 多次修改且耗时过长
+    customerSatisfactionMin: 0,
+    description: '多次修改且耗时过长,客户满意度<4分',
+    bonusMultiplier: 0.8
+  }
+];
+
+// 默认报价规则
+export const DEFAULT_PRICING_RULES: PricingRule[] = [
+  // 客餐厅
+  { spaceType: '客餐厅', basePrice: 600, styleType: '现代简约', styleMultiplier: 1.0, finalPrice: 600, serviceIncludes: ['建模', '渲染', '1次小图修改'] },
+  { spaceType: '客餐厅', basePrice: 600, styleType: '新中式', styleMultiplier: 1.2, finalPrice: 720, serviceIncludes: ['建模', '渲染', '1次小图修改'] },
+  { spaceType: '客餐厅', basePrice: 600, styleType: '轻奢', styleMultiplier: 1.5, finalPrice: 900, serviceIncludes: ['建模', '渲染', '1次小图修改', '4K高清交付'] },
+  
+  // 卧室
+  { spaceType: '卧室', basePrice: 400, styleType: '现代简约', styleMultiplier: 1.0, finalPrice: 400, serviceIncludes: ['建模', '渲染', '1次小图修改'] },
+  { spaceType: '卧室', basePrice: 400, styleType: '新中式', styleMultiplier: 1.2, finalPrice: 480, serviceIncludes: ['建模', '渲染', '1次小图修改'] },
+  { spaceType: '卧室', basePrice: 400, styleType: '轻奢', styleMultiplier: 1.5, finalPrice: 600, serviceIncludes: ['建模', '渲染', '1次小图修改', '4K高清交付'] },
+  
+  // 厨房
+  { spaceType: '厨房', basePrice: 500, styleType: '现代简约', styleMultiplier: 1.0, finalPrice: 500, serviceIncludes: ['建模', '渲染', '1次小图修改'] },
+  { spaceType: '厨房', basePrice: 500, styleType: '新中式', styleMultiplier: 1.2, finalPrice: 600, serviceIncludes: ['建模', '渲染', '1次小图修改'] },
+  
+  // 别墅
+  { spaceType: '别墅', basePrice: 1200, styleType: '现代简约', styleMultiplier: 1.0, finalPrice: 1200, serviceIncludes: ['建模', '渲染', '2次修改', '全景6K交付'] },
+  { spaceType: '别墅', basePrice: 1200, styleType: '新中式', styleMultiplier: 1.5, finalPrice: 1800, serviceIncludes: ['建模', '渲染', '2次修改', '全景6K交付', '专属设计师'] }
+];

+ 243 - 0
src/app/pages/customer-service/consultation-order/components/designer-assignment/designer-assignment.component.html

@@ -0,0 +1,243 @@
+<!-- 设计师分配组件 -->
+<div class="designer-assignment-container">
+  <div class="assignment-header">
+    <div class="header-left">
+      <h4>设计师分配</h4>
+      <p>支持多设计师协作,按报价项目分配工作量</p>
+    </div>
+    <div class="header-actions">
+      <button 
+        type="button" 
+        class="btn-secondary btn-sm"
+        (click)="showDesignerCalendar.set(!showDesignerCalendar())">
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <rect x="3" y="4" width="18" height="14" rx="2" ry="2"></rect>
+          <line x1="16" y1="2" x2="16" y2="6"></line>
+          <line x1="8" y1="2" x2="8" y2="6"></line>
+          <line x1="3" y1="10" x2="21" y2="10"></line>
+        </svg>
+        {{ showDesignerCalendar() ? '隐藏日历' : '查看日历' }}
+      </button>
+      
+      <button 
+        type="button" 
+        class="btn-primary btn-sm"
+        (click)="addDesignerAssignment()">
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
+          <circle cx="9" cy="7" r="4"></circle>
+          <line x1="19" y1="8" x2="19" y2="14"></line>
+          <line x1="22" y1="11" x2="16" y2="11"></line>
+        </svg>
+        添加设计师
+      </button>
+    </div>
+  </div>
+
+  <!-- 设计师日历视图 -->
+  @if (showDesignerCalendar()) {
+    <div class="designer-calendar-section">
+      <app-designer-calendar 
+        [designers]="availableDesigners()"
+        [selectedDate]="selectedDate()"
+        (designerSelected)="onDesignerSelect($event)"
+        (assignmentRequested)="onDesignerSelect($event)">
+      </app-designer-calendar>
+    </div>
+  }
+
+  <!-- 项目组选择 -->
+  <div class="project-group-section">
+    <div class="group-selector">
+      <label class="field-label">项目组分配</label>
+      <select 
+        [(ngModel)]="selectedProjectGroup" 
+        (ngModelChange)="onProjectGroupChange($event)"
+        class="field-select">
+        <option value="">选择项目组</option>
+        @for (group of projectGroups; track group.id) {
+          <option [value]="group.id">{{ group.name }} (组长: {{ group.leader }})</option>
+        }
+      </select>
+      <div class="field-hint">选择项目组后,组长可进一步分配具体任务给组员</div>
+    </div>
+
+    <!-- 跨组合作选项 -->
+    @if (selectedProjectGroup) {
+      <div class="cross-group-option">
+        <label class="checkbox-label">
+          <input 
+            type="checkbox" 
+            [(ngModel)]="allowCrossGroupCollaboration"
+            (ngModelChange)="onCrossGroupChange($event)">
+          <span class="checkmark"></span>
+          允许跨组合作
+        </label>
+        <div class="field-hint">组长可以选择其他项目组成员参与协作</div>
+      </div>
+    }
+  </div>
+
+  <!-- 设计师分配列表 -->
+  <div class="assignment-list">
+    @if (designerAssignments().length === 0) {
+      <div class="empty-state">
+        <div class="empty-icon">
+          <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+            <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
+            <circle cx="9" cy="7" r="4"></circle>
+            <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
+            <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
+          </svg>
+        </div>
+        <p>暂未分配设计师</p>
+        <p class="empty-hint">请先选择项目组或直接添加设计师</p>
+      </div>
+    } @else {
+      @for (assignment of designerAssignments(); track assignment.id) {
+        <div class="assignment-item">
+          <div class="designer-info">
+            <div class="designer-avatar">
+              <img [src]="assignment.designer.avatar || '/assets/default-avatar.png'" 
+                   [alt]="assignment.designer.name">
+            </div>
+            <div class="designer-details">
+              <div class="designer-name">{{ assignment.designer.name }}</div>
+              <div class="designer-meta">
+                <span class="designer-group">{{ assignment.designer.groupName }}</span>
+                @if (assignment.designer.isLeader) {
+                  <span class="leader-badge">组长</span>
+                }
+                @if (assignment.isCrossGroup) {
+                  <span class="cross-group-badge">跨组</span>
+                }
+              </div>
+              <div class="designer-status">
+                <span class="status-indicator" [class]="assignment.designer.status"></span>
+                {{ getDesignerStatusText(assignment.designer.status) }}
+              </div>
+            </div>
+          </div>
+
+          <div class="assignment-details">
+            <!-- 分配的报价项目 -->
+            <div class="assigned-quotations">
+              <label class="field-label">负责项目</label>
+              <div class="quotation-chips">
+                @for (quotationId of assignment.quotationItemIds; track quotationId) {
+                  <div class="quotation-chip">
+                    <span>{{ getQuotationItemName(quotationId) }}</span>
+                    <button 
+                      type="button" 
+                      class="remove-quotation"
+                      (click)="removeQuotationFromAssignment(assignment.id, quotationId)">
+                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                        <line x1="18" y1="6" x2="6" y2="18"></line>
+                        <line x1="6" y1="6" x2="18" y2="18"></line>
+                      </svg>
+                    </button>
+                  </div>
+                }
+                
+                <!-- 添加项目按钮 -->
+                <div class="add-quotation-dropdown" [class.open]="openDropdownId() === assignment.id">
+                  <button 
+                    type="button" 
+                    class="add-quotation-btn"
+                    (click)="toggleQuotationDropdown(assignment.id)">
+                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <line x1="12" y1="5" x2="12" y2="19"></line>
+                      <line x1="5" y1="12" x2="19" y2="12"></line>
+                    </svg>
+                  </button>
+                  
+                  @if (openDropdownId() === assignment.id) {
+                    <div class="quotation-dropdown">
+                      @for (item of getUnassignedQuotationItems(assignment.id); track item.id) {
+                        <div 
+                          class="dropdown-item"
+                          (click)="addQuotationToAssignment(assignment.id, item.id)">
+                          <span>{{ item.roomType }}</span>
+                          <span class="item-amount">¥{{ item.amount | number:'1.2-2' }}</span>
+                        </div>
+                      }
+                      @if (getUnassignedQuotationItems(assignment.id).length === 0) {
+                        <div class="dropdown-empty">所有项目已分配</div>
+                      }
+                    </div>
+                  }
+                </div>
+              </div>
+            </div>
+
+            <!-- 工作量统计 -->
+            <div class="workload-info">
+              <div class="workload-item">
+                <span class="workload-label">分配金额:</span>
+                <span class="workload-value">¥{{ assignment.assignedAmount | number:'1.2-2' }}</span>
+              </div>
+              <div class="workload-item">
+                <span class="workload-label">工作量占比:</span>
+                <span class="workload-value">{{ assignment.workloadPercentage | number:'1.1-1' }}%</span>
+              </div>
+            </div>
+          </div>
+
+          <div class="assignment-actions">
+            <button 
+              type="button" 
+              class="action-btn edit-btn"
+              (click)="editAssignment(assignment)">
+              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
+                <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
+              </svg>
+            </button>
+            <button 
+              type="button" 
+              class="action-btn delete-btn"
+              (click)="removeAssignment(assignment.id)">
+              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <polyline points="3 6 5 6 21 6"></polyline>
+                <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2 2v2"></path>
+              </svg>
+            </button>
+          </div>
+        </div>
+      }
+    }
+  </div>
+
+  <!-- 分配汇总 -->
+  @if (designerAssignments().length > 0) {
+    <div class="assignment-summary">
+      <div class="summary-stats">
+        <div class="stat-item">
+          <span class="stat-label">参与设计师:</span>
+          <span class="stat-value">{{ designerAssignments().length }} 人</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-label">已分配金额:</span>
+          <span class="stat-value">¥{{ totalAssignedAmount() | number:'1.2-2' }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-label">分配进度:</span>
+          <span class="stat-value" [class.complete]="assignmentProgress() >= 100">
+            {{ assignmentProgress() | number:'1.1-1' }}%
+          </span>
+        </div>
+      </div>
+      
+      @if (assignmentProgress() < 100) {
+        <div class="assignment-warning">
+          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
+            <line x1="12" y1="9" x2="12" y2="13"></line>
+            <line x1="12" y1="17" x2="12.01" y2="17"></line>
+          </svg>
+          还有 ¥{{ remainingAmount() | number:'1.2-2' }} 未分配给设计师
+        </div>
+      }
+    </div>
+  }
+</div>

+ 567 - 0
src/app/pages/customer-service/consultation-order/components/designer-assignment/designer-assignment.component.scss

@@ -0,0 +1,567 @@
+.designer-assignment-container {
+  .assignment-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    margin-bottom: 24px;
+    
+    .header-left {
+      h4 {
+        font-size: 16px;
+        font-weight: 600;
+        color: #1a1a1a;
+        margin: 0 0 4px 0;
+      }
+      
+      p {
+        font-size: 13px;
+        color: #666;
+        margin: 0;
+      }
+    }
+    
+    .header-actions {
+      display: flex;
+      gap: 12px;
+      
+      .btn-sm {
+        padding: 8px 16px;
+        font-size: 13px;
+        border-radius: 6px;
+        border: none;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        transition: all 0.2s ease;
+        
+        svg {
+          flex-shrink: 0;
+        }
+      }
+      
+      .btn-secondary {
+        background: #f8fafc;
+        color: #475569;
+        border: 1px solid #e2e8f0;
+        
+        &:hover {
+          background: #f1f5f9;
+          border-color: #cbd5e1;
+        }
+      }
+      
+      .btn-primary {
+        background: #4f46e5;
+        color: white;
+        
+        &:hover {
+          background: #4338ca;
+        }
+      }
+    }
+  }
+}
+
+.designer-calendar-section {
+  margin-bottom: 24px;
+  padding: 20px;
+  background: #f8fafc;
+  border-radius: 8px;
+  border: 1px solid #e2e8f0;
+}
+
+.project-group-section {
+  margin-bottom: 24px;
+  padding: 20px;
+  background: white;
+  border-radius: 8px;
+  border: 1px solid #e2e8f0;
+  
+  .group-selector {
+    margin-bottom: 16px;
+    
+    .field-label {
+      display: block;
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 8px;
+    }
+    
+    .field-select {
+      width: 100%;
+      max-width: 300px;
+      padding: 10px 12px;
+      border: 1px solid #d1d5db;
+      border-radius: 6px;
+      font-size: 14px;
+      
+      &:focus {
+        outline: none;
+        border-color: #4f46e5;
+        box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+      }
+    }
+    
+    .field-hint {
+      margin-top: 6px;
+      font-size: 12px;
+      color: #6b7280;
+    }
+  }
+  
+  .cross-group-option {
+    .checkbox-label {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      cursor: pointer;
+      font-size: 14px;
+      color: #374151;
+      
+      input[type="checkbox"] {
+        display: none;
+      }
+      
+      .checkmark {
+        width: 18px;
+        height: 18px;
+        border: 2px solid #d1d5db;
+        border-radius: 4px;
+        position: relative;
+        transition: all 0.2s ease;
+        
+        &::after {
+          content: '';
+          position: absolute;
+          left: 5px;
+          top: 2px;
+          width: 4px;
+          height: 8px;
+          border: solid white;
+          border-width: 0 2px 2px 0;
+          transform: rotate(45deg);
+          opacity: 0;
+          transition: opacity 0.2s ease;
+        }
+      }
+      
+      input:checked + .checkmark {
+        background: #4f46e5;
+        border-color: #4f46e5;
+        
+        &::after {
+          opacity: 1;
+        }
+      }
+    }
+    
+    .field-hint {
+      margin-top: 6px;
+      margin-left: 26px;
+      font-size: 12px;
+      color: #6b7280;
+    }
+  }
+}
+
+.assignment-list {
+  .empty-state {
+    text-align: center;
+    padding: 48px 24px;
+    color: #6b7280;
+    
+    .empty-icon {
+      margin-bottom: 16px;
+      
+      svg {
+        color: #d1d5db;
+      }
+    }
+    
+    p {
+      margin: 0 0 8px 0;
+      font-size: 14px;
+      
+      &.empty-hint {
+        font-size: 13px;
+        color: #9ca3af;
+      }
+    }
+  }
+  
+  .assignment-item {
+    background: white;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    padding: 20px;
+    margin-bottom: 16px;
+    display: flex;
+    gap: 20px;
+    transition: all 0.2s ease;
+    
+    &:hover {
+      border-color: #cbd5e1;
+      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+    }
+    
+    .designer-info {
+      flex-shrink: 0;
+      width: 200px;
+      
+      .designer-avatar {
+        width: 48px;
+        height: 48px;
+        border-radius: 50%;
+        overflow: hidden;
+        margin-bottom: 12px;
+        
+        img {
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+        }
+      }
+      
+      .designer-details {
+        .designer-name {
+          font-size: 15px;
+          font-weight: 600;
+          color: #1e293b;
+          margin-bottom: 6px;
+        }
+        
+        .designer-meta {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 6px;
+          margin-bottom: 8px;
+          
+          .designer-group {
+            font-size: 12px;
+            color: #64748b;
+            background: #f1f5f9;
+            padding: 2px 6px;
+            border-radius: 4px;
+          }
+          
+          .leader-badge {
+            font-size: 12px;
+            color: #dc2626;
+            background: #fef2f2;
+            padding: 2px 6px;
+            border-radius: 4px;
+            border: 1px solid #fecaca;
+          }
+          
+          .cross-group-badge {
+            font-size: 12px;
+            color: #7c3aed;
+            background: #f3f4f6;
+            padding: 2px 6px;
+            border-radius: 4px;
+            border: 1px solid #e5e7eb;
+          }
+        }
+        
+        .designer-status {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          font-size: 12px;
+          color: #64748b;
+          
+          .status-indicator {
+            width: 8px;
+            height: 8px;
+            border-radius: 50%;
+            
+            &.available {
+              background: #10b981;
+            }
+            
+            &.busy {
+              background: #f59e0b;
+            }
+            
+            &.stagnant {
+              background: #ef4444;
+            }
+            
+            &.overloaded {
+              background: #8b5cf6;
+            }
+          }
+        }
+      }
+    }
+    
+    .assignment-details {
+      flex: 1;
+      
+      .assigned-quotations {
+        margin-bottom: 16px;
+        
+        .field-label {
+          font-size: 13px;
+          font-weight: 500;
+          color: #374151;
+          margin-bottom: 8px;
+          display: block;
+        }
+        
+        .quotation-chips {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 8px;
+          align-items: center;
+          
+          .quotation-chip {
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            background: #eff6ff;
+            color: #1e40af;
+            padding: 6px 10px;
+            border-radius: 6px;
+            font-size: 13px;
+            border: 1px solid #dbeafe;
+            
+            .remove-quotation {
+              background: none;
+              border: none;
+              color: #64748b;
+              cursor: pointer;
+              padding: 0;
+              display: flex;
+              align-items: center;
+              
+              &:hover {
+                color: #ef4444;
+              }
+            }
+          }
+          
+          .add-quotation-dropdown {
+            position: relative;
+            
+            .add-quotation-btn {
+              width: 28px;
+              height: 28px;
+              border: 1px dashed #cbd5e1;
+              background: white;
+              border-radius: 6px;
+              cursor: pointer;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              color: #64748b;
+              transition: all 0.2s ease;
+              
+              &:hover {
+                border-color: #4f46e5;
+                color: #4f46e5;
+              }
+            }
+            
+            .quotation-dropdown {
+              position: absolute;
+              top: 100%;
+              left: 0;
+              z-index: 10;
+              background: white;
+              border: 1px solid #e2e8f0;
+              border-radius: 6px;
+              box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+              min-width: 200px;
+              max-height: 200px;
+              overflow-y: auto;
+              
+              .dropdown-item {
+                padding: 10px 12px;
+                cursor: pointer;
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                font-size: 13px;
+                
+                &:hover {
+                  background: #f8fafc;
+                }
+                
+                .item-amount {
+                  color: #059669;
+                  font-weight: 500;
+                }
+              }
+              
+              .dropdown-empty {
+                padding: 10px 12px;
+                font-size: 13px;
+                color: #9ca3af;
+                text-align: center;
+              }
+            }
+          }
+        }
+      }
+      
+      .workload-info {
+        display: flex;
+        gap: 24px;
+        
+        .workload-item {
+          display: flex;
+          flex-direction: column;
+          gap: 4px;
+          
+          .workload-label {
+            font-size: 12px;
+            color: #64748b;
+          }
+          
+          .workload-value {
+            font-size: 14px;
+            font-weight: 600;
+            color: #1e293b;
+          }
+        }
+      }
+    }
+    
+    .assignment-actions {
+      flex-shrink: 0;
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+      
+      .action-btn {
+        width: 32px;
+        height: 32px;
+        border: none;
+        border-radius: 6px;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        transition: all 0.2s ease;
+        
+        &.edit-btn {
+          background: #f0f9ff;
+          color: #0369a1;
+          
+          &:hover {
+            background: #e0f2fe;
+          }
+        }
+        
+        &.delete-btn {
+          background: #fef2f2;
+          color: #dc2626;
+          
+          &:hover {
+            background: #fee2e2;
+          }
+        }
+      }
+    }
+  }
+}
+
+.assignment-summary {
+  margin-top: 24px;
+  padding: 20px;
+  background: #f8fafc;
+  border-radius: 8px;
+  border: 1px solid #e2e8f0;
+  
+  .summary-stats {
+    display: flex;
+    gap: 32px;
+    margin-bottom: 16px;
+    
+    .stat-item {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+      
+      .stat-label {
+        font-size: 13px;
+        color: #64748b;
+      }
+      
+      .stat-value {
+        font-size: 16px;
+        font-weight: 600;
+        color: #1e293b;
+        
+        &.complete {
+          color: #059669;
+        }
+      }
+    }
+  }
+  
+  .assignment-warning {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 12px 16px;
+    background: #fef3cd;
+    border: 1px solid #f6cc6d;
+    border-radius: 6px;
+    font-size: 14px;
+    color: #92400e;
+    
+    svg {
+      flex-shrink: 0;
+      color: #d97706;
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .designer-assignment-container {
+    .assignment-header {
+      flex-direction: column;
+      gap: 16px;
+      align-items: stretch;
+      
+      .header-actions {
+        justify-content: flex-end;
+      }
+    }
+  }
+  
+  .assignment-list {
+    .assignment-item {
+      flex-direction: column;
+      gap: 16px;
+      
+      .designer-info {
+        width: 100%;
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        
+        .designer-avatar {
+          margin-bottom: 0;
+        }
+      }
+      
+      .assignment-actions {
+        flex-direction: row;
+        justify-content: flex-end;
+      }
+    }
+  }
+  
+  .assignment-summary {
+    .summary-stats {
+      flex-direction: column;
+      gap: 16px;
+    }
+  }
+}

+ 350 - 0
src/app/pages/customer-service/consultation-order/components/designer-assignment/designer-assignment.component.ts

@@ -0,0 +1,350 @@
+import { Component, OnInit, Input, Output, EventEmitter, computed, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { DesignerCalendarComponent } from '../designer-calendar/designer-calendar.component';
+
+export interface Designer {
+  id: string;
+  name: string;
+  avatar?: string;
+  groupId: string;
+  groupName: string;
+  isLeader: boolean;
+  status: 'available' | 'busy' | 'stagnant' | 'overloaded';
+  currentProjects: number;
+  lastOrderDate?: string;
+  idleDays?: number;
+}
+
+export interface ProjectGroup {
+  id: string;
+  name: string;
+  leader: string;
+  leaderId: string;
+  members: Designer[];
+}
+
+export interface DesignerAssignment {
+  id: string;
+  designer: Designer;
+  quotationItemIds: string[];
+  assignedAmount: number;
+  workloadPercentage: number;
+  isCrossGroup: boolean;
+}
+
+export interface QuotationItem {
+  id: string;
+  roomType: string;
+  amount: number;
+  description?: string;
+}
+
+@Component({
+  selector: 'app-designer-assignment',
+  standalone: true,
+  imports: [CommonModule, FormsModule, DesignerCalendarComponent],
+  templateUrl: './designer-assignment.component.html',
+  styleUrls: ['./designer-assignment.component.scss']
+})
+export class DesignerAssignmentComponent {
+  @Input() quotationItems = signal<QuotationItem[]>([]);
+  @Input() selectedDesigners = signal<DesignerAssignment[]>([]);
+  @Output() designerChange = new EventEmitter<DesignerAssignment[]>();
+
+  designerAssignments = signal<DesignerAssignment[]>([]);
+  availableDesigners = signal<Designer[]>([]);
+  showDesignerCalendar = signal(false);
+  selectedDate = signal(new Date());
+  selectedProjectGroup = '';
+  allowCrossGroupCollaboration = false;
+  openDropdownId = signal<string | null>(null);
+
+  // 项目组数据
+  projectGroups: ProjectGroup[] = [
+    {
+      id: 'group1',
+      name: '家装设计组',
+      leader: '张设计师',
+      leaderId: 'designer1',
+      members: []
+    },
+    {
+      id: 'group2', 
+      name: '工装设计组',
+      leader: '李设计师',
+      leaderId: 'designer2',
+      members: []
+    },
+    {
+      id: 'group3',
+      name: '软装设计组', 
+      leader: '王设计师',
+      leaderId: 'designer3',
+      members: []
+    }
+  ];
+
+  // 计算总分配金额
+  totalAssignedAmount = computed(() => {
+    return this.designerAssignments().reduce((sum, assignment) => sum + assignment.assignedAmount, 0);
+  });
+
+  // 计算总报价金额
+  totalQuotationAmount = computed(() => {
+    return this.quotationItems().reduce((sum, item) => sum + item.amount, 0);
+  });
+
+  // 计算分配进度
+  assignmentProgress = computed(() => {
+    const total = this.totalQuotationAmount();
+    const assigned = this.totalAssignedAmount();
+    return total > 0 ? (assigned / total) * 100 : 0;
+  });
+
+  // 计算剩余金额
+  remainingAmount = computed(() => {
+    return this.totalQuotationAmount() - this.totalAssignedAmount();
+  });
+
+  constructor() {
+    this.loadAvailableDesigners();
+  }
+
+  ngOnInit() {
+    if (this.selectedDesigners().length > 0) {
+      this.designerAssignments.set(this.selectedDesigners());
+    }
+  }
+
+  private loadAvailableDesigners() {
+    // 模拟设计师数据
+    const mockDesigners: Designer[] = [
+      {
+        id: 'designer1',
+        name: '张设计师',
+        avatar: '/assets/avatars/designer1.jpg',
+        groupId: 'group1',
+        groupName: '家装设计组',
+        isLeader: true,
+        status: 'available',
+        currentProjects: 2,
+        lastOrderDate: '2024-01-15',
+        idleDays: 3
+      },
+      {
+        id: 'designer2',
+        name: '李设计师',
+        avatar: '/assets/avatars/designer2.jpg',
+        groupId: 'group2',
+        groupName: '工装设计组',
+        isLeader: true,
+        status: 'busy',
+        currentProjects: 4,
+        lastOrderDate: '2024-01-18'
+      },
+      {
+        id: 'designer3',
+        name: '王设计师',
+        avatar: '/assets/avatars/designer3.jpg',
+        groupId: 'group3',
+        groupName: '软装设计组',
+        isLeader: true,
+        status: 'available',
+        currentProjects: 1,
+        lastOrderDate: '2024-01-10',
+        idleDays: 8
+      },
+      {
+        id: 'designer4',
+        name: '赵设计师',
+        groupId: 'group1',
+        groupName: '家装设计组',
+        isLeader: false,
+        status: 'stagnant',
+        currentProjects: 0,
+        lastOrderDate: '2023-12-20',
+        idleDays: 28
+      },
+      {
+        id: 'designer5',
+        name: '刘设计师',
+        groupId: 'group2',
+        groupName: '工装设计组',
+        isLeader: false,
+        status: 'available',
+        currentProjects: 2,
+        lastOrderDate: '2024-01-16',
+        idleDays: 2
+      }
+    ];
+
+    this.availableDesigners.set(mockDesigners);
+  }
+
+  // 项目组变更
+  onProjectGroupChange(groupId: string) {
+    this.selectedProjectGroup = groupId;
+    
+    if (groupId) {
+      const group = this.projectGroups.find(g => g.id === groupId);
+      if (group) {
+        // 自动添加组长
+        const leader = this.availableDesigners().find(d => d.id === group.leaderId);
+        if (leader && !this.designerAssignments().find(a => a.designer.id === leader.id)) {
+          this.addDesignerToAssignment(leader);
+        }
+      }
+    }
+  }
+
+  // 跨组合作变更
+  onCrossGroupChange(allowed: boolean) {
+    this.allowCrossGroupCollaboration = allowed;
+  }
+
+  // 添加设计师分配
+  addDesignerAssignment() {
+    // 这里可以打开设计师选择对话框
+    // 暂时添加一个可用的设计师
+    const availableDesigner = this.availableDesigners().find(d => 
+      !this.designerAssignments().find(a => a.designer.id === d.id)
+    );
+    
+    if (availableDesigner) {
+      this.addDesignerToAssignment(availableDesigner);
+    }
+  }
+
+  private addDesignerToAssignment(designer: Designer) {
+    const newAssignment: DesignerAssignment = {
+      id: this.generateId(),
+      designer,
+      quotationItemIds: [],
+      assignedAmount: 0,
+      workloadPercentage: 0,
+      isCrossGroup: this.selectedProjectGroup ? designer.groupId !== this.selectedProjectGroup : false
+    };
+
+    const currentAssignments = this.designerAssignments();
+    this.designerAssignments.set([...currentAssignments, newAssignment]);
+    this.emitChange();
+  }
+
+  // 移除设计师分配
+  removeAssignment(assignmentId: string) {
+    const currentAssignments = this.designerAssignments();
+    const filteredAssignments = currentAssignments.filter(a => a.id !== assignmentId);
+    this.designerAssignments.set(filteredAssignments);
+    this.emitChange();
+  }
+
+  // 编辑分配
+  editAssignment(assignment: DesignerAssignment) {
+    // 这里可以打开编辑对话框
+    console.log('编辑分配:', assignment);
+  }
+
+  // 切换报价项目下拉菜单
+  toggleQuotationDropdown(assignmentId: string) {
+    const currentOpen = this.openDropdownId();
+    this.openDropdownId.set(currentOpen === assignmentId ? null : assignmentId);
+  }
+
+  // 获取未分配的报价项目
+  getUnassignedQuotationItems(currentAssignmentId: string): QuotationItem[] {
+    const allAssignedIds = this.designerAssignments()
+      .filter(a => a.id !== currentAssignmentId)
+      .flatMap(a => a.quotationItemIds);
+    
+    return this.quotationItems().filter(item => !allAssignedIds.includes(item.id));
+  }
+
+  // 添加报价项目到分配
+  addQuotationToAssignment(assignmentId: string, quotationItemId: string) {
+    const currentAssignments = this.designerAssignments();
+    const updatedAssignments = currentAssignments.map(assignment => {
+      if (assignment.id === assignmentId) {
+        const quotationItem = this.quotationItems().find(q => q.id === quotationItemId);
+        if (quotationItem) {
+          return {
+            ...assignment,
+            quotationItemIds: [...assignment.quotationItemIds, quotationItemId],
+            assignedAmount: assignment.assignedAmount + quotationItem.amount,
+            workloadPercentage: this.calculateWorkloadPercentage(assignment.assignedAmount + quotationItem.amount)
+          };
+        }
+      }
+      return assignment;
+    });
+
+    this.designerAssignments.set(updatedAssignments);
+    this.openDropdownId.set(null);
+    this.emitChange();
+  }
+
+  // 从分配中移除报价项目
+  removeQuotationFromAssignment(assignmentId: string, quotationItemId: string) {
+    const currentAssignments = this.designerAssignments();
+    const updatedAssignments = currentAssignments.map(assignment => {
+      if (assignment.id === assignmentId) {
+        const quotationItem = this.quotationItems().find(q => q.id === quotationItemId);
+        if (quotationItem) {
+          return {
+            ...assignment,
+            quotationItemIds: assignment.quotationItemIds.filter(id => id !== quotationItemId),
+            assignedAmount: assignment.assignedAmount - quotationItem.amount,
+            workloadPercentage: this.calculateWorkloadPercentage(assignment.assignedAmount - quotationItem.amount)
+          };
+        }
+      }
+      return assignment;
+    });
+
+    this.designerAssignments.set(updatedAssignments);
+    this.emitChange();
+  }
+
+  // 获取报价项目名称
+  getQuotationItemName(quotationItemId: string): string {
+    const item = this.quotationItems().find(q => q.id === quotationItemId);
+    return item ? item.roomType : '未知项目';
+  }
+
+  // 计算工作量占比
+  private calculateWorkloadPercentage(assignedAmount: number): number {
+    const total = this.totalQuotationAmount();
+    return total > 0 ? (assignedAmount / total) * 100 : 0;
+  }
+
+  // 获取设计师状态文本
+  getDesignerStatusText(status: Designer['status']): string {
+    const statusMap = {
+      'available': '空闲可接单',
+      'busy': '忙碌中',
+      'stagnant': '停滞期',
+      'overloaded': '满载'
+    };
+    return statusMap[status] || '未知状态';
+  }
+
+  // 设计师选择事件
+  onDesignerSelect(designer: Designer) {
+    if (!this.designerAssignments().find(a => a.designer.id === designer.id)) {
+      this.addDesignerToAssignment(designer);
+    }
+  }
+
+  // 日期变更事件
+  onDateChange(date: Date) {
+    this.selectedDate.set(date);
+  }
+
+  private emitChange() {
+    this.designerChange.emit(this.designerAssignments());
+  }
+
+  private generateId(): string {
+    return 'assignment_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+  }
+}

+ 317 - 0
src/app/pages/customer-service/consultation-order/components/designer-calendar/designer-calendar.component.html

@@ -0,0 +1,317 @@
+<div class="designer-calendar-container">
+  <div class="calendar-header">
+    <div class="header-left">
+      <h4>设计师日历</h4>
+      <p>查看设计师工作负载和空闲时间</p>
+    </div>
+    <div class="header-actions">
+      <button class="btn btn-sm btn-secondary" (click)="toggleView()">
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          @if (viewMode === 'calendar') {
+            <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
+            <line x1="16" y1="2" x2="16" y2="6"/>
+            <line x1="8" y1="2" x2="8" y2="6"/>
+            <line x1="3" y1="10" x2="21" y2="10"/>
+          } @else {
+            <line x1="8" y1="6" x2="21" y2="6"/>
+            <line x1="8" y1="12" x2="21" y2="12"/>
+            <line x1="8" y1="18" x2="21" y2="18"/>
+            <line x1="3" y1="6" x2="3.01" y2="6"/>
+            <line x1="3" y1="12" x2="3.01" y2="12"/>
+            <line x1="3" y1="18" x2="3.01" y2="18"/>
+          }
+        </svg>
+        {{ viewMode === 'calendar' ? '列表视图' : '日历视图' }}
+      </button>
+      <button class="btn btn-sm btn-secondary" (click)="refreshData()">
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <polyline points="23 4 23 10 17 10"/>
+          <polyline points="1 20 1 14 7 14"/>
+          <path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
+        </svg>
+        刷新
+      </button>
+    </div>
+  </div>
+
+  <!-- 筛选器 -->
+  <div class="calendar-filters">
+    <div class="filter-group">
+      <label class="filter-label">项目组</label>
+      <select class="filter-select" [(ngModel)]="selectedGroup" (ngModelChange)="onGroupChange()">
+        <option value="">全部项目组</option>
+        <option *ngFor="let group of projectGroups" [value]="group.id">{{ group.name }}</option>
+      </select>
+    </div>
+    <div class="filter-group">
+      <label class="filter-label">设计师状态</label>
+      <select class="filter-select" [(ngModel)]="selectedStatus" (ngModelChange)="onStatusChange()">
+        <option value="">全部状态</option>
+        <option value="available">空闲</option>
+        <option value="busy">忙碌</option>
+        <option value="overloaded">满载</option>
+        <option value="stagnant">停滞期</option>
+      </select>
+    </div>
+    <div class="filter-group">
+      <label class="filter-label">时间范围</label>
+      <select class="filter-select" [(ngModel)]="timeRange" (ngModelChange)="onTimeRangeChange()">
+        <option value="week">本周</option>
+        <option value="month">本月</option>
+        <option value="quarter">本季度</option>
+      </select>
+    </div>
+    <div class="filter-group">
+      <label class="checkbox-label">
+        <input type="checkbox" [(ngModel)]="hideStagnantProjects" (ngModelChange)="onFilterChange()">
+        <span class="checkmark"></span>
+        过滤停滞期项目
+      </label>
+    </div>
+  </div>
+
+  <!-- 日历视图 -->
+  @if (viewMode === 'calendar') {
+    <div class="calendar-view">
+      <div class="calendar-navigation">
+        <button class="nav-btn" (click)="previousPeriod()">
+          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <polyline points="15 18 9 12 15 6"/>
+          </svg>
+        </button>
+        <h3 class="current-period">{{ getCurrentPeriodLabel() }}</h3>
+        <button class="nav-btn" (click)="nextPeriod()">
+          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <polyline points="9 18 15 12 9 6"/>
+          </svg>
+        </button>
+      </div>
+
+      <div class="calendar-grid">
+        <div class="calendar-header-row">
+          <div class="designer-column">设计师</div>
+          <div class="date-column" *ngFor="let date of calendarDates">
+            <div class="date-label">{{ formatDate(date, 'MM/dd') }}</div>
+            <div class="weekday-label">{{ formatDate(date, 'EEE') }}</div>
+          </div>
+        </div>
+
+        <div class="calendar-row" *ngFor="let designer of filteredDesigners">
+          <div class="designer-cell">
+            <div class="designer-info">
+              <div class="designer-avatar">
+                <img [src]="designer.avatar" [alt]="designer.name">
+              </div>
+              <div class="designer-details">
+                <div class="designer-name">{{ designer.name }}</div>
+                <div class="designer-group">{{ designer.groupName }}</div>
+                <div class="designer-stats">
+                  <span class="stat-item">{{ designer.currentProjects }}个项目</span>
+                  <span class="stat-item" [class]="getIdleDaysClass(designer.idleDays ?? 0)">
+                      {{ designer.idleDays ?? 0 }}天未接单
+                  </span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="date-cell" *ngFor="let date of calendarDates">
+            <div class="date-content" [class]="getDateCellClass(designer, date)">
+              @if (getDateEvents(designer, date).length > 0) {
+                <div class="event-indicators">
+                  <div class="event-indicator" 
+                       *ngFor="let event of getDateEvents(designer, date)" 
+                       [class]="getEventClass(event)"
+                       [title]="event.title">
+                    {{ event.type === 'review' ? '对' : event.type === 'project' ? '项' : '休' }}
+                  </div>
+                </div>
+              } @else {
+                <div class="empty-indicator"></div>
+              }
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  }
+
+  <!-- 列表视图 -->
+  @if (viewMode === 'list') {
+    <div class="list-view">
+      @if (filteredDesigners.length === 0) {
+        <div class="empty-state">
+          <div class="empty-icon">
+            <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+              <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
+              <circle cx="9" cy="7" r="4"/>
+              <path d="m22 2-5 10-5-5 10-5z"/>
+            </svg>
+          </div>
+          <p>暂无符合条件的设计师</p>
+          <p class="empty-hint">请调整筛选条件或添加设计师</p>
+        </div>
+      } @else {
+        <div class="designer-list">
+          <div class="designer-card" *ngFor="let designer of filteredDesigners">
+            <div class="card-header">
+              <div class="designer-info">
+                <div class="designer-avatar">
+                  <img [src]="designer.avatar" [alt]="designer.name">
+                </div>
+                <div class="designer-details">
+                  <div class="designer-name">{{ designer.name }}</div>
+                  <div class="designer-meta">
+                    <span class="designer-group">{{ designer.groupName }}</span>
+                    @if (designer.isLeader) {
+                      <span class="leader-badge">组长</span>
+                    }
+                    <span class="status-indicator" [class]="designer.status"></span>
+                    <span class="status-text">{{ getStatusText(designer.status) }}</span>
+                  </div>
+                </div>
+              </div>
+              <div class="designer-actions">
+                <button class="action-btn" (click)="viewDesignerDetail(designer)" title="查看详情">
+                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
+                    <circle cx="12" cy="12" r="3"/>
+                  </svg>
+                </button>
+                <button class="action-btn" (click)="assignToDesigner(designer)" title="分配任务">
+                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                    <circle cx="12" cy="12" r="10"/>
+                    <line x1="12" y1="8" x2="12" y2="16"/>
+                    <line x1="8" y1="12" x2="16" y2="12"/>
+                  </svg>
+                </button>
+              </div>
+            </div>
+
+            <div class="card-content">
+              <div class="workload-section">
+                <h5>工作负载</h5>
+                <div class="workload-stats">
+                  <div class="stat-item">
+                    <span class="stat-label">当前项目</span>
+                    <span class="stat-value">{{ designer.currentProjects }}个</span>
+                  </div>
+                  <div class="stat-item">
+                    <span class="stat-label">本月完成</span>
+                    <span class="stat-value">{{ designer.completedThisMonth }}个</span>
+                  </div>
+                  <div class="stat-item">
+                    <span class="stat-label">平均周期</span>
+                    <span class="stat-value">{{ designer.averageCycle }}天</span>
+                  </div>
+                  <div class="stat-item">
+                    <span class="stat-label">空闲天数</span>
+                    <span class="stat-value" [class]="getIdleDaysClass(designer.idleDays ?? 0)">
+                  {{ designer.idleDays ?? 0 }}天
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              <div class="schedule-section">
+                <h5>近期安排</h5>
+                @if ((designer.upcomingEvents || []).length === 0) {
+                  <p class="no-events">暂无安排</p>
+                } @else {
+                  <div class="event-list">
+                    <div class="event-item" *ngFor="let event of (designer.upcomingEvents || []).slice(0, 3)">
+                      <div class="event-date">{{ formatDate(event.date, 'MM/dd') }}</div>
+                      <div class="event-content">
+                        <div class="event-title">{{ event.title }}</div>
+                        <div class="event-type" [class]="getEventClass(event)">
+                          {{ getEventTypeText(event.type) }}
+                        </div>
+                      </div>
+                    </div>
+                    @if ((designer.upcomingEvents || []).length > 3) {
+                      <div class="more-events">
+                        还有 {{ (designer.upcomingEvents || []).length - 3 }} 个安排...
+                      </div>
+                    }
+                  </div>
+                }
+              </div>
+
+              <div class="availability-section">
+                <h5>可用性分析</h5>
+                <div class="availability-info">
+                  <div class="availability-item">
+                    <span class="availability-label">下次可接单</span>
+                    <span class="availability-value">{{ getNextAvailableDate(designer) }}</span>
+                  </div>
+                  <div class="availability-item">
+                    <span class="availability-label">推荐分配</span>
+                    <span class="availability-value" [class]="getRecommendationClass(designer)">
+                      {{ getRecommendationText(designer) }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      }
+    </div>
+  }
+
+  <!-- 统计摘要 -->
+  <div class="calendar-summary">
+    <div class="summary-stats">
+      <div class="stat-card">
+        <div class="stat-icon available">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10"/>
+            <polyline points="12 6 12 12 16 14"/>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ getDesignerCountByStatus('available') }}</div>
+          <div class="stat-label">空闲设计师</div>
+        </div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-icon busy">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10"/>
+            <line x1="12" y1="8" x2="12" y2="16"/>
+            <line x1="8" y1="12" x2="16" y2="12"/>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ getDesignerCountByStatus('busy') }}</div>
+          <div class="stat-label">忙碌设计师</div>
+        </div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-icon overloaded">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10"/>
+            <line x1="15" y1="9" x2="9" y2="15"/>
+            <line x1="9" y1="9" x2="15" y2="15"/>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ getDesignerCountByStatus('overloaded') }}</div>
+          <div class="stat-label">满载设计师</div>
+        </div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-icon stagnant">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10"/>
+            <path d="M8 12l2 2 4-4"/>
+          </svg>
+        </div>
+        <div class="stat-content">
+          <div class="stat-value">{{ getStagnantProjectsCount() }}</div>
+          <div class="stat-label">停滞期项目</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 843 - 0
src/app/pages/customer-service/consultation-order/components/designer-calendar/designer-calendar.component.scss

@@ -0,0 +1,843 @@
+.designer-calendar-container {
+  .calendar-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    margin-bottom: 24px;
+    
+    .header-left {
+      h4 {
+        font-size: 16px;
+        font-weight: 600;
+        color: #1a1a1a;
+        margin: 0 0 4px 0;
+      }
+      
+      p {
+        font-size: 13px;
+        color: #666;
+        margin: 0;
+      }
+    }
+    
+    .header-actions {
+      display: flex;
+      gap: 12px;
+      
+      .btn {
+        padding: 8px 16px;
+        font-size: 13px;
+        border-radius: 6px;
+        border: none;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        transition: all 0.2s ease;
+        
+        &.btn-secondary {
+          background: #f8fafc;
+          color: #475569;
+          border: 1px solid #e2e8f0;
+          
+          &:hover {
+            background: #f1f5f9;
+            border-color: #cbd5e1;
+          }
+        }
+      }
+    }
+  }
+}
+
+.calendar-filters {
+  display: flex;
+  gap: 20px;
+  align-items: flex-end;
+  margin-bottom: 24px;
+  padding: 20px;
+  background: #f8fafc;
+  border-radius: 8px;
+  border: 1px solid #e2e8f0;
+  flex-wrap: wrap;
+  
+  .filter-group {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    
+    .filter-label {
+      font-size: 13px;
+      font-weight: 500;
+      color: #374151;
+    }
+    
+    .filter-select {
+      padding: 8px 12px;
+      border: 1px solid #d1d5db;
+      border-radius: 6px;
+      font-size: 14px;
+      background: white;
+      min-width: 120px;
+      
+      &:focus {
+        outline: none;
+        border-color: #4f46e5;
+        box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+      }
+    }
+    
+    .checkbox-label {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      cursor: pointer;
+      font-size: 14px;
+      color: #374151;
+      margin-top: 20px;
+      
+      input[type="checkbox"] {
+        display: none;
+      }
+      
+      .checkmark {
+        width: 18px;
+        height: 18px;
+        border: 2px solid #d1d5db;
+        border-radius: 4px;
+        position: relative;
+        transition: all 0.2s ease;
+        
+        &::after {
+          content: '';
+          position: absolute;
+          left: 5px;
+          top: 2px;
+          width: 4px;
+          height: 8px;
+          border: solid white;
+          border-width: 0 2px 2px 0;
+          transform: rotate(45deg);
+          opacity: 0;
+          transition: opacity 0.2s ease;
+        }
+      }
+      
+      input:checked + .checkmark {
+        background: #4f46e5;
+        border-color: #4f46e5;
+        
+        &::after {
+          opacity: 1;
+        }
+      }
+    }
+  }
+}
+
+// 日历视图样式
+.calendar-view {
+  .calendar-navigation {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 20px;
+    margin-bottom: 20px;
+    
+    .nav-btn {
+      width: 36px;
+      height: 36px;
+      border: 1px solid #e2e8f0;
+      background: white;
+      border-radius: 6px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      transition: all 0.2s ease;
+      
+      &:hover {
+        border-color: #4f46e5;
+        color: #4f46e5;
+      }
+    }
+    
+    .current-period {
+      font-size: 18px;
+      font-weight: 600;
+      color: #1e293b;
+      margin: 0;
+      min-width: 200px;
+      text-align: center;
+    }
+  }
+  
+  .calendar-grid {
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    overflow: hidden;
+    background: white;
+    
+    .calendar-header-row {
+      display: flex;
+      background: #f8fafc;
+      border-bottom: 1px solid #e2e8f0;
+      
+      .designer-column {
+        width: 200px;
+        padding: 12px 16px;
+        font-weight: 600;
+        color: #374151;
+        border-right: 1px solid #e2e8f0;
+        display: flex;
+        align-items: center;
+      }
+      
+      .date-column {
+        flex: 1;
+        min-width: 80px;
+        padding: 12px 8px;
+        text-align: center;
+        border-right: 1px solid #e2e8f0;
+        
+        &:last-child {
+          border-right: none;
+        }
+        
+        .date-label {
+          font-size: 14px;
+          font-weight: 600;
+          color: #1e293b;
+          margin-bottom: 2px;
+        }
+        
+        .weekday-label {
+          font-size: 12px;
+          color: #64748b;
+        }
+      }
+    }
+    
+    .calendar-row {
+      display: flex;
+      border-bottom: 1px solid #e2e8f0;
+      
+      &:last-child {
+        border-bottom: none;
+      }
+      
+      .designer-cell {
+        width: 200px;
+        padding: 16px;
+        border-right: 1px solid #e2e8f0;
+        
+        .designer-info {
+          display: flex;
+          align-items: center;
+          gap: 12px;
+          
+          .designer-avatar {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            overflow: hidden;
+            flex-shrink: 0;
+            
+            img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+          }
+          
+          .designer-details {
+            flex: 1;
+            
+            .designer-name {
+              font-size: 14px;
+              font-weight: 600;
+              color: #1e293b;
+              margin-bottom: 4px;
+            }
+            
+            .designer-group {
+              font-size: 12px;
+              color: #64748b;
+              margin-bottom: 6px;
+            }
+            
+            .designer-stats {
+              display: flex;
+              flex-direction: column;
+              gap: 2px;
+              
+              .stat-item {
+                font-size: 11px;
+                color: #64748b;
+                
+                &.critical {
+                  color: #dc2626;
+                  font-weight: 500;
+                }
+                
+                &.warning {
+                  color: #d97706;
+                  font-weight: 500;
+                }
+              }
+            }
+          }
+        }
+      }
+      
+      .date-cell {
+        flex: 1;
+        min-width: 80px;
+        padding: 16px 8px;
+        border-right: 1px solid #e2e8f0;
+        
+        &:last-child {
+          border-right: none;
+        }
+        
+        .date-content {
+          height: 40px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 4px;
+          
+          &.available-day {
+            background: #f0fdf4;
+            border: 1px dashed #bbf7d0;
+          }
+          
+          &.has-events {
+            background: #eff6ff;
+            border: 1px solid #dbeafe;
+          }
+          
+          &.review-day {
+            background: #fef3c7;
+            border: 1px solid #fcd34d;
+          }
+          
+          &.project-day {
+            background: #e0e7ff;
+            border: 1px solid #c7d2fe;
+          }
+          
+          &.vacation-day {
+            background: #f3f4f6;
+            border: 1px solid #d1d5db;
+          }
+          
+          &.today {
+            box-shadow: 0 0 0 2px #4f46e5;
+          }
+          
+          .event-indicators {
+            display: flex;
+            gap: 4px;
+            flex-wrap: wrap;
+            
+            .event-indicator {
+              width: 20px;
+              height: 20px;
+              border-radius: 4px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              font-size: 10px;
+              font-weight: 600;
+              color: white;
+              
+              &.event-project {
+                background: #4f46e5;
+              }
+              
+              &.event-review {
+                background: #f59e0b;
+              }
+              
+              &.event-vacation {
+                background: #6b7280;
+              }
+            }
+          }
+          
+          .empty-indicator {
+            width: 20px;
+            height: 20px;
+            border-radius: 50%;
+            background: #e5e7eb;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 列表视图样式
+.list-view {
+  .empty-state {
+    text-align: center;
+    padding: 48px 24px;
+    color: #6b7280;
+    
+    .empty-icon {
+      margin-bottom: 16px;
+      
+      svg {
+        color: #d1d5db;
+      }
+    }
+    
+    p {
+      margin: 0 0 8px 0;
+      font-size: 14px;
+      
+      &.empty-hint {
+        font-size: 13px;
+        color: #9ca3af;
+      }
+    }
+  }
+  
+  .designer-list {
+    display: grid;
+    gap: 20px;
+    
+    .designer-card {
+      background: white;
+      border: 1px solid #e2e8f0;
+      border-radius: 8px;
+      overflow: hidden;
+      transition: all 0.2s ease;
+      
+      &:hover {
+        border-color: #cbd5e1;
+        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+      }
+      
+      .card-header {
+        padding: 20px;
+        border-bottom: 1px solid #f1f5f9;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        
+        .designer-info {
+          display: flex;
+          align-items: center;
+          gap: 16px;
+          
+          .designer-avatar {
+            width: 56px;
+            height: 56px;
+            border-radius: 50%;
+            overflow: hidden;
+            
+            img {
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+          }
+          
+          .designer-details {
+            .designer-name {
+              font-size: 16px;
+              font-weight: 600;
+              color: #1e293b;
+              margin-bottom: 6px;
+            }
+            
+            .designer-meta {
+              display: flex;
+              align-items: center;
+              gap: 8px;
+              margin-bottom: 8px;
+              
+              .designer-group {
+                font-size: 12px;
+                color: #64748b;
+                background: #f1f5f9;
+                padding: 2px 6px;
+                border-radius: 4px;
+              }
+              
+              .leader-badge {
+                font-size: 12px;
+                color: #dc2626;
+                background: #fef2f2;
+                padding: 2px 6px;
+                border-radius: 4px;
+                border: 1px solid #fecaca;
+              }
+              
+              .status-indicator {
+                width: 8px;
+                height: 8px;
+                border-radius: 50%;
+                
+                &.available {
+                  background: #10b981;
+                }
+                
+                &.busy {
+                  background: #f59e0b;
+                }
+                
+                &.overloaded {
+                  background: #8b5cf6;
+                }
+                
+                &.stagnant {
+                  background: #ef4444;
+                }
+              }
+              
+              .status-text {
+                font-size: 12px;
+                color: #64748b;
+              }
+            }
+          }
+        }
+        
+        .designer-actions {
+          display: flex;
+          gap: 8px;
+          
+          .action-btn {
+            width: 32px;
+            height: 32px;
+            border: 1px solid #e2e8f0;
+            background: white;
+            border-radius: 6px;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: #64748b;
+            transition: all 0.2s ease;
+            
+            &:hover {
+              border-color: #4f46e5;
+              color: #4f46e5;
+            }
+          }
+        }
+      }
+      
+      .card-content {
+        padding: 20px;
+        display: grid;
+        gap: 20px;
+        
+        .workload-section,
+        .schedule-section,
+        .availability-section {
+          h5 {
+            font-size: 14px;
+            font-weight: 600;
+            color: #374151;
+            margin: 0 0 12px 0;
+          }
+        }
+        
+        .workload-stats {
+          display: grid;
+          grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+          gap: 16px;
+          
+          .stat-item {
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
+            
+            .stat-label {
+              font-size: 12px;
+              color: #64748b;
+            }
+            
+            .stat-value {
+              font-size: 16px;
+              font-weight: 600;
+              color: #1e293b;
+              
+              &.critical {
+                color: #dc2626;
+              }
+              
+              &.warning {
+                color: #d97706;
+              }
+            }
+          }
+        }
+        
+        .no-events {
+          font-size: 13px;
+          color: #9ca3af;
+          font-style: italic;
+          margin: 0;
+        }
+        
+        .event-list {
+          display: flex;
+          flex-direction: column;
+          gap: 8px;
+          
+          .event-item {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+            padding: 8px 12px;
+            background: #f8fafc;
+            border-radius: 6px;
+            
+            .event-date {
+              font-size: 12px;
+              color: #64748b;
+              font-weight: 500;
+              min-width: 40px;
+            }
+            
+            .event-content {
+              flex: 1;
+              
+              .event-title {
+                font-size: 13px;
+                color: #1e293b;
+                margin-bottom: 2px;
+              }
+              
+              .event-type {
+                font-size: 11px;
+                padding: 2px 6px;
+                border-radius: 4px;
+                
+                &.event-project {
+                  background: #e0e7ff;
+                  color: #4338ca;
+                }
+                
+                &.event-review {
+                  background: #fef3c7;
+                  color: #92400e;
+                }
+                
+                &.event-vacation {
+                  background: #f3f4f6;
+                  color: #6b7280;
+                }
+              }
+            }
+          }
+          
+          .more-events {
+            font-size: 12px;
+            color: #64748b;
+            text-align: center;
+            padding: 4px;
+          }
+        }
+        
+        .availability-info {
+          display: flex;
+          gap: 24px;
+          
+          .availability-item {
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
+            
+            .availability-label {
+              font-size: 12px;
+              color: #64748b;
+            }
+            
+            .availability-value {
+              font-size: 14px;
+              font-weight: 600;
+              
+              &.high-recommend {
+                color: #059669;
+              }
+              
+              &.recommend {
+                color: #0369a1;
+              }
+              
+              &.caution {
+                color: #d97706;
+              }
+              
+              &.not-recommend {
+                color: #dc2626;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 统计摘要样式
+.calendar-summary {
+  margin-top: 24px;
+  
+  .summary-stats {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+    gap: 16px;
+    
+    .stat-card {
+      background: white;
+      border: 1px solid #e2e8f0;
+      border-radius: 8px;
+      padding: 20px;
+      display: flex;
+      align-items: center;
+      gap: 16px;
+      
+      .stat-icon {
+        width: 48px;
+        height: 48px;
+        border-radius: 8px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        
+        &.available {
+          background: #f0fdf4;
+          color: #059669;
+        }
+        
+        &.busy {
+          background: #fef3c7;
+          color: #d97706;
+        }
+        
+        &.overloaded {
+          background: #f3f4f6;
+          color: #8b5cf6;
+        }
+        
+        &.stagnant {
+          background: #fef2f2;
+          color: #dc2626;
+        }
+      }
+      
+      .stat-content {
+        flex: 1;
+        
+        .stat-value {
+          font-size: 24px;
+          font-weight: 700;
+          color: #1e293b;
+          margin-bottom: 4px;
+        }
+        
+        .stat-label {
+          font-size: 13px;
+          color: #64748b;
+        }
+      }
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 1024px) {
+  .calendar-view {
+    .calendar-grid {
+      .designer-cell {
+        width: 150px;
+        
+        .designer-info {
+          flex-direction: column;
+          align-items: flex-start;
+          gap: 8px;
+          
+          .designer-avatar {
+            width: 32px;
+            height: 32px;
+          }
+        }
+      }
+      
+      .date-column,
+      .date-cell {
+        min-width: 60px;
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .designer-calendar-container {
+    .calendar-header {
+      flex-direction: column;
+      gap: 16px;
+      align-items: stretch;
+      
+      .header-actions {
+        justify-content: flex-end;
+      }
+    }
+  }
+  
+  .calendar-filters {
+    flex-direction: column;
+    gap: 16px;
+    
+    .filter-group {
+      .filter-select {
+        min-width: 100%;
+      }
+    }
+  }
+  
+  .calendar-view {
+    .calendar-grid {
+      overflow-x: auto;
+      
+      .calendar-header-row,
+      .calendar-row {
+        min-width: 800px;
+      }
+    }
+  }
+  
+  .list-view {
+    .designer-list {
+      .designer-card {
+        .card-header {
+          flex-direction: column;
+          gap: 16px;
+          align-items: flex-start;
+          
+          .designer-actions {
+            align-self: flex-end;
+          }
+        }
+        
+        .card-content {
+          .workload-stats {
+            grid-template-columns: repeat(2, 1fr);
+          }
+          
+          .availability-info {
+            flex-direction: column;
+            gap: 12px;
+          }
+        }
+      }
+    }
+  }
+}

+ 419 - 0
src/app/pages/customer-service/consultation-order/components/designer-calendar/designer-calendar.component.ts

@@ -0,0 +1,419 @@
+import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+export interface Designer {
+  id: string;
+  name: string;
+  avatar?: string;
+  groupId: string;
+  groupName: string;
+  isLeader: boolean;
+  status: 'available' | 'busy' | 'stagnant' | 'overloaded';
+  currentProjects: number;
+  lastOrderDate?: string;
+  idleDays?: number;
+  completedThisMonth?: number;
+  averageCycle?: number;
+  upcomingEvents?: DesignerEvent[];
+  workload?: number; // 0-100
+  nextAvailableDate?: Date;
+}
+
+export interface DesignerEvent {
+  id: string;
+  date: Date;
+  title: string;
+  type: 'project' | 'review' | 'vacation';
+  projectId?: string;
+  duration: number; // 小时
+}
+
+export interface ProjectGroup {
+  id: string;
+  name: string;
+  leaderId: string;
+  memberIds: string[];
+}
+
+@Component({
+  selector: 'app-designer-calendar',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './designer-calendar.component.html',
+  styleUrls: ['./designer-calendar.component.scss']
+})
+export class DesignerCalendarComponent implements OnInit {
+  @Input() designers: Designer[] = [];
+  @Input() selectedDate: Date = new Date();
+  @Input() projectGroups: ProjectGroup[] = [];
+  @Output() designerSelected = new EventEmitter<Designer>();
+  @Output() assignmentRequested = new EventEmitter<Designer>();
+
+  // 视图模式
+  viewMode: 'calendar' | 'list' = 'calendar';
+  
+  // 筛选条件
+  selectedGroup: string = '';
+  selectedStatus: string = '';
+  timeRange: 'week' | 'month' | 'quarter' = 'week';
+  hideStagnantProjects: boolean = false;
+  
+  // 日历数据
+  currentDate: Date = new Date();
+  calendarDates: Date[] = [];
+  filteredDesigners: Designer[] = [];
+
+  ngOnInit() {
+    this.initializeMockData();
+    this.generateCalendarDates();
+    this.applyFilters();
+  }
+
+  private initializeMockData() {
+    if (this.designers.length === 0) {
+      this.designers = [
+        {
+          id: '1',
+          name: '张设计师',
+          avatar: '/assets/avatars/designer1.jpg',
+          groupId: '1',
+          groupName: '家装组',
+          isLeader: true,
+          status: 'available',
+          currentProjects: 2,
+          completedThisMonth: 5,
+          averageCycle: 15,
+          idleDays: 3,
+          workload: 60,
+          nextAvailableDate: new Date(),
+          upcomingEvents: [
+            {
+              id: '1',
+              date: new Date(Date.now() + 86400000),
+              title: '客厅设计方案',
+              type: 'project',
+              projectId: 'p1',
+              duration: 4
+            }
+          ]
+        },
+        {
+          id: '2',
+          name: '李设计师',
+          avatar: '/assets/avatars/designer2.jpg',
+          groupId: '2',
+          groupName: '工装组',
+          isLeader: false,
+          status: 'busy',
+          currentProjects: 4,
+          completedThisMonth: 3,
+          averageCycle: 18,
+          idleDays: 0,
+          workload: 85,
+          nextAvailableDate: new Date(Date.now() + 5 * 86400000),
+          upcomingEvents: [
+            {
+              id: '2',
+              date: new Date(),
+              title: '办公室设计对图',
+              type: 'review',
+              projectId: 'p2',
+              duration: 2
+            }
+          ]
+        },
+        {
+          id: '3',
+          name: '王设计师',
+          avatar: '/assets/avatars/designer3.jpg',
+          groupId: '1',
+          groupName: '家装组',
+          isLeader: false,
+          status: 'stagnant',
+          currentProjects: 1,
+          completedThisMonth: 1,
+          averageCycle: 25,
+          idleDays: 15,
+          workload: 20,
+          nextAvailableDate: new Date(),
+          upcomingEvents: []
+        }
+      ];
+    }
+
+    if (this.projectGroups.length === 0) {
+      this.projectGroups = [
+        { id: '1', name: '家装组', leaderId: '1', memberIds: ['1', '3'] },
+        { id: '2', name: '工装组', leaderId: '2', memberIds: ['2'] }
+      ];
+    }
+  }
+
+  private generateCalendarDates() {
+    this.calendarDates = [];
+    const startDate = new Date(this.currentDate);
+    
+    if (this.timeRange === 'week') {
+      // 获取本周的日期
+      const dayOfWeek = startDate.getDay();
+      startDate.setDate(startDate.getDate() - dayOfWeek);
+      
+      for (let i = 0; i < 7; i++) {
+        const date = new Date(startDate);
+        date.setDate(startDate.getDate() + i);
+        this.calendarDates.push(date);
+      }
+    } else if (this.timeRange === 'month') {
+      // 获取本月的日期
+      startDate.setDate(1);
+      const daysInMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate();
+      
+      for (let i = 0; i < daysInMonth; i++) {
+        const date = new Date(startDate);
+        date.setDate(i + 1);
+        this.calendarDates.push(date);
+      }
+    } else {
+      // 获取本季度的周数
+      const quarterStart = new Date(startDate.getFullYear(), Math.floor(startDate.getMonth() / 3) * 3, 1);
+      const quarterEnd = new Date(startDate.getFullYear(), Math.floor(startDate.getMonth() / 3) * 3 + 3, 0);
+      
+      const current = new Date(quarterStart);
+      while (current <= quarterEnd) {
+        this.calendarDates.push(new Date(current));
+        current.setDate(current.getDate() + 7); // 按周显示
+      }
+    }
+  }
+
+  private applyFilters() {
+    this.filteredDesigners = this.designers.filter(designer => {
+      // 项目组筛选
+      if (this.selectedGroup && designer.groupId !== this.selectedGroup) {
+        return false;
+      }
+      
+      // 状态筛选
+      if (this.selectedStatus && designer.status !== this.selectedStatus) {
+        return false;
+      }
+      
+      // 停滞期项目筛选
+      if (this.hideStagnantProjects && designer.status === 'stagnant') {
+        return false;
+      }
+      
+      return true;
+    });
+  }
+
+  private getGroupName(groupId: string): string {
+    const group = this.projectGroups.find(g => g.id === groupId);
+    return group ? group.name : '';
+  }
+
+  // 视图切换
+  toggleView() {
+    this.viewMode = this.viewMode === 'calendar' ? 'list' : 'calendar';
+  }
+
+  // 筛选器事件
+  onGroupChange() {
+    this.applyFilters();
+  }
+
+  onStatusChange() {
+    this.applyFilters();
+  }
+
+  onTimeRangeChange() {
+    this.generateCalendarDates();
+    this.applyFilters();
+  }
+
+  onFilterChange() {
+    this.applyFilters();
+  }
+
+  // 日历导航
+  previousPeriod() {
+    if (this.timeRange === 'week') {
+      this.currentDate.setDate(this.currentDate.getDate() - 7);
+    } else if (this.timeRange === 'month') {
+      this.currentDate.setMonth(this.currentDate.getMonth() - 1);
+    } else {
+      this.currentDate.setMonth(this.currentDate.getMonth() - 3);
+    }
+    this.generateCalendarDates();
+  }
+
+  nextPeriod() {
+    if (this.timeRange === 'week') {
+      this.currentDate.setDate(this.currentDate.getDate() + 7);
+    } else if (this.timeRange === 'month') {
+      this.currentDate.setMonth(this.currentDate.getMonth() + 1);
+    } else {
+      this.currentDate.setMonth(this.currentDate.getMonth() + 3);
+    }
+    this.generateCalendarDates();
+  }
+
+  getCurrentPeriodLabel(): string {
+    const year = this.currentDate.getFullYear();
+    const month = this.currentDate.getMonth() + 1;
+    
+    if (this.timeRange === 'week') {
+      const weekStart = new Date(this.currentDate);
+      weekStart.setDate(this.currentDate.getDate() - this.currentDate.getDay());
+      const weekEnd = new Date(weekStart);
+      weekEnd.setDate(weekStart.getDate() + 6);
+      
+      return `${weekStart.getMonth() + 1}/${weekStart.getDate()} - ${weekEnd.getMonth() + 1}/${weekEnd.getDate()}`;
+    } else if (this.timeRange === 'month') {
+      return `${year}年${month}月`;
+    } else {
+      const quarter = Math.floor(month / 3) + 1;
+      return `${year}年第${quarter}季度`;
+    }
+  }
+
+  // 日期格式化
+  formatDate(date: Date, format: string): string {
+    const options: Intl.DateTimeFormatOptions = {};
+    
+    if (format === 'MM/dd') {
+      return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
+    } else if (format === 'EEE') {
+      const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
+      return weekdays[date.getDay()];
+    }
+    
+    return date.toLocaleDateString();
+  }
+
+  // 获取日期事件
+  getDateEvents(designer: Designer, date: Date): DesignerEvent[] {
+    if (!designer.upcomingEvents) return [];
+    return designer.upcomingEvents.filter(event => {
+      const eventDate = new Date(event.date);
+      return eventDate.toDateString() === date.toDateString();
+    });
+  }
+
+  // 获取日期单元格样式
+  getDateCellClass(designer: Designer, date: Date): string {
+    const events = this.getDateEvents(designer, date);
+    const classes = [];
+    
+    if (events.length > 0) {
+      classes.push('has-events');
+      
+      if (events.some(e => e.type === 'review')) {
+        classes.push('review-day');
+      }
+      if (events.some(e => e.type === 'project')) {
+        classes.push('project-day');
+      }
+      if (events.some(e => e.type === 'vacation')) {
+        classes.push('vacation-day');
+      }
+    } else {
+      classes.push('available-day');
+    }
+    
+    // 今天
+    if (date.toDateString() === new Date().toDateString()) {
+      classes.push('today');
+    }
+    
+    return classes.join(' ');
+  }
+
+  // 获取事件样式
+  getEventClass(event: DesignerEvent): string {
+    return `event-${event.type}`;
+  }
+
+  // 获取状态文本
+  getStatusText(status: string): string {
+    const statusMap = {
+      'available': '空闲',
+      'busy': '忙碌',
+      'overloaded': '满载',
+      'stagnant': '停滞期'
+    };
+    return statusMap[status as keyof typeof statusMap] || status;
+  }
+
+  // 获取事件类型文本
+  getEventTypeText(type: string): string {
+    const typeMap = {
+      'project': '项目工作',
+      'review': '对图时间',
+      'vacation': '休假'
+    };
+    return typeMap[type as keyof typeof typeMap] || type;
+  }
+
+  // 获取空闲天数样式
+  getIdleDaysClass(days: number): string {
+    if (days >= 10) return 'critical';
+    if (days >= 5) return 'warning';
+    return 'normal';
+  }
+
+  // 获取下次可接单日期
+  getNextAvailableDate(designer: Designer): string {
+    if (!designer.nextAvailableDate) return '立即可用';
+    const nextDate = new Date(designer.nextAvailableDate);
+    const today = new Date();
+    const diffTime = nextDate.getTime() - today.getTime();
+    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+    
+    if (diffDays <= 0) return '立即可用';
+    if (diffDays === 1) return '明天';
+    return `${diffDays}天后`;
+  }
+
+  // 获取推荐分配等级
+  getRecommendationClass(designer: Designer): string {
+    if (designer.status === 'available' && (designer.idleDays ?? 0) >= 5) return 'high-recommend';
+    if (designer.status === 'available') return 'recommend';
+    if (designer.status === 'busy') return 'caution';
+    return 'not-recommend';
+  }
+
+  // 获取推荐分配文本
+  getRecommendationText(designer: Designer): string {
+    if (designer.status === 'available' && (designer.idleDays ?? 0) >= 5) return '强烈推荐';
+    if (designer.status === 'available') return '推荐';
+    if (designer.status === 'busy') return '谨慎分配';
+    return '不推荐';
+  }
+
+  // 统计方法
+  getDesignerCountByStatus(status: string): number {
+    return this.filteredDesigners.filter(d => d.status === status).length;
+  }
+
+  getStagnantProjectsCount(): number {
+    return this.filteredDesigners
+      .filter(d => d.status === 'stagnant')
+      .reduce((sum, d) => sum + d.currentProjects, 0);
+  }
+
+  // 操作方法
+  refreshData() {
+    // 刷新数据逻辑
+    this.applyFilters();
+  }
+
+  viewDesignerDetail(designer: Designer) {
+    this.designerSelected.emit(designer);
+  }
+
+  assignToDesigner(designer: Designer) {
+    this.assignmentRequested.emit(designer);
+  }
+}

+ 100 - 0
src/app/pages/customer-service/consultation-order/components/order-form/order-form.component.html

@@ -0,0 +1,100 @@
+<!-- 订单基本信息表单 -->
+<div class="order-form-container">
+  <div class="form-header">
+    <h3>订单信息</h3>
+    <p>请填写订单的基本信息和要求</p>
+  </div>
+
+  <form [formGroup]="orderForm" class="order-form">
+    <!-- 第一行:订单金额、装修类型 -->
+    <div class="form-row">
+      <div class="form-field">
+        <label for="orderAmount" class="field-label">订单金额 <span class="required">*</span></label>
+        <div class="input-with-unit">
+          <input 
+            type="number" 
+            id="orderAmount" 
+            formControlName="orderAmount" 
+            placeholder="请输入订单总金额" 
+            class="field-input"
+            min="0"
+            step="0.01">
+          <span class="input-unit">元</span>
+        </div>
+        @if (orderForm.get('orderAmount')?.invalid && orderForm.get('orderAmount')?.touched) {
+          <div class="field-error">订单金额为必填项</div>
+        }
+      </div>
+      
+      <div class="form-field">
+        <label for="decorationType" class="field-label">装修类型 <span class="required">*</span></label>
+        <select id="decorationType" formControlName="decorationType" class="field-select">
+          <option value="">请选择装修类型</option>
+          <option value="家装">家装</option>
+          <option value="工装">工装</option>
+        </select>
+        @if (orderForm.get('decorationType')?.invalid && orderForm.get('decorationType')?.touched) {
+          <div class="field-error">装修类型为必填项</div>
+        }
+      </div>
+    </div>
+
+    <!-- 第二行:小图时间、大图时间(选填) -->
+    <div class="form-row">
+      <div class="form-field">
+        <label for="smallImageTime" class="field-label">小图时间 <span class="required">*</span></label>
+        <input 
+          type="date" 
+          id="smallImageTime" 
+          formControlName="smallImageTime" 
+          class="field-input">
+        @if (orderForm.get('smallImageTime')?.invalid && orderForm.get('smallImageTime')?.touched) {
+          <div class="field-error">小图时间为必填项</div>
+        }
+      </div>
+      
+      <div class="form-field">
+        <label for="largeImageTime" class="field-label">大图时间</label>
+        <input 
+          type="date" 
+          id="largeImageTime" 
+          formControlName="largeImageTime" 
+          class="field-input">
+        <div class="field-hint">选填,如有大图需求请填写</div>
+      </div>
+    </div>
+
+    <!-- 需求原因 -->
+    <div class="form-row">
+      <div class="form-field full-width">
+        <label for="requirementReason" class="field-label">需求原因 <span class="required">*</span></label>
+        <textarea 
+          id="requirementReason" 
+          formControlName="requirementReason" 
+          placeholder="请详细描述客户的装修需求和原因" 
+          class="field-textarea"
+          rows="4"></textarea>
+        @if (orderForm.get('requirementReason')?.invalid && orderForm.get('requirementReason')?.touched) {
+          <div class="field-error">需求原因为必填项</div>
+        }
+      </div>
+    </div>
+
+    <!-- 报价明细组件 -->
+    <div class="quotation-section">
+      <app-quotation-detail 
+        [quotationItems]="quotationItems"
+        (quotationChange)="onQuotationChange($event)">
+      </app-quotation-detail>
+    </div>
+
+    <!-- 设计师分配组件 -->
+    <div class="assignment-section">
+      <app-designer-assignment 
+        [selectedDesigners]="selectedDesigners"
+        [quotationItems]="quotationItems"
+        (designerChange)="onDesignerChange($event)">
+      </app-designer-assignment>
+    </div>
+  </form>
+</div>

+ 176 - 0
src/app/pages/customer-service/consultation-order/components/order-form/order-form.component.scss

@@ -0,0 +1,176 @@
+.order-form-container {
+  background: white;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  margin-bottom: 24px;
+
+  .form-header {
+    margin-bottom: 24px;
+    
+    h3 {
+      font-size: 18px;
+      font-weight: 600;
+      color: #1a1a1a;
+      margin: 0 0 8px 0;
+    }
+    
+    p {
+      font-size: 14px;
+      color: #666;
+      margin: 0;
+    }
+  }
+}
+
+.order-form {
+  .form-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 20px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  
+  .form-field {
+    flex: 1;
+    
+    &.full-width {
+      flex: 1 1 100%;
+    }
+    
+    .field-label {
+      display: block;
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 8px;
+      
+      .required {
+        color: #ff4757;
+        margin-left: 2px;
+      }
+    }
+    
+    .field-input,
+    .field-select,
+    .field-textarea {
+      width: 100%;
+      padding: 12px 16px;
+      border: 1px solid #e1e5e9;
+      border-radius: 8px;
+      font-size: 14px;
+      transition: all 0.2s ease;
+      
+      &:focus {
+        outline: none;
+        border-color: #4f46e5;
+        box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
+      }
+      
+      &::placeholder {
+        color: #9ca3af;
+      }
+      
+      &:invalid {
+        border-color: #ef4444;
+      }
+    }
+    
+    .field-textarea {
+      resize: vertical;
+      min-height: 100px;
+      font-family: inherit;
+    }
+    
+    .input-with-unit {
+      position: relative;
+      display: flex;
+      align-items: center;
+      
+      .field-input {
+        padding-right: 50px;
+      }
+      
+      .input-unit {
+        position: absolute;
+        right: 16px;
+        font-size: 14px;
+        color: #666;
+        pointer-events: none;
+      }
+    }
+    
+    .field-error {
+      margin-top: 6px;
+      font-size: 12px;
+      color: #ef4444;
+    }
+    
+    .field-hint {
+      margin-top: 6px;
+      font-size: 12px;
+      color: #6b7280;
+    }
+  }
+}
+
+.quotation-section,
+.assignment-section {
+  margin-top: 32px;
+  padding-top: 24px;
+  border-top: 1px solid #e5e7eb;
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .order-form {
+    .form-row {
+      flex-direction: column;
+      gap: 16px;
+    }
+  }
+  
+  .order-form-container {
+    padding: 16px;
+  }
+}
+
+// 表单验证状态
+.form-field {
+  .field-input.ng-invalid.ng-touched,
+  .field-select.ng-invalid.ng-touched,
+  .field-textarea.ng-invalid.ng-touched {
+    border-color: #ef4444;
+    background-color: #fef2f2;
+  }
+  
+  .field-input.ng-valid.ng-touched,
+  .field-select.ng-valid.ng-touched,
+  .field-textarea.ng-valid.ng-touched {
+    border-color: #10b981;
+  }
+}
+
+// 金额匹配提示
+.amount-mismatch-warning {
+  background: #fef3cd;
+  border: 1px solid #f6cc6d;
+  border-radius: 8px;
+  padding: 12px 16px;
+  margin-top: 16px;
+  
+  .warning-text {
+    font-size: 14px;
+    color: #92400e;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    svg {
+      flex-shrink: 0;
+    }
+  }
+}

+ 113 - 0
src/app/pages/customer-service/consultation-order/components/order-form/order-form.component.ts

@@ -0,0 +1,113 @@
+import { Component, Input, Output, EventEmitter, signal, computed } from '@angular/core';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { DesignerAssignmentComponent, DesignerAssignment, QuotationItem } from '../designer-assignment/designer-assignment.component';
+import { QuotationDetailComponent } from '../quotation-detail/quotation-detail.component';
+
+export interface OrderFormData {
+  orderAmount: number;
+  decorationType: string;
+  smallImageTime: string;
+  largeImageTime?: string;
+  requirementReason: string;
+}
+
+@Component({
+  selector: 'app-order-form',
+  standalone: true,
+  imports: [CommonModule, ReactiveFormsModule, DesignerAssignmentComponent, QuotationDetailComponent],
+  templateUrl: './order-form.component.html',
+  styleUrls: ['./order-form.component.scss']
+})
+export class OrderFormComponent {
+  @Input() initialData?: Partial<OrderFormData>;
+  @Output() formChange = new EventEmitter<OrderFormData>();
+  @Output() validChange = new EventEmitter<boolean>();
+
+  orderForm: FormGroup;
+  quotationItems = signal<QuotationItem[]>([]);
+  selectedDesigners = signal<DesignerAssignment[]>([]);
+
+  // 计算总金额是否匹配
+  totalAmountMatch = computed(() => {
+    const formAmount = this.orderForm?.get('orderAmount')?.value || 0;
+    const quotationTotal = this.quotationItems().reduce((sum, item) => sum + item.amount, 0);
+    return Math.abs(formAmount - quotationTotal) < 0.01;
+  });
+
+  constructor(private fb: FormBuilder) {
+    this.orderForm = this.createForm();
+    this.setupFormSubscription();
+  }
+
+  ngOnInit() {
+    if (this.initialData) {
+      this.orderForm.patchValue(this.initialData);
+    }
+  }
+
+  private createForm(): FormGroup {
+    return this.fb.group({
+      orderAmount: [0, [Validators.required, Validators.min(0.01)]],
+      decorationType: ['', Validators.required],
+      smallImageTime: ['', Validators.required],
+      largeImageTime: [''],
+      requirementReason: ['', [Validators.required, Validators.minLength(10)]]
+    });
+  }
+
+  private setupFormSubscription() {
+    this.orderForm.valueChanges.subscribe(value => {
+      this.formChange.emit(value);
+      this.validChange.emit(this.isFormValid());
+    });
+  }
+
+  onQuotationChange(items: QuotationItem[]) {
+    this.quotationItems.set(items);
+    
+    // 自动更新订单总金额
+    const totalAmount = items.reduce((sum, item) => sum + item.amount, 0);
+    this.orderForm.patchValue({ orderAmount: totalAmount }, { emitEvent: false });
+    
+    this.emitFormChange();
+  }
+
+  onDesignerChange(designers: DesignerAssignment[]) {
+    this.selectedDesigners.set(designers);
+    this.emitFormChange();
+  }
+
+  private emitFormChange() {
+    const formData: OrderFormData = {
+      ...this.orderForm.value,
+      quotationItems: this.quotationItems(),
+      designerAssignments: this.selectedDesigners()
+    };
+    this.formChange.emit(formData);
+    this.validChange.emit(this.isFormValid());
+  }
+
+  isFormValid(): boolean {
+    const formValid = this.orderForm.valid;
+    const hasQuotation = this.quotationItems().length > 0;
+    const hasDesigners = this.selectedDesigners().length > 0;
+    const amountMatch = this.totalAmountMatch();
+    
+    return formValid && hasQuotation && hasDesigners && amountMatch;
+  }
+
+  getFormData(): OrderFormData & { quotationItems: QuotationItem[], designerAssignments: DesignerAssignment[] } {
+    return {
+      ...this.orderForm.value,
+      quotationItems: this.quotationItems(),
+      designerAssignments: this.selectedDesigners()
+    };
+  }
+
+  resetForm() {
+    this.orderForm.reset();
+    this.quotationItems.set([]);
+    this.selectedDesigners.set([]);
+  }
+}

+ 167 - 0
src/app/pages/customer-service/consultation-order/components/quotation-detail/quotation-detail.component.html

@@ -0,0 +1,167 @@
+<!-- 报价明细组件 -->
+<div class="quotation-detail-container">
+  <div class="quotation-header">
+    <div class="header-left">
+      <h4>报价明细</h4>
+      <p>AI辅助生成详细报价清单</p>
+    </div>
+    <div class="header-actions">
+      <button 
+        type="button" 
+        class="btn-secondary btn-sm"
+        (click)="generateAIQuotation()"
+        [disabled]="isGenerating()">
+        @if (isGenerating()) {
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin">
+            <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
+          </svg>
+        } @else {
+          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
+            <path d="M2 17l10 5 10-5"></path>
+            <path d="M2 12l10 5 10-5"></path>
+          </svg>
+        }
+        {{ isGenerating() ? 'AI生成中...' : 'AI智能报价' }}
+      </button>
+      
+      <button 
+        type="button" 
+        class="btn-primary btn-sm"
+        (click)="addQuotationItem()">
+        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+          <line x1="12" y1="5" x2="12" y2="19"></line>
+          <line x1="5" y1="12" x2="19" y2="12"></line>
+        </svg>
+        添加项目
+      </button>
+    </div>
+  </div>
+
+  <!-- 报价项目列表 -->
+  <div class="quotation-items">
+    @if (quotationItems().length === 0) {
+      <div class="empty-state">
+        <div class="empty-icon">
+          <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+            <rect x="3" y="4" width="18" height="14" rx="2" ry="2"></rect>
+            <line x1="8" y1="2" x2="16" y2="2"></line>
+            <line x1="10" y1="11" x2="14" y2="11"></line>
+            <line x1="12" y1="9" x2="12" y2="13"></line>
+          </svg>
+        </div>
+        <p>暂无报价项目</p>
+        <p class="empty-hint">点击"AI智能报价"或"添加项目"开始创建报价清单</p>
+      </div>
+    } @else {
+      @for (item of quotationItems(); track item.id) {
+        <div class="quotation-item" [class.editing]="editingItemId() === item.id">
+          <div class="item-content">
+            <div class="item-info">
+              <div class="room-type">
+                @if (editingItemId() === item.id) {
+                  <select 
+                    [(ngModel)]="editingItem.roomType" 
+                    class="edit-select">
+                    <option value="">选择空间类型</option>
+                    @for (roomType of roomTypeOptions; track roomType) {
+                      <option [value]="roomType">{{ roomType }}</option>
+                    }
+                  </select>
+                } @else {
+                  <span class="room-name">{{ item.roomType }}</span>
+                }
+              </div>
+              
+              <div class="item-amount">
+                @if (editingItemId() === item.id) {
+                  <div class="amount-input">
+                    <input 
+                      type="number" 
+                      [(ngModel)]="editingItem.amount"
+                      class="edit-input"
+                      placeholder="金额"
+                      min="0"
+                      step="0.01">
+                    <span class="unit">元</span>
+                  </div>
+                } @else {
+                  <span class="amount">¥{{ item.amount | number:'1.2-2' }}</span>
+                }
+              </div>
+            </div>
+            
+            @if (item.description || editingItemId() === item.id) {
+              <div class="item-description">
+                @if (editingItemId() === item.id) {
+                  <textarea 
+                    [(ngModel)]="editingItem.description"
+                    class="edit-textarea"
+                    placeholder="项目描述(选填)"
+                    rows="2"></textarea>
+                } @else {
+                  <p>{{ item.description }}</p>
+                }
+              </div>
+            }
+          </div>
+          
+          <div class="item-actions">
+            @if (editingItemId() === item.id) {
+              <button 
+                type="button" 
+                class="action-btn save-btn"
+                (click)="saveItem()">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <polyline points="20 6 9 17 4 12"></polyline>
+                </svg>
+              </button>
+              <button 
+                type="button" 
+                class="action-btn cancel-btn"
+                (click)="cancelEdit()">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <line x1="18" y1="6" x2="6" y2="18"></line>
+                  <line x1="6" y1="6" x2="18" y2="18"></line>
+                </svg>
+              </button>
+            } @else {
+              <button 
+                type="button" 
+                class="action-btn edit-btn"
+                (click)="editItem(item)">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
+                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
+                </svg>
+              </button>
+              <button 
+                type="button" 
+                class="action-btn delete-btn"
+                (click)="deleteItem(item.id)">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <polyline points="3 6 5 6 21 6"></polyline>
+                  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
+                </svg>
+              </button>
+            }
+          </div>
+        </div>
+      }
+    }
+  </div>
+
+  <!-- 总计 -->
+  @if (quotationItems().length > 0) {
+    <div class="quotation-summary">
+      <div class="summary-row">
+        <span class="summary-label">项目总数:</span>
+        <span class="summary-value">{{ quotationItems().length }} 项</span>
+      </div>
+      <div class="summary-row total">
+        <span class="summary-label">总计金额:</span>
+        <span class="summary-value">¥{{ totalAmount() | number:'1.2-2' }}</span>
+      </div>
+    </div>
+  }
+</div>

+ 348 - 0
src/app/pages/customer-service/consultation-order/components/quotation-detail/quotation-detail.component.scss

@@ -0,0 +1,348 @@
+.quotation-detail-container {
+  .quotation-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    margin-bottom: 20px;
+    
+    .header-left {
+      h4 {
+        font-size: 16px;
+        font-weight: 600;
+        color: #1a1a1a;
+        margin: 0 0 4px 0;
+      }
+      
+      p {
+        font-size: 13px;
+        color: #666;
+        margin: 0;
+      }
+    }
+    
+    .header-actions {
+      display: flex;
+      gap: 12px;
+      
+      .btn-sm {
+        padding: 8px 16px;
+        font-size: 13px;
+        border-radius: 6px;
+        border: none;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        transition: all 0.2s ease;
+        
+        &:disabled {
+          opacity: 0.6;
+          cursor: not-allowed;
+        }
+        
+        svg {
+          flex-shrink: 0;
+        }
+      }
+      
+      .btn-secondary {
+        background: #f8fafc;
+        color: #475569;
+        border: 1px solid #e2e8f0;
+        
+        &:hover:not(:disabled) {
+          background: #f1f5f9;
+          border-color: #cbd5e1;
+        }
+      }
+      
+      .btn-primary {
+        background: #4f46e5;
+        color: white;
+        
+        &:hover:not(:disabled) {
+          background: #4338ca;
+        }
+      }
+    }
+  }
+}
+
+.quotation-items {
+  .empty-state {
+    text-align: center;
+    padding: 48px 24px;
+    color: #6b7280;
+    
+    .empty-icon {
+      margin-bottom: 16px;
+      
+      svg {
+        color: #d1d5db;
+      }
+    }
+    
+    p {
+      margin: 0 0 8px 0;
+      font-size: 14px;
+      
+      &.empty-hint {
+        font-size: 13px;
+        color: #9ca3af;
+      }
+    }
+  }
+  
+  .quotation-item {
+    background: #f8fafc;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    padding: 16px;
+    margin-bottom: 12px;
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    transition: all 0.2s ease;
+    
+    &:hover {
+      border-color: #cbd5e1;
+    }
+    
+    &.editing {
+      border-color: #4f46e5;
+      background: #fefbff;
+    }
+    
+    .item-content {
+      flex: 1;
+      
+      .item-info {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 8px;
+        
+        .room-type {
+          .room-name {
+            font-weight: 500;
+            color: #1e293b;
+            font-size: 14px;
+          }
+          
+          .edit-select {
+            padding: 6px 12px;
+            border: 1px solid #d1d5db;
+            border-radius: 6px;
+            font-size: 14px;
+            min-width: 120px;
+            
+            &:focus {
+              outline: none;
+              border-color: #4f46e5;
+              box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+            }
+          }
+        }
+        
+        .item-amount {
+          .amount {
+            font-weight: 600;
+            color: #059669;
+            font-size: 15px;
+          }
+          
+          .amount-input {
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            
+            .edit-input {
+              padding: 6px 12px;
+              border: 1px solid #d1d5db;
+              border-radius: 6px;
+              font-size: 14px;
+              width: 100px;
+              text-align: right;
+              
+              &:focus {
+                outline: none;
+                border-color: #4f46e5;
+                box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+              }
+            }
+            
+            .unit {
+              font-size: 13px;
+              color: #6b7280;
+            }
+          }
+        }
+      }
+      
+      .item-description {
+        p {
+          font-size: 13px;
+          color: #64748b;
+          margin: 0;
+          line-height: 1.4;
+        }
+        
+        .edit-textarea {
+          width: 100%;
+          padding: 8px 12px;
+          border: 1px solid #d1d5db;
+          border-radius: 6px;
+          font-size: 13px;
+          resize: vertical;
+          font-family: inherit;
+          
+          &:focus {
+            outline: none;
+            border-color: #4f46e5;
+            box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
+          }
+        }
+      }
+    }
+    
+    .item-actions {
+      display: flex;
+      gap: 8px;
+      margin-left: 16px;
+      
+      .action-btn {
+        width: 32px;
+        height: 32px;
+        border: none;
+        border-radius: 6px;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        transition: all 0.2s ease;
+        
+        svg {
+          width: 14px;
+          height: 14px;
+        }
+        
+        &.edit-btn {
+          background: #f0f9ff;
+          color: #0369a1;
+          
+          &:hover {
+            background: #e0f2fe;
+          }
+        }
+        
+        &.delete-btn {
+          background: #fef2f2;
+          color: #dc2626;
+          
+          &:hover {
+            background: #fee2e2;
+          }
+        }
+        
+        &.save-btn {
+          background: #f0fdf4;
+          color: #16a34a;
+          
+          &:hover {
+            background: #dcfce7;
+          }
+        }
+        
+        &.cancel-btn {
+          background: #fafafa;
+          color: #525252;
+          
+          &:hover {
+            background: #f5f5f5;
+          }
+        }
+      }
+    }
+  }
+}
+
+.quotation-summary {
+  margin-top: 20px;
+  padding: 16px;
+  background: #f8fafc;
+  border-radius: 8px;
+  border: 1px solid #e2e8f0;
+  
+  .summary-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    &.total {
+      padding-top: 8px;
+      border-top: 1px solid #e2e8f0;
+      font-weight: 600;
+      
+      .summary-value {
+        color: #059669;
+        font-size: 16px;
+      }
+    }
+    
+    .summary-label {
+      font-size: 14px;
+      color: #64748b;
+    }
+    
+    .summary-value {
+      font-size: 14px;
+      color: #1e293b;
+    }
+  }
+}
+
+// 动画效果
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .quotation-detail-container {
+    .quotation-header {
+      flex-direction: column;
+      gap: 16px;
+      align-items: stretch;
+      
+      .header-actions {
+        justify-content: flex-end;
+      }
+    }
+  }
+  
+  .quotation-items {
+    .quotation-item {
+      flex-direction: column;
+      gap: 12px;
+      
+      .item-actions {
+        margin-left: 0;
+        justify-content: flex-end;
+      }
+    }
+  }
+}

+ 212 - 0
src/app/pages/customer-service/consultation-order/components/quotation-detail/quotation-detail.component.ts

@@ -0,0 +1,212 @@
+import { Component, Input, Output, EventEmitter, signal, computed } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+export interface QuotationItem {
+  id: string;
+  roomType: string;
+  amount: number;
+  description?: string;
+}
+
+@Component({
+  selector: 'app-quotation-detail',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './quotation-detail.component.html',
+  styleUrls: ['./quotation-detail.component.scss']
+})
+export class QuotationDetailComponent {
+  @Input() quotationItems = signal<QuotationItem[]>([]);
+  @Input() decorationType?: string;
+  @Input() orderAmount?: number;
+  @Output() quotationChange = new EventEmitter<QuotationItem[]>();
+
+  isGenerating = signal(false);
+  editingItemId = signal<string | null>(null);
+  editingItem: Partial<QuotationItem> = {};
+
+  roomTypeOptions = [
+    '客厅', '餐厅', '客餐厅', '主卧', '次卧', '儿童房', 
+    '书房', '厨房', '主卫', '客卫', '阳台', '玄关', 
+    '走廊', '储物间', '衣帽间', '其他空间'
+  ];
+
+  // 计算总金额
+  totalAmount = computed(() => {
+    return this.quotationItems().reduce((sum, item) => sum + item.amount, 0);
+  });
+
+  constructor() {}
+
+  // AI智能报价生成
+  async generateAIQuotation() {
+    if (this.isGenerating()) return;
+    
+    this.isGenerating.set(true);
+    
+    try {
+      // 模拟AI生成报价(实际项目中应调用AI服务)
+      await this.simulateAIGeneration();
+      
+      const aiQuotation = this.generateDefaultQuotation();
+      this.quotationItems.set(aiQuotation);
+      this.emitChange();
+    } catch (error) {
+      console.error('AI报价生成失败:', error);
+    } finally {
+      this.isGenerating.set(false);
+    }
+  }
+
+  private async simulateAIGeneration(): Promise<void> {
+    return new Promise(resolve => setTimeout(resolve, 2000));
+  }
+
+  private generateDefaultQuotation(): QuotationItem[] {
+    const baseAmount = this.orderAmount || 100000;
+    const isHomeDecoration = this.decorationType === '家装';
+    
+    if (isHomeDecoration) {
+      return [
+        {
+          id: this.generateId(),
+          roomType: '客餐厅',
+          amount: Math.round(baseAmount * 0.35),
+          description: '包含客厅和餐厅的整体设计,含家具配置和软装搭配'
+        },
+        {
+          id: this.generateId(),
+          roomType: '主卧',
+          amount: Math.round(baseAmount * 0.25),
+          description: '主卧室设计,包含衣柜、床品和照明设计'
+        },
+        {
+          id: this.generateId(),
+          roomType: '次卧',
+          amount: Math.round(baseAmount * 0.15),
+          description: '次卧室或儿童房设计'
+        },
+        {
+          id: this.generateId(),
+          roomType: '厨房',
+          amount: Math.round(baseAmount * 0.15),
+          description: '厨房功能设计和橱柜配置'
+        },
+        {
+          id: this.generateId(),
+          roomType: '卫生间',
+          amount: Math.round(baseAmount * 0.1),
+          description: '卫生间设计和洁具配置'
+        }
+      ];
+    } else {
+      // 工装报价
+      return [
+        {
+          id: this.generateId(),
+          roomType: '办公区域',
+          amount: Math.round(baseAmount * 0.4),
+          description: '办公空间设计,包含工位和公共区域'
+        },
+        {
+          id: this.generateId(),
+          roomType: '会议室',
+          amount: Math.round(baseAmount * 0.2),
+          description: '会议室设计和设备配置'
+        },
+        {
+          id: this.generateId(),
+          roomType: '接待区',
+          amount: Math.round(baseAmount * 0.2),
+          description: '前台接待区域设计'
+        },
+        {
+          id: this.generateId(),
+          roomType: '其他区域',
+          amount: Math.round(baseAmount * 0.2),
+          description: '茶水间、休息区等其他功能区域'
+        }
+      ];
+    }
+  }
+
+  // 添加报价项目
+  addQuotationItem() {
+    const newItem: QuotationItem = {
+      id: this.generateId(),
+      roomType: '',
+      amount: 0,
+      description: ''
+    };
+    
+    const currentItems = this.quotationItems();
+    this.quotationItems.set([...currentItems, newItem]);
+    this.editItem(newItem);
+  }
+
+  // 编辑项目
+  editItem(item: QuotationItem) {
+    this.editingItemId.set(item.id);
+    this.editingItem = { ...item };
+  }
+
+  // 保存编辑
+  saveItem() {
+    const editingId = this.editingItemId();
+    if (!editingId || !this.editingItem.roomType || !this.editingItem.amount) {
+      return;
+    }
+
+    const currentItems = this.quotationItems();
+    const updatedItems = currentItems.map(item => 
+      item.id === editingId 
+        ? { ...item, ...this.editingItem } as QuotationItem
+        : item
+    );
+    
+    this.quotationItems.set(updatedItems);
+    this.cancelEdit();
+    this.emitChange();
+  }
+
+  // 取消编辑
+  cancelEdit() {
+    const editingId = this.editingItemId();
+    
+    // 如果是新添加的空项目,删除它
+    if (editingId) {
+      const currentItems = this.quotationItems();
+      const editingItem = currentItems.find(item => item.id === editingId);
+      
+      if (editingItem && !editingItem.roomType && !editingItem.amount) {
+        this.deleteItem(editingId);
+      }
+    }
+    
+    this.editingItemId.set(null);
+    this.editingItem = {};
+  }
+
+  // 删除项目
+  deleteItem(itemId: string) {
+    const currentItems = this.quotationItems();
+    const filteredItems = currentItems.filter(item => item.id !== itemId);
+    this.quotationItems.set(filteredItems);
+    
+    if (this.editingItemId() === itemId) {
+      this.editingItemId.set(null);
+      this.editingItem = {};
+    }
+    
+    this.emitChange();
+  }
+
+  private emitChange() {
+    this.quotationChange.emit(this.quotationItems());
+  }
+
+  private generateId(): string {
+    return 'quotation_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+  }
+}

+ 7 - 11
src/app/pages/customer-service/dashboard/pages/after-sales/after-sales.component.html

@@ -287,7 +287,7 @@
       </div>
       
       <!-- 功能模块标签 -->
-      <div class="detail-tabs">
+      <!-- <div class="detail-tabs">
         <button 
           class="tab-button"
           [class.active]="activeDetailTab() === 'payment'"
@@ -309,10 +309,10 @@
           <mat-icon>feedback</mat-icon>
           客户建议收集与处理
         </button>
-      </div>
+      </div> -->
       
       <!-- 尾款管理模块 -->
-      @if (activeDetailTab() === 'payment') {
+      <!-- @if (activeDetailTab() === 'payment') { -->
         <div class="payment-management">
           <div class="module-header">
             <h3>尾款管理</h3>
@@ -395,10 +395,9 @@
             </div>
           }
         </div>
-      }
       
       <!-- 客户评价处理模块 -->
-      @if (activeDetailTab() === 'review') {
+      <!-- @if (activeDetailTab() === 'review') { -->
         <div class="review-management">
           <div class="module-header">
             <h3>客户评价处理</h3>
@@ -593,10 +592,9 @@
             </div>
           }
         </div>
-      }
       
       <!-- 客户建议收集与处理模块 -->
-      @if (activeDetailTab() === 'suggestion') {
+      <!-- @if (activeDetailTab() === 'suggestion') { -->
         <div class="suggestion-management">
           <div class="module-header">
             <h3>客户建议收集与处理</h3>
@@ -768,7 +766,5 @@
             </div>
           }
         </div>
-      }
-    </div>
-  }
-</div>
+      </div>
+    }

+ 200 - 25
src/app/pages/customer-service/dashboard/pages/after-sales/after-sales.component.scss

@@ -599,12 +599,16 @@ $transition: all 0.2s ease;
     justify-content: space-between;
     align-items: center;
     margin-bottom: 20px;
+    flex-wrap: wrap;
+    gap: 12px;
 
     h3 {
       margin: 0;
       color: $text-primary;
       font-size: 18px;
       font-weight: 600;
+      flex: 1;
+      min-width: 120px;
     }
 
     .upload-button,
@@ -620,6 +624,8 @@ $transition: all 0.2s ease;
       cursor: pointer;
       transition: $transition;
       font-size: 14px;
+      white-space: nowrap;
+      flex-shrink: 0;
 
       &:hover:not(:disabled) {
         background-color: $primary-light;
@@ -637,6 +643,23 @@ $transition: all 0.2s ease;
         height: 16px;
       }
     }
+
+    // 响应式处理
+    @media (max-width: 768px) {
+      flex-direction: column;
+      align-items: flex-start;
+      
+      h3 {
+        width: 100%;
+        margin-bottom: 8px;
+      }
+      
+      .upload-button,
+      .add-review-button {
+        width: 100%;
+        justify-content: center;
+      }
+    }
   }
 
   // 尾款管理模块
@@ -762,6 +785,12 @@ $transition: all 0.2s ease;
               &:hover {
                 text-decoration: underline;
               }
+
+              @media (max-width: 768px) {
+                margin-left: 0;
+                margin-top: 8px;
+                display: flex;
+              }
             }
           }
         }
@@ -1094,70 +1123,114 @@ $transition: all 0.2s ease;
       }
 
       .review-record {
-        background: ios.$ios-card-background;
-        border: 1px solid ios.$ios-border;
+        background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
+        border: 1px solid rgba(0, 122, 255, 0.1);
         border-radius: ios.$ios-radius-lg;
         padding: ios.$ios-spacing-lg;
         margin-bottom: ios.$ios-spacing-md;
-        box-shadow: ios.$ios-shadow-sm;
-        transition: all 0.3s ease;
+        box-shadow: 
+          0 4px 20px rgba(0, 0, 0, 0.08),
+          0 1px 4px rgba(0, 0, 0, 0.04);
+        transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+        position: relative;
+        overflow: hidden;
+
+        &::before {
+          content: '';
+          position: absolute;
+          top: 0;
+          left: 0;
+          right: 0;
+          height: 3px;
+          background: linear-gradient(90deg, #007aff, #5ac8fa);
+          border-radius: ios.$ios-radius-lg ios.$ios-radius-lg 0 0;
+        }
 
         &:hover {
-          transform: translateY(-1px);
-          box-shadow: ios.$ios-shadow-md;
+          transform: translateY(-2px);
+          box-shadow: 
+            0 8px 32px rgba(0, 0, 0, 0.12),
+            0 2px 8px rgba(0, 0, 0, 0.08);
         }
 
         .review-header {
           display: flex;
           align-items: center;
           gap: ios.$ios-spacing-md;
-          margin-bottom: ios.$ios-spacing-sm;
+          margin-bottom: ios.$ios-spacing-md;
+          flex-wrap: wrap;
 
           .review-type,
           .review-channel {
             padding: ios.$ios-spacing-xs ios.$ios-spacing-sm;
-            background-color: rgba(ios.$ios-primary, 0.1);
+            background: linear-gradient(135deg, rgba(0, 122, 255, 0.1) 0%, rgba(90, 200, 250, 0.1) 100%);
             color: ios.$ios-primary;
             border-radius: ios.$ios-radius-md;
             font-size: ios.$ios-font-size-caption-1;
             font-weight: ios.$ios-font-weight-medium;
             font-family: ios.$ios-font-family;
+            border: 1px solid rgba(0, 122, 255, 0.2);
+            flex-shrink: 0;
           }
 
           .review-date {
             color: ios.$ios-text-secondary;
             font-size: ios.$ios-font-size-subhead;
             font-family: ios.$ios-font-family;
+            flex-shrink: 0;
           }
 
           .review-score {
             display: flex;
             gap: 2px;
+            align-items: center;
+            padding: ios.$ios-spacing-xs ios.$ios-spacing-sm;
+            background: rgba(255, 193, 7, 0.1);
+            border-radius: ios.$ios-radius-md;
+            border: 1px solid rgba(255, 193, 7, 0.2);
+            flex-shrink: 0;
 
             mat-icon {
               font-size: 16px;
               width: 16px;
               height: 16px;
-              color: ios.$ios-border;
+              color: #ddd;
+              transition: color 0.2s ease;
 
               &.filled {
-                color: ios.$ios-warning;
+                color: #ffc107;
+                text-shadow: 0 0 4px rgba(255, 193, 7, 0.3);
               }
             }
           }
+
+          @media (max-width: 768px) {
+            flex-direction: column;
+            align-items: flex-start;
+            gap: ios.$ios-spacing-xs;
+          }
         }
 
         .review-content {
-          background: ios.$ios-background-secondary;
+          background: rgba(248, 249, 250, 0.8);
           border-radius: ios.$ios-radius-md;
           padding: ios.$ios-spacing-md;
+          border: 1px solid rgba(0, 122, 255, 0.05);
 
           p {
             margin: ios.$ios-spacing-xs 0;
             color: ios.$ios-text-primary;
             font-size: ios.$ios-font-size-subhead;
             font-family: ios.$ios-font-family;
-            line-height: 1.5;
+            line-height: 1.6;
+
+            &:first-child {
+              margin-top: 0;
+            }
+
+            &:last-child {
+              margin-bottom: 0;
+            }
           }
 
           .review-remark,
@@ -1165,24 +1238,39 @@ $transition: all 0.2s ease;
             color: ios.$ios-text-secondary;
             font-size: ios.$ios-font-size-caption-1;
             font-family: ios.$ios-font-family;
+            padding: ios.$ios-spacing-xs 0;
+            border-top: 1px solid rgba(0, 122, 255, 0.1);
+            margin-top: ios.$ios-spacing-sm;
+
+            strong {
+              color: ios.$ios-primary;
+              font-weight: ios.$ios-font-weight-semibold;
+            }
           }
         }
 
         .follow-up-reminder {
           display: flex;
           align-items: center;
-          gap: 8px;
-          margin-top: 12px;
-          padding: 8px;
-          background-color: rgba($warning-color, 0.1);
-          border-radius: $border-radius;
-          color: $warning-color;
-          font-size: 12px;
+          gap: ios.$ios-spacing-xs;
+          margin-top: ios.$ios-spacing-md;
+          padding: ios.$ios-spacing-sm;
+          background: linear-gradient(135deg, rgba(255, 149, 0, 0.1) 0%, rgba(255, 193, 7, 0.1) 100%);
+          border-radius: ios.$ios-radius-md;
+          border: 1px solid rgba(255, 149, 0, 0.2);
 
           mat-icon {
-            font-size: 16px;
-            width: 16px;
-            height: 16px;
+            color: #ff9500;
+            font-size: 18px;
+            width: 18px;
+            height: 18px;
+          }
+
+          span {
+            color: #ff9500;
+            font-size: ios.$ios-font-size-caption-1;
+            font-weight: ios.$ios-font-weight-medium;
+            font-family: ios.$ios-font-family;
           }
         }
       }
@@ -1324,6 +1412,18 @@ $transition: all 0.2s ease;
 
         .suggestion-actions {
           margin-bottom: ios.$ios-spacing-sm;
+          display: flex;
+          gap: ios.$ios-spacing-xs;
+          flex-wrap: wrap;
+
+          @media (max-width: 768px) {
+            flex-direction: column;
+            
+            .mat-mdc-button {
+              width: 100%;
+              justify-content: center;
+            }
+          }
 
           .mat-mdc-button {
             font-family: ios.$ios-font-family;
@@ -1331,6 +1431,18 @@ $transition: all 0.2s ease;
             padding: ios.$ios-spacing-xs ios.$ios-spacing-md;
             font-weight: ios.$ios-font-weight-medium;
             font-size: ios.$ios-font-size-caption-1;
+            min-width: 100px;
+            white-space: nowrap;
+
+            @media (max-width: 768px) {
+              min-width: unset;
+              padding: ios.$ios-spacing-sm ios.$ios-spacing-md;
+            }
+
+            @media (max-width: 480px) {
+              padding: ios.$ios-spacing-xs ios.$ios-spacing-sm;
+              font-size: ios.$ios-font-size-caption-2;
+            }
 
             &.mat-stroked-button {
               border: 1px solid ios.$ios-border;
@@ -1342,6 +1454,19 @@ $transition: all 0.2s ease;
                 box-shadow: ios.$ios-shadow-sm;
               }
             }
+
+            mat-icon {
+              margin-right: ios.$ios-spacing-xs;
+              font-size: 16px;
+              width: 16px;
+              height: 16px;
+
+              @media (max-width: 480px) {
+                font-size: 14px;
+                width: 14px;
+                height: 14px;
+              }
+            }
           }
         }
 
@@ -1562,12 +1687,43 @@ $transition: all 0.2s ease;
             margin-top: ios.$ios-spacing-md;
             padding-top: ios.$ios-spacing-md;
             border-top: 1px solid ios.$ios-border;
+            flex-wrap: wrap;
+
+            @media (max-width: 768px) {
+              flex-direction: column;
+              gap: ios.$ios-spacing-xs;
+              
+              .mat-mdc-button {
+                width: 100%;
+                justify-content: center;
+              }
+            }
+
+            @media (max-width: 480px) {
+              padding: ios.$ios-spacing-sm;
+              margin-top: ios.$ios-spacing-sm;
+            }
 
             .mat-mdc-button {
               font-family: ios.$ios-font-family;
               border-radius: ios.$ios-radius-md;
               padding: ios.$ios-spacing-sm ios.$ios-spacing-lg;
               font-weight: ios.$ios-font-weight-medium;
+              min-width: 120px;
+              white-space: nowrap;
+              overflow: hidden;
+              text-overflow: ellipsis;
+
+              @media (max-width: 768px) {
+                min-width: unset;
+                padding: ios.$ios-spacing-md ios.$ios-spacing-lg;
+                font-size: ios.$ios-font-size-body;
+              }
+
+              @media (max-width: 480px) {
+                padding: ios.$ios-spacing-sm ios.$ios-spacing-md;
+                font-size: ios.$ios-font-size-caption-1;
+              }
 
               &.mat-primary {
                 background: ios.$ios-primary;
@@ -1578,6 +1734,12 @@ $transition: all 0.2s ease;
                   background: #0056CC;
                   box-shadow: ios.$ios-shadow-sm;
                 }
+
+                &:disabled {
+                  background: ios.$ios-text-tertiary;
+                  color: white;
+                  opacity: 0.6;
+                }
               }
 
               &.mat-stroked-button {
@@ -1594,6 +1756,19 @@ $transition: all 0.2s ease;
               &:active {
                 transform: translateY(1px);
               }
+
+              mat-icon {
+                margin-right: ios.$ios-spacing-xs;
+                font-size: 18px;
+                width: 18px;
+                height: 18px;
+
+                @media (max-width: 480px) {
+                  font-size: 16px;
+                  width: 16px;
+                  height: 16px;
+                }
+              }
             }
           }
         }
@@ -1630,7 +1805,7 @@ $transition: all 0.2s ease;
 
             &::before {
               content: '📋';
-              font-size: 20px;
+              font-size: ios.$ios-font-size-body;
             }
           }
 
@@ -1638,7 +1813,7 @@ $transition: all 0.2s ease;
             display: grid;
             grid-template-columns: auto auto auto 1fr;
             gap: ios.$ios-spacing-md;
-            padding: ios.$ios-spacing-sm 0;
+            padding: ios.$ios-spacing-xs 0;
             font-size: ios.$ios-font-size-caption-1;
             color: ios.$ios-text-secondary;
             font-family: ios.$ios-font-family;
@@ -1762,7 +1937,7 @@ $transition: all 0.2s ease;
     align-items: center;
     
     &:hover {
-      background-color: rgba(0, 122, 255, 0.05);
+      background-color: rgba(0, 122, 255, 0.15);
     }
   }
   

+ 2 - 0
src/app/pages/customer-service/dashboard/pages/consultation-list/consultation-list.component.scss

@@ -225,6 +225,8 @@
     -webkit-box-orient: vertical;
     overflow: hidden;
     font-weight: 400;
+    line-clamp: 2;
+    box-orient: vertical;
   }
 
   // 卡片底部操作按钮

+ 1 - 1
src/app/pages/designer/personal-board/personal-board.ts

@@ -322,7 +322,7 @@ export class PersonalBoard implements OnInit, AfterViewInit {
       '建模': 30,
       '软装': 0,
       '渲染': 50,
-      '后期': 0,
+      '后期': 20,
       '尾款结算': 0,
       '客户评价': 0,
       '投诉处理': 0

+ 290 - 0
src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.html

@@ -0,0 +1,290 @@
+<div class="designer-assignment-container">
+  <div class="section-header">
+    <h3>设计师分配</h3>
+    <div class="assignment-summary">
+      @if (selectedTeamId) {
+        <span class="team-info">主要团队:{{ getSelectedTeam()?.name }}</span>
+        @if (assignmentData.crossTeamCollaborators.length > 0) {
+          <span class="cross-team-info">跨组合作:{{ assignmentData.crossTeamCollaborators.length }}人</span>
+        }
+      }
+    </div>
+  </div>
+
+  <!-- 项目组选择 -->
+  <div class="team-selection-section">
+    <h4>选择主要项目组</h4>
+    <div class="team-grid">
+      @for (team of projectTeams; track team.id) {
+        <div 
+          class="team-card" 
+          [class.selected]="selectedTeamId === team.id"
+          (click)="selectPrimaryTeam(team.id)"
+        >
+          <div class="team-header">
+            <h5>{{ team.name }}</h5>
+            <span class="team-leader">组长:{{ team.leaderName }}</span>
+          </div>
+          <div class="team-stats">
+            <div class="stat-item">
+              <span class="stat-label">成员数</span>
+              <span class="stat-value">{{ team.members.length }}</span>
+            </div>
+            <div class="stat-item">
+              <span class="stat-label">空闲人数</span>
+              <span class="stat-value idle">{{ getIdleCount(team) }}</span>
+            </div>
+          </div>
+        </div>
+      }
+    </div>
+  </div>
+
+  <!-- 自动分配建议 -->
+  @if (selectedTeamId && getAutoAssignmentSuggestion()) {
+    <div class="auto-suggestion">
+      <div class="suggestion-header">
+        <span class="suggestion-icon">💡</span>
+        <span>分配建议</span>
+      </div>
+      <p>{{ getAutoAssignmentSuggestion() }}</p>
+    </div>
+  }
+
+  <!-- 报价项目分配 -->
+  @if (selectedTeamId && quotationItems.length > 0) {
+    <div class="quotation-assignment-section">
+      <h4>报价项目分配</h4>
+      <div class="assignment-grid">
+        @for (assignment of assignmentData.quotationAssignments; track assignment.quotationItemId) {
+          <div class="assignment-card">
+            <div class="assignment-header">
+              <h5>{{ assignment.quotationItemName }}</h5>
+              <div class="assigned-count">
+                已分配:{{ assignment.assignedDesigners.length }}人
+              </div>
+            </div>
+            
+            <div class="designer-selection">
+              <div class="available-designers">
+                <h6>团队成员</h6>
+                <div class="designer-list">
+                  @for (designer of getSelectedTeam()?.members; track designer.id) {
+                    <div 
+                      class="designer-item"
+                      [class.assigned]="assignment.assignedDesigners.includes(designer.id)"
+                      (click)="onDesignerClick(designer)"
+                    >
+                      <div class="designer-avatar">
+                        @if (designer.avatar) {
+                          <img [src]="designer.avatar" [alt]="designer.name" />
+                        } @else {
+                          <div class="avatar-placeholder">{{ designer.name.charAt(0) }}</div>
+                        }
+                      </div>
+                      
+                      <div class="designer-info">
+                        <div class="designer-name">
+                          {{ designer.name }}
+                          @if (designer.isTeamLeader) {
+                            <span class="leader-badge">组长</span>
+                          }
+                        </div>
+                        <div class="designer-status">
+                          <span 
+                            class="status-dot" 
+                            [style.background-color]="getDesignerStatusColor(designer.status)"
+                          ></span>
+                          <span class="status-text">{{ getDesignerStatusText(designer.status) }}</span>
+                          @if (designer.idleDays > 0) {
+                            <span class="idle-days">{{ designer.idleDays }}天未接单</span>
+                          }
+                        </div>
+                        <div class="workload-bar">
+                          <div class="workload-fill" [style.width.%]="designer.workload"></div>
+                          <span class="workload-text">{{ designer.workload }}%</span>
+                        </div>
+                      </div>
+                      
+                      <div class="assignment-actions">
+                        @if (assignment.assignedDesigners.includes(designer.id)) {
+                          <button 
+                            class="remove-btn"
+                            (click)="removeDesignerFromQuotation(assignment.quotationItemId, designer.id); $event.stopPropagation()"
+                          >
+                            移除
+                          </button>
+                        } @else {
+                          <button 
+                            class="assign-btn"
+                            (click)="assignDesignerToQuotation(assignment.quotationItemId, designer.id); $event.stopPropagation()"
+                          >
+                            分配
+                          </button>
+                        }
+                      </div>
+                    </div>
+                  }
+                </div>
+              </div>
+            </div>
+          </div>
+        }
+      </div>
+    </div>
+  }
+
+  <!-- 跨组合作 -->
+  @if (selectedTeamId) {
+    <div class="cross-team-section">
+      <div class="section-title">
+        <h4>跨组合作</h4>
+        <button 
+          class="toggle-btn"
+          (click)="showCrossTeamSelection = !showCrossTeamSelection"
+        >
+          @if (showCrossTeamSelection) {
+            <span>收起</span>
+          } @else {
+            <span>添加跨组合作</span>
+          }
+        </button>
+      </div>
+
+      <!-- 已选择的跨组合作设计师 -->
+      @if (assignmentData.crossTeamCollaborators.length > 0) {
+        <div class="selected-collaborators">
+          <h5>已选择的跨组合作设计师</h5>
+          <div class="collaborator-list">
+            @for (designerId of assignmentData.crossTeamCollaborators; track designerId) {
+              <div class="collaborator-item">
+                @if (getDesignerById(designerId); as designer) {
+                  <div class="designer-info">
+                    <span class="designer-name">{{ designer.name }}</span>
+                    <span class="team-name">{{ designer.teamName }}</span>
+                  </div>
+                  <button 
+                    class="remove-collaborator-btn"
+                    (click)="removeCrossTeamCollaborator(designerId)"
+                  >
+                    ×
+                  </button>
+                }
+              </div>
+            }
+          </div>
+        </div>
+      }
+
+      <!-- 跨组设计师选择 -->
+      @if (showCrossTeamSelection) {
+        <div class="cross-team-selection">
+          <h5>选择其他组设计师</h5>
+          <div class="cross-team-grid">
+            @for (team of projectTeams; track team.id) {
+              @if (team.id !== selectedTeamId) {
+                <div class="cross-team-group">
+                  <h6>{{ team.name }}</h6>
+                  <div class="designer-list">
+                    @for (designer of team.members; track designer.id) {
+                      <div 
+                        class="designer-item"
+                        [class.selected]="assignmentData.crossTeamCollaborators.includes(designer.id)"
+                        (click)="onDesignerClick(designer)"
+                      >
+                        <div class="designer-info">
+                          <span class="designer-name">{{ designer.name }}</span>
+                          <div class="designer-status">
+                            <span 
+                              class="status-dot" 
+                              [style.background-color]="getDesignerStatusColor(designer.status)"
+                            ></span>
+                            <span class="status-text">{{ getDesignerStatusText(designer.status) }}</span>
+                          </div>
+                        </div>
+                        <div class="selection-actions">
+                          @if (assignmentData.crossTeamCollaborators.includes(designer.id)) {
+                            <button 
+                              class="remove-btn"
+                              (click)="removeCrossTeamCollaborator(designer.id); $event.stopPropagation()"
+                            >
+                              移除
+                            </button>
+                          } @else {
+                            <button 
+                              class="add-btn"
+                              (click)="addCrossTeamCollaborator(designer.id); $event.stopPropagation()"
+                            >
+                              添加
+                            </button>
+                          }
+                        </div>
+                      </div>
+                    }
+                  </div>
+                </div>
+              }
+            }
+          </div>
+        </div>
+      }
+    </div>
+  }
+
+  <!-- 备注 -->
+  <div class="notes-section">
+    <label for="assignmentNotes">分配备注</label>
+    <textarea
+      id="assignmentNotes"
+      [(ngModel)]="assignmentData.notes"
+      (ngModelChange)="emitAssignmentChange()"
+      placeholder="请输入分配相关的备注信息..."
+      class="notes-textarea"
+      rows="3"
+    ></textarea>
+  </div>
+
+  <!-- 分配总结 -->
+  <div class="assignment-summary-section">
+    <h4>分配总结</h4>
+    <div class="summary-content">
+      @if (selectedTeamId) {
+        <div class="summary-item">
+          <span class="label">主要团队:</span>
+          <span class="value">{{ getSelectedTeam()?.name }}</span>
+        </div>
+      }
+      
+      @if (assignmentData.quotationAssignments.length > 0) {
+        <div class="summary-item">
+          <span class="label">项目分配:</span>
+          <div class="assignment-list">
+            @for (assignment of assignmentData.quotationAssignments; track assignment.quotationItemId) {
+              @if (assignment.assignedDesigners.length > 0) {
+                <div class="assignment-summary-item">
+                  <span class="project-name">{{ assignment.quotationItemName }}</span>
+                  <span class="assigned-designers">
+                    @for (designerId of assignment.assignedDesigners; track designerId) {
+                      <span class="designer-tag">{{ getDesignerById(designerId)?.name }}</span>
+                    }
+                  </span>
+                </div>
+              }
+            }
+          </div>
+        </div>
+      }
+      
+      @if (assignmentData.crossTeamCollaborators.length > 0) {
+        <div class="summary-item">
+          <span class="label">跨组合作:</span>
+          <div class="collaborator-tags">
+            @for (designerId of assignmentData.crossTeamCollaborators; track designerId) {
+              <span class="collaborator-tag">{{ getDesignerById(designerId)?.name }}</span>
+            }
+          </div>
+        </div>
+      }
+    </div>
+  </div>
+</div>

+ 711 - 0
src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.scss

@@ -0,0 +1,711 @@
+.designer-assignment-container {
+  background: #ffffff;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  margin-bottom: 24px;
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 24px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #e8e8e8;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #333333;
+    }
+
+    .assignment-summary {
+      display: flex;
+      gap: 16px;
+      font-size: 14px;
+
+      .team-info {
+        color: #1890ff;
+        font-weight: 500;
+      }
+
+      .cross-team-info {
+        color: #722ed1;
+        font-weight: 500;
+      }
+    }
+  }
+
+  .team-selection-section {
+    margin-bottom: 32px;
+
+    h4 {
+      margin: 0 0 16px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #333333;
+    }
+
+    .team-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+      gap: 16px;
+
+      .team-card {
+        border: 2px solid #e8e8e8;
+        border-radius: 12px;
+        padding: 20px;
+        cursor: pointer;
+        transition: all 0.3s ease;
+        background: #ffffff;
+
+        &:hover {
+          border-color: #1890ff;
+          box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
+        }
+
+        &.selected {
+          border-color: #1890ff;
+          background: #f0f8ff;
+          box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
+        }
+
+        .team-header {
+          margin-bottom: 16px;
+
+          h5 {
+            margin: 0 0 8px 0;
+            font-size: 16px;
+            font-weight: 600;
+            color: #333333;
+          }
+
+          .team-leader {
+            font-size: 14px;
+            color: #666666;
+          }
+        }
+
+        .team-stats {
+          display: flex;
+          gap: 24px;
+
+          .stat-item {
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
+
+            .stat-label {
+              font-size: 12px;
+              color: #999999;
+            }
+
+            .stat-value {
+              font-size: 18px;
+              font-weight: 600;
+              color: #333333;
+
+              &.idle {
+                color: #52c41a;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .auto-suggestion {
+    background: #fffbe6;
+    border: 1px solid #ffe58f;
+    border-radius: 8px;
+    padding: 16px;
+    margin-bottom: 24px;
+
+    .suggestion-header {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin-bottom: 8px;
+      font-weight: 600;
+      color: #d48806;
+
+      .suggestion-icon {
+        font-size: 16px;
+      }
+    }
+
+    p {
+      margin: 0;
+      font-size: 14px;
+      color: #d48806;
+      line-height: 1.5;
+    }
+  }
+
+  .quotation-assignment-section {
+    margin-bottom: 32px;
+
+    h4 {
+      margin: 0 0 20px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #333333;
+    }
+
+    .assignment-grid {
+      display: flex;
+      flex-direction: column;
+      gap: 20px;
+
+      .assignment-card {
+        border: 1px solid #e8e8e8;
+        border-radius: 12px;
+        padding: 20px;
+        background: #fafafa;
+
+        .assignment-header {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-bottom: 16px;
+
+          h5 {
+            margin: 0;
+            font-size: 16px;
+            font-weight: 600;
+            color: #333333;
+          }
+
+          .assigned-count {
+            font-size: 14px;
+            color: #1890ff;
+            font-weight: 500;
+          }
+        }
+
+        .designer-selection {
+          .available-designers {
+            h6 {
+              margin: 0 0 12px 0;
+              font-size: 14px;
+              font-weight: 600;
+              color: #666666;
+            }
+
+            .designer-list {
+              display: flex;
+              flex-direction: column;
+              gap: 12px;
+
+              .designer-item {
+                display: flex;
+                align-items: center;
+                gap: 16px;
+                padding: 16px;
+                border: 1px solid #e8e8e8;
+                border-radius: 8px;
+                background: #ffffff;
+                cursor: pointer;
+                transition: all 0.3s ease;
+
+                &:hover {
+                  border-color: #1890ff;
+                  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
+                }
+
+                &.assigned {
+                  border-color: #52c41a;
+                  background: #f6ffed;
+                }
+
+                .designer-avatar {
+                  width: 48px;
+                  height: 48px;
+                  border-radius: 50%;
+                  overflow: hidden;
+                  flex-shrink: 0;
+
+                  img {
+                    width: 100%;
+                    height: 100%;
+                    object-fit: cover;
+                  }
+
+                  .avatar-placeholder {
+                    width: 100%;
+                    height: 100%;
+                    background: #1890ff;
+                    color: white;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 18px;
+                    font-weight: 600;
+                  }
+                }
+
+                .designer-info {
+                  flex: 1;
+
+                  .designer-name {
+                    display: flex;
+                    align-items: center;
+                    gap: 8px;
+                    margin-bottom: 8px;
+                    font-size: 16px;
+                    font-weight: 600;
+                    color: #333333;
+
+                    .leader-badge {
+                      padding: 2px 8px;
+                      background: #faad14;
+                      color: white;
+                      border-radius: 12px;
+                      font-size: 10px;
+                    }
+                  }
+
+                  .designer-status {
+                    display: flex;
+                    align-items: center;
+                    gap: 8px;
+                    margin-bottom: 8px;
+                    font-size: 14px;
+
+                    .status-dot {
+                      width: 8px;
+                      height: 8px;
+                      border-radius: 50%;
+                    }
+
+                    .status-text {
+                      color: #666666;
+                    }
+
+                    .idle-days {
+                      color: #ff4d4f;
+                      font-size: 12px;
+                    }
+                  }
+
+                  .workload-bar {
+                    position: relative;
+                    width: 100%;
+                    height: 6px;
+                    background: #f0f0f0;
+                    border-radius: 3px;
+                    overflow: hidden;
+
+                    .workload-fill {
+                      height: 100%;
+                      background: linear-gradient(90deg, #52c41a 0%, #faad14 50%, #ff4d4f 100%);
+                      transition: width 0.3s ease;
+                    }
+
+                    .workload-text {
+                      position: absolute;
+                      right: 0;
+                      top: -20px;
+                      font-size: 12px;
+                      color: #666666;
+                    }
+                  }
+                }
+
+                .assignment-actions {
+                  .assign-btn, .remove-btn {
+                    padding: 6px 12px;
+                    border: none;
+                    border-radius: 6px;
+                    font-size: 12px;
+                    cursor: pointer;
+                    transition: all 0.3s ease;
+                  }
+
+                  .assign-btn {
+                    background: #1890ff;
+                    color: white;
+
+                    &:hover {
+                      background: #40a9ff;
+                    }
+                  }
+
+                  .remove-btn {
+                    background: #ff4d4f;
+                    color: white;
+
+                    &:hover {
+                      background: #ff7875;
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .cross-team-section {
+    margin-bottom: 32px;
+
+    .section-title {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+
+      h4 {
+        margin: 0;
+        font-size: 16px;
+        font-weight: 600;
+        color: #333333;
+      }
+
+      .toggle-btn {
+        padding: 8px 16px;
+        background: #722ed1;
+        color: white;
+        border: none;
+        border-radius: 6px;
+        font-size: 14px;
+        cursor: pointer;
+        transition: all 0.3s ease;
+
+        &:hover {
+          background: #9254de;
+        }
+      }
+    }
+
+    .selected-collaborators {
+      margin-bottom: 20px;
+
+      h5 {
+        margin: 0 0 12px 0;
+        font-size: 14px;
+        font-weight: 600;
+        color: #666666;
+      }
+
+      .collaborator-list {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 12px;
+
+        .collaborator-item {
+          display: flex;
+          align-items: center;
+          gap: 12px;
+          padding: 12px 16px;
+          background: #f9f0ff;
+          border: 1px solid #d3adf7;
+          border-radius: 8px;
+
+          .designer-info {
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
+
+            .designer-name {
+              font-size: 14px;
+              font-weight: 600;
+              color: #333333;
+            }
+
+            .team-name {
+              font-size: 12px;
+              color: #722ed1;
+            }
+          }
+
+          .remove-collaborator-btn {
+            width: 20px;
+            height: 20px;
+            border: none;
+            background: #ff4d4f;
+            color: white;
+            border-radius: 50%;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 14px;
+
+            &:hover {
+              background: #ff7875;
+            }
+          }
+        }
+      }
+    }
+
+    .cross-team-selection {
+      .cross-team-grid {
+        display: flex;
+        flex-direction: column;
+        gap: 20px;
+
+        .cross-team-group {
+          h6 {
+            margin: 0 0 12px 0;
+            font-size: 14px;
+            font-weight: 600;
+            color: #722ed1;
+          }
+
+          .designer-list {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+            gap: 12px;
+
+            .designer-item {
+              display: flex;
+              align-items: center;
+              justify-content: space-between;
+              padding: 12px 16px;
+              border: 1px solid #e8e8e8;
+              border-radius: 8px;
+              background: #ffffff;
+              cursor: pointer;
+              transition: all 0.3s ease;
+
+              &:hover {
+                border-color: #722ed1;
+                box-shadow: 0 2px 8px rgba(114, 46, 209, 0.1);
+              }
+
+              &.selected {
+                border-color: #722ed1;
+                background: #f9f0ff;
+              }
+
+              .designer-info {
+                .designer-name {
+                  font-size: 14px;
+                  font-weight: 600;
+                  color: #333333;
+                  margin-bottom: 4px;
+                }
+
+                .designer-status {
+                  display: flex;
+                  align-items: center;
+                  gap: 6px;
+                  font-size: 12px;
+
+                  .status-dot {
+                    width: 6px;
+                    height: 6px;
+                    border-radius: 50%;
+                  }
+
+                  .status-text {
+                    color: #666666;
+                  }
+                }
+              }
+
+              .selection-actions {
+                .add-btn, .remove-btn {
+                  padding: 4px 8px;
+                  border: none;
+                  border-radius: 4px;
+                  font-size: 12px;
+                  cursor: pointer;
+                  transition: all 0.3s ease;
+                }
+
+                .add-btn {
+                  background: #722ed1;
+                  color: white;
+
+                  &:hover {
+                    background: #9254de;
+                  }
+                }
+
+                .remove-btn {
+                  background: #ff4d4f;
+                  color: white;
+
+                  &:hover {
+                    background: #ff7875;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .notes-section {
+    margin-bottom: 32px;
+
+    label {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: 600;
+      color: #333333;
+    }
+
+    .notes-textarea {
+      width: 100%;
+      padding: 12px 16px;
+      border: 1px solid #d9d9d9;
+      border-radius: 8px;
+      font-size: 14px;
+      resize: vertical;
+      font-family: inherit;
+      transition: all 0.3s ease;
+
+      &:focus {
+        outline: none;
+        border-color: #1890ff;
+        box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+      }
+    }
+  }
+
+  .assignment-summary-section {
+    border-top: 2px solid #e8e8e8;
+    padding-top: 20px;
+
+    h4 {
+      margin: 0 0 16px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #333333;
+    }
+
+    .summary-content {
+      .summary-item {
+        display: flex;
+        align-items: flex-start;
+        gap: 12px;
+        margin-bottom: 16px;
+
+        .label {
+          font-size: 14px;
+          font-weight: 600;
+          color: #666666;
+          min-width: 80px;
+        }
+
+        .value {
+          font-size: 14px;
+          color: #333333;
+        }
+
+        .assignment-list {
+          display: flex;
+          flex-direction: column;
+          gap: 8px;
+
+          .assignment-summary-item {
+            display: flex;
+            align-items: center;
+            gap: 12px;
+
+            .project-name {
+              font-size: 14px;
+              font-weight: 500;
+              color: #333333;
+            }
+
+            .assigned-designers {
+              display: flex;
+              gap: 6px;
+
+              .designer-tag {
+                padding: 2px 8px;
+                background: #e6f7ff;
+                color: #1890ff;
+                border-radius: 12px;
+                font-size: 12px;
+              }
+            }
+          }
+        }
+
+        .collaborator-tags {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 6px;
+
+          .collaborator-tag {
+            padding: 2px 8px;
+            background: #f9f0ff;
+            color: #722ed1;
+            border-radius: 12px;
+            font-size: 12px;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .designer-assignment-container {
+    padding: 16px;
+
+    .section-header {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+    }
+
+    .team-selection-section {
+      .team-grid {
+        grid-template-columns: 1fr;
+      }
+    }
+
+    .quotation-assignment-section {
+      .assignment-grid {
+        .assignment-card {
+          .designer-selection {
+            .available-designers {
+              .designer-list {
+                .designer-item {
+                  flex-direction: column;
+                  align-items: flex-start;
+                  gap: 12px;
+
+                  .designer-info {
+                    width: 100%;
+                  }
+
+                  .assignment-actions {
+                    align-self: flex-end;
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    .cross-team-section {
+      .cross-team-selection {
+        .cross-team-grid {
+          .cross-team-group {
+            .designer-list {
+              grid-template-columns: 1fr;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 315 - 0
src/app/pages/designer/project-detail/components/designer-assignment/designer-assignment.component.ts

@@ -0,0 +1,315 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+export interface Designer {
+  id: string;
+  name: string;
+  avatar?: string;
+  teamId: string;
+  teamName: string;
+  isTeamLeader: boolean;
+  status: 'idle' | 'busy' | 'reviewing';
+  idleDays: number;
+  recentOrders: number;
+  lastOrderDate?: string;
+  reviewDates: string[]; // 对图日期,这些日期不能安排其他工作
+  workload: number; // 当前工作量 (0-100)
+  skills: string[]; // 技能标签
+}
+
+export interface ProjectTeam {
+  id: string;
+  name: string;
+  leaderId: string;
+  leaderName: string;
+  members: Designer[];
+}
+
+export interface QuotationAssignment {
+  quotationItemId: string;
+  quotationItemName: string;
+  assignedDesigners: string[];
+  estimatedHours?: number;
+}
+
+export interface DesignerAssignmentData {
+  primaryTeamId: string;
+  quotationAssignments: QuotationAssignment[];
+  crossTeamCollaborators: string[]; // 跨组合作的设计师ID
+  notes?: string;
+}
+
+@Component({
+  selector: 'app-designer-assignment',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './designer-assignment.component.html',
+  styleUrls: ['./designer-assignment.component.scss']
+})
+export class DesignerAssignmentComponent implements OnInit {
+  @Input() quotationItems: any[] = [];
+  @Input() initialAssignment?: DesignerAssignmentData;
+  @Output() assignmentChange = new EventEmitter<DesignerAssignmentData>();
+  @Output() designerClick = new EventEmitter<Designer>();
+
+  // 模拟数据 - 实际项目中应该从服务获取
+  projectTeams: ProjectTeam[] = [
+    {
+      id: 'team-1',
+      name: '家装设计组',
+      leaderId: 'designer-1',
+      leaderName: '张组长',
+      members: [
+        {
+          id: 'designer-1',
+          name: '张组长',
+          teamId: 'team-1',
+          teamName: '家装设计组',
+          isTeamLeader: true,
+          status: 'busy',
+          idleDays: 0,
+          recentOrders: 3,
+          lastOrderDate: '2024-01-15',
+          reviewDates: ['2024-01-20', '2024-01-25'],
+          workload: 85,
+          skills: ['家装设计', '软装搭配', '项目管理']
+        },
+        {
+          id: 'designer-2',
+          name: '李设计师',
+          teamId: 'team-1',
+          teamName: '家装设计组',
+          isTeamLeader: false,
+          status: 'idle',
+          idleDays: 5,
+          recentOrders: 1,
+          lastOrderDate: '2024-01-10',
+          reviewDates: [],
+          workload: 30,
+          skills: ['家装设计', '3D建模']
+        }
+      ]
+    },
+    {
+      id: 'team-2',
+      name: '工装设计组',
+      leaderId: 'designer-3',
+      leaderName: '王组长',
+      members: [
+        {
+          id: 'designer-3',
+          name: '王组长',
+          teamId: 'team-2',
+          teamName: '工装设计组',
+          isTeamLeader: true,
+          status: 'reviewing',
+          idleDays: 0,
+          recentOrders: 2,
+          lastOrderDate: '2024-01-14',
+          reviewDates: ['2024-01-18', '2024-01-22'],
+          workload: 70,
+          skills: ['工装设计', '商业空间', '项目管理']
+        },
+        {
+          id: 'designer-4',
+          name: '赵设计师',
+          teamId: 'team-2',
+          teamName: '工装设计组',
+          isTeamLeader: false,
+          status: 'idle',
+          idleDays: 12,
+          recentOrders: 0,
+          lastOrderDate: '2024-01-03',
+          reviewDates: [],
+          workload: 10,
+          skills: ['工装设计', '效果图制作']
+        }
+      ]
+    }
+  ];
+
+  assignmentData: DesignerAssignmentData = {
+    primaryTeamId: '',
+    quotationAssignments: [],
+    crossTeamCollaborators: [],
+    notes: ''
+  };
+
+  selectedTeamId = '';
+  showCrossTeamSelection = false;
+  availableCrossTeamDesigners: Designer[] = [];
+
+  constructor() {}
+
+  ngOnInit() {
+    if (this.initialAssignment) {
+      this.assignmentData = { ...this.initialAssignment };
+      this.selectedTeamId = this.assignmentData.primaryTeamId;
+    }
+
+    // 初始化报价分配
+    this.initializeQuotationAssignments();
+  }
+
+  // 初始化报价分配
+  initializeQuotationAssignments() {
+    if (this.quotationItems.length > 0 && this.assignmentData.quotationAssignments.length === 0) {
+      this.assignmentData.quotationAssignments = this.quotationItems.map(item => ({
+        quotationItemId: item.id,
+        quotationItemName: item.name,
+        assignedDesigners: [],
+        estimatedHours: 0
+      }));
+    }
+  }
+
+  // 选择主要项目组
+  selectPrimaryTeam(teamId: string) {
+    this.selectedTeamId = teamId;
+    this.assignmentData.primaryTeamId = teamId;
+    
+    // 清空之前的分配
+    this.assignmentData.quotationAssignments.forEach(assignment => {
+      assignment.assignedDesigners = [];
+    });
+    
+    this.updateAvailableCrossTeamDesigners();
+    this.emitAssignmentChange();
+  }
+
+  // 获取选中的项目组
+  getSelectedTeam(): ProjectTeam | undefined {
+    return this.projectTeams.find(team => team.id === this.selectedTeamId);
+  }
+
+  // 分配设计师到报价项目
+  assignDesignerToQuotation(quotationId: string, designerId: string) {
+    const assignment = this.assignmentData.quotationAssignments.find(a => a.quotationItemId === quotationId);
+    if (assignment) {
+      if (!assignment.assignedDesigners.includes(designerId)) {
+        assignment.assignedDesigners.push(designerId);
+        this.emitAssignmentChange();
+      }
+    }
+  }
+
+  // 移除设计师分配
+  removeDesignerFromQuotation(quotationId: string, designerId: string) {
+    const assignment = this.assignmentData.quotationAssignments.find(a => a.quotationItemId === quotationId);
+    if (assignment) {
+      assignment.assignedDesigners = assignment.assignedDesigners.filter(id => id !== designerId);
+      this.emitAssignmentChange();
+    }
+  }
+
+  // 更新可用的跨组合作设计师
+  updateAvailableCrossTeamDesigners() {
+    this.availableCrossTeamDesigners = [];
+    this.projectTeams.forEach(team => {
+      if (team.id !== this.selectedTeamId) {
+        this.availableCrossTeamDesigners.push(...team.members);
+      }
+    });
+  }
+
+  // 添加跨组合作设计师
+  addCrossTeamCollaborator(designerId: string) {
+    if (!this.assignmentData.crossTeamCollaborators.includes(designerId)) {
+      this.assignmentData.crossTeamCollaborators.push(designerId);
+      this.emitAssignmentChange();
+    }
+  }
+
+  // 移除跨组合作设计师
+  removeCrossTeamCollaborator(designerId: string) {
+    this.assignmentData.crossTeamCollaborators = this.assignmentData.crossTeamCollaborators.filter(id => id !== designerId);
+    this.emitAssignmentChange();
+  }
+
+  // 获取设计师信息
+  getDesignerById(designerId: string): Designer | undefined {
+    for (const team of this.projectTeams) {
+      const designer = team.members.find(d => d.id === designerId);
+      if (designer) return designer;
+    }
+    return undefined;
+  }
+
+  // 获取设计师状态颜色
+  getDesignerStatusColor(status: string): string {
+    switch (status) {
+      case 'idle': return '#52c41a';
+      case 'busy': return '#faad14';
+      case 'reviewing': return '#1890ff';
+      default: return '#d9d9d9';
+    }
+  }
+
+  // 获取设计师状态文本
+  getDesignerStatusText(status: string): string {
+    switch (status) {
+      case 'idle': return '空闲';
+      case 'busy': return '忙碌';
+      case 'reviewing': return '对图中';
+      default: return '未知';
+    }
+  }
+
+  // 获取工作量状态
+  getWorkloadStatus(workload: number): 'low' | 'medium' | 'high' {
+    if (workload < 30) return 'low';
+    if (workload < 70) return 'medium';
+    return 'high';
+  }
+
+  // 点击设计师
+  onDesignerClick(designer: Designer) {
+    this.designerClick.emit(designer);
+  }
+
+  // 发送分配变化事件
+  emitAssignmentChange() {
+    this.assignmentChange.emit({ ...this.assignmentData });
+  }
+
+  // 自动分配建议
+  getAutoAssignmentSuggestion(): string {
+    const selectedTeam = this.getSelectedTeam();
+    if (!selectedTeam) return '';
+
+    const idleDesigners = selectedTeam.members.filter(d => d.status === 'idle' && d.workload < 50);
+    const busyDesigners = selectedTeam.members.filter(d => d.workload >= 70);
+
+    let suggestion = '';
+    if (idleDesigners.length > 0) {
+      suggestion += `建议优先分配给空闲设计师:${idleDesigners.map(d => d.name).join('、')}。`;
+    }
+    if (busyDesigners.length > 0) {
+      suggestion += `注意:${busyDesigners.map(d => d.name).join('、')} 工作量较重。`;
+    }
+
+    return suggestion;
+  }
+
+  // 检查是否有冲突的对图日期
+  hasReviewDateConflict(designerId: string, projectStartDate?: string): boolean {
+    const designer = this.getDesignerById(designerId);
+    if (!designer || !projectStartDate) return false;
+
+    // 简单的日期冲突检查逻辑
+    const startDate = new Date(projectStartDate);
+    const endDate = new Date(startDate.getTime() + 30 * 24 * 60 * 60 * 1000); // 假设项目周期30天
+
+    return designer.reviewDates.some(reviewDate => {
+      const review = new Date(reviewDate);
+      return review >= startDate && review <= endDate;
+    });
+  }
+
+  // 获取团队空闲人数的安全方法
+  getIdleCount(team: ProjectTeam): number {
+    if (!team || !team.members) return 0;
+    return team.members.filter(m => m && m.status === 'idle').length;
+  }
+}

+ 121 - 0
src/app/pages/designer/project-detail/components/designer-calendar/designer-calendar.component.html

@@ -0,0 +1,121 @@
+<div class="designer-calendar-overlay" *ngIf="visible" (click)="onClose()">
+  <div class="designer-calendar-modal" (click)="$event.stopPropagation()" *ngIf="calendarData">
+    <!-- 模态框头部 -->
+    <div class="modal-header">
+      <h3>{{ calendarData.designerName }} - 工作日历</h3>
+      <button class="close-btn" (click)="onClose()">
+        <i class="icon-close">×</i>
+      </button>
+    </div>
+
+    <!-- 设计师统计信息 -->
+    <div class="designer-stats">
+      <div class="stats-grid">
+        <div class="stat-item">
+          <span class="stat-label">工作状态</span>
+          <span class="stat-value" [style.color]="getStatusColor(calendarData.stats)">
+            {{ getStatusText(calendarData.stats) }}
+          </span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-label">空闲天数</span>
+          <span class="stat-value">{{ calendarData.stats.idleDays }}天</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-label">最近接单</span>
+          <span class="stat-value">{{ calendarData.stats.recentOrders }}单</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-label">总订单数</span>
+          <span class="stat-value">{{ calendarData.stats.totalOrders }}单</span>
+        </div>
+        <div class="stat-item" *ngIf="calendarData.stats.stagnantProjects > 0">
+          <span class="stat-label">停滞项目</span>
+          <span class="stat-value warning">{{ calendarData.stats.stagnantProjects }}个</span>
+        </div>
+        <div class="stat-item" *ngIf="calendarData.stats.lastOrderDate">
+          <span class="stat-label">最后接单</span>
+          <span class="stat-value">{{ calendarData.stats.lastOrderDate | date:'MM-dd' }}</span>
+        </div>
+      </div>
+      
+      <!-- 工作负荷指示器 -->
+      <div class="workload-indicator">
+        <span class="workload-label">工作负荷</span>
+        <div class="workload-bar">
+          <div class="workload-fill" 
+               [style.width.%]="getWorkloadPercentage(calendarData.stats)"
+               [class.high]="getWorkloadPercentage(calendarData.stats) > 80"
+               [class.medium]="getWorkloadPercentage(calendarData.stats) > 50 && getWorkloadPercentage(calendarData.stats) <= 80"
+               [class.low]="getWorkloadPercentage(calendarData.stats) <= 50">
+          </div>
+        </div>
+        <span class="workload-percentage">{{ getWorkloadPercentage(calendarData.stats) | number:'1.0-0' }}%</span>
+      </div>
+    </div>
+
+    <!-- 日历导航 -->
+    <div class="calendar-nav">
+      <button class="nav-btn" (click)="previousMonth()">
+        <i class="icon-prev">‹</i>
+      </button>
+      <h4 class="month-title">{{ formatDate(calendarData.currentMonth) }}</h4>
+      <button class="nav-btn" (click)="nextMonth()">
+        <i class="icon-next">›</i>
+      </button>
+    </div>
+
+    <!-- 日历主体 -->
+    <div class="calendar-container">
+      <!-- 星期标题 -->
+      <div class="calendar-header">
+        <div class="weekday" *ngFor="let day of weekDays">{{ day }}</div>
+      </div>
+
+      <!-- 日历网格 -->
+      <div class="calendar-grid">
+        <div *ngFor="let day of calendarData.calendar" 
+             [class]="getDayClass(day)"
+             [title]="day.tooltip"
+             (click)="onDayClick(day)">
+          <span class="day-number">{{ day.date.getDate() }}</span>
+          <div class="day-indicators" *ngIf="day.date.getMonth() === currentMonth.getMonth()">
+            <span class="order-count" *ngIf="day.orderCount > 0">{{ day.orderCount }}</span>
+            <span class="review-indicator" *ngIf="day.isReviewDay">对图</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 图例说明 -->
+    <div class="calendar-legend">
+      <div class="legend-item">
+        <span class="legend-color idle"></span>
+        <span class="legend-text">空闲可分配</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color busy"></span>
+        <span class="legend-text">有订单</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color review"></span>
+        <span class="legend-text">对图日期</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color today"></span>
+        <span class="legend-text">今天</span>
+      </div>
+    </div>
+
+    <!-- 操作提示 -->
+    <div class="calendar-tips">
+      <p><strong>提示:</strong></p>
+      <ul>
+        <li>绿色日期表示空闲,可以分配新订单</li>
+        <li>蓝色日期表示有订单,但仍可分配(不超过2单)</li>
+        <li>红色日期表示对图日期,完全不能分配其他工作</li>
+        <li>点击空闲日期可以快速分配订单</li>
+      </ul>
+    </div>
+  </div>
+</div>

+ 450 - 0
src/app/pages/designer/project-detail/components/designer-calendar/designer-calendar.component.scss

@@ -0,0 +1,450 @@
+.designer-calendar-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  backdrop-filter: blur(4px);
+}
+
+.designer-calendar-modal {
+  background: white;
+  border-radius: 16px;
+  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+  max-width: 800px;
+  width: 90vw;
+  max-height: 90vh;
+  overflow-y: auto;
+  animation: modalSlideIn 0.3s ease-out;
+}
+
+@keyframes modalSlideIn {
+  from {
+    opacity: 0;
+    transform: translateY(-20px) scale(0.95);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24px 24px 16px;
+  border-bottom: 1px solid #f0f0f0;
+
+  h3 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: #2c3e50;
+  }
+
+  .close-btn {
+    background: none;
+    border: none;
+    font-size: 24px;
+    color: #95a5a6;
+    cursor: pointer;
+    padding: 4px;
+    border-radius: 4px;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background: #f8f9fa;
+      color: #e74c3c;
+    }
+  }
+}
+
+.designer-stats {
+  padding: 20px 24px;
+  background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+  border-bottom: 1px solid #dee2e6;
+
+  .stats-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+    gap: 16px;
+    margin-bottom: 20px;
+  }
+
+  .stat-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 12px;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+
+    .stat-label {
+      font-size: 12px;
+      color: #6c757d;
+      margin-bottom: 4px;
+    }
+
+    .stat-value {
+      font-size: 16px;
+      font-weight: 600;
+      color: #2c3e50;
+
+      &.warning {
+        color: #e74c3c;
+      }
+    }
+  }
+
+  .workload-indicator {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+
+    .workload-label {
+      font-size: 14px;
+      color: #6c757d;
+      min-width: 60px;
+    }
+
+    .workload-bar {
+      flex: 1;
+      height: 8px;
+      background: #e9ecef;
+      border-radius: 4px;
+      overflow: hidden;
+
+      .workload-fill {
+        height: 100%;
+        border-radius: 4px;
+        transition: width 0.3s ease;
+
+        &.low {
+          background: linear-gradient(90deg, #28a745, #20c997);
+        }
+
+        &.medium {
+          background: linear-gradient(90deg, #ffc107, #fd7e14);
+        }
+
+        &.high {
+          background: linear-gradient(90deg, #dc3545, #e74c3c);
+        }
+      }
+    }
+
+    .workload-percentage {
+      font-size: 14px;
+      font-weight: 600;
+      color: #495057;
+      min-width: 40px;
+      text-align: right;
+    }
+  }
+}
+
+.calendar-nav {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 24px;
+  border-bottom: 1px solid #f0f0f0;
+
+  .nav-btn {
+    background: #f8f9fa;
+    border: 1px solid #dee2e6;
+    border-radius: 6px;
+    width: 36px;
+    height: 36px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    font-size: 18px;
+    color: #495057;
+
+    &:hover {
+      background: #e9ecef;
+      border-color: #adb5bd;
+    }
+  }
+
+  .month-title {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #2c3e50;
+  }
+}
+
+.calendar-container {
+  padding: 0 24px 20px;
+}
+
+.calendar-header {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  gap: 1px;
+  margin-bottom: 8px;
+
+  .weekday {
+    padding: 12px 4px;
+    text-align: center;
+    font-size: 14px;
+    font-weight: 600;
+    color: #6c757d;
+    background: #f8f9fa;
+  }
+}
+
+.calendar-grid {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  gap: 1px;
+  background: #dee2e6;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.calendar-day {
+  background: white;
+  min-height: 60px;
+  padding: 8px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: flex-start;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  position: relative;
+
+  .day-number {
+    font-size: 14px;
+    font-weight: 500;
+    margin-bottom: 4px;
+  }
+
+  .day-indicators {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 2px;
+
+    .order-count {
+      background: #007bff;
+      color: white;
+      font-size: 10px;
+      padding: 2px 6px;
+      border-radius: 10px;
+      min-width: 16px;
+      text-align: center;
+    }
+
+    .review-indicator {
+      background: #dc3545;
+      color: white;
+      font-size: 9px;
+      padding: 1px 4px;
+      border-radius: 6px;
+    }
+  }
+
+  &.other-month {
+    color: #adb5bd;
+    background: #f8f9fa;
+
+    .day-indicators {
+      opacity: 0.5;
+    }
+  }
+
+  &.weekend {
+    background: #fff5f5;
+  }
+
+  &.today {
+    background: #e3f2fd;
+    border: 2px solid #2196f3;
+
+    .day-number {
+      color: #1976d2;
+      font-weight: 700;
+    }
+  }
+
+  &.idle {
+    background: #e8f5e8;
+    border: 1px solid #4caf50;
+
+    &:hover {
+      background: #c8e6c9;
+      transform: scale(1.05);
+    }
+
+    .day-number {
+      color: #2e7d32;
+    }
+  }
+
+  &.busy {
+    background: #e3f2fd;
+
+    .day-number {
+      color: #1565c0;
+    }
+
+    &:hover {
+      background: #bbdefb;
+    }
+  }
+
+  &.review {
+    background: #ffebee;
+    border: 1px solid #f44336;
+    cursor: not-allowed;
+
+    .day-number {
+      color: #c62828;
+    }
+
+    &:hover {
+      transform: none;
+    }
+  }
+
+  &.stagnant {
+    background: #fff3e0;
+    border: 1px solid #ff9800;
+
+    .day-number {
+      color: #ef6c00;
+    }
+  }
+}
+
+.calendar-legend {
+  display: flex;
+  justify-content: center;
+  gap: 20px;
+  padding: 16px 24px;
+  border-top: 1px solid #f0f0f0;
+  background: #f8f9fa;
+
+  .legend-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+
+    .legend-color {
+      width: 12px;
+      height: 12px;
+      border-radius: 2px;
+
+      &.idle {
+        background: #4caf50;
+      }
+
+      &.busy {
+        background: #2196f3;
+      }
+
+      &.review {
+        background: #f44336;
+      }
+
+      &.today {
+        background: #1976d2;
+        border: 1px solid #0d47a1;
+      }
+    }
+
+    .legend-text {
+      font-size: 12px;
+      color: #6c757d;
+    }
+  }
+}
+
+.calendar-tips {
+  padding: 16px 24px;
+  background: #f8f9fa;
+  border-top: 1px solid #dee2e6;
+
+  p {
+    margin: 0 0 8px;
+    font-size: 14px;
+    color: #495057;
+  }
+
+  ul {
+    margin: 0;
+    padding-left: 20px;
+    font-size: 13px;
+    color: #6c757d;
+    line-height: 1.5;
+
+    li {
+      margin-bottom: 4px;
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .designer-calendar-modal {
+    width: 95vw;
+    margin: 10px;
+  }
+
+  .designer-stats {
+    .stats-grid {
+      grid-template-columns: repeat(2, 1fr);
+      gap: 12px;
+    }
+
+    .workload-indicator {
+      flex-direction: column;
+      align-items: stretch;
+      gap: 8px;
+
+      .workload-label {
+        min-width: auto;
+        text-align: center;
+      }
+    }
+  }
+
+  .calendar-legend {
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .calendar-day {
+    min-height: 50px;
+    padding: 4px;
+
+    .day-number {
+      font-size: 12px;
+    }
+
+    .day-indicators {
+      .order-count,
+      .review-indicator {
+        font-size: 8px;
+        padding: 1px 3px;
+      }
+    }
+  }
+}

+ 187 - 0
src/app/pages/designer/project-detail/components/order-creation/order-creation.component.html

@@ -0,0 +1,187 @@
+<div class="order-creation-container">
+  <div class="section-header">
+    <h3>订单创建信息</h3>
+    <span class="required-note">* 为必填项</span>
+    <div class="header-actions">
+      <button type="button" class="btn-primary" (click)="showPricingRules()">
+        报价规则配置
+      </button>
+      <button type="button" class="btn-secondary" (click)="toggleStagnantFilter()">
+        @if (hideStagnantProjects) { 隐藏停滞项目:已开启 } @else { 隐藏停滞项目:未开启 }
+      </button>
+    </div>
+  </div>
+
+  <form [formGroup]="orderForm" class="order-form">
+    <!-- 自动报价生成 -->
+    <div class="form-group">
+      <label class="form-label">自动报价生成</label>
+      <div class="auto-quotation-section">
+        <div class="quotation-selectors">
+          <div class="selector-group">
+            <label for="spaceType">空间类型</label>
+            <select id="spaceType" class="form-select" [(ngModel)]="selectedSpaceType">
+              <option value="">请选择空间类型</option>
+              @for (space of spaceTypeConfig(); track space.type) {
+                <option [value]="space.type">{{ space.type }} ({{ space.basePrice }}元/人天)</option>
+              }
+            </select>
+          </div>
+          <div class="selector-group">
+            <label for="styleLevel">风格等级</label>
+            <select id="styleLevel" class="form-select" [(ngModel)]="selectedStyleLevel">
+              <option value="">请选择风格等级</option>
+              @for (style of styleLevelConfig(); track style.level) {
+                <option [value]="style.level">{{ style.name }} ({{ style.multiplier }}x)</option>
+              }
+            </select>
+          </div>
+        </div>
+        <div class="quotation-actions">
+          <button type="button" class="btn-primary" (click)="generateAutoQuotation()">
+            生成报价
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 生成的报价清单 -->
+    @if (generatedQuotation().length > 0) {
+      <div class="form-group">
+        <label class="form-label">生成的报价清单</label>
+        <div class="quotation-result">
+          @for (rule of generatedQuotation(); track rule.spaceType) {
+            <div class="quotation-item">
+              <div class="item-header">
+                <h4>{{ rule.spaceType }} - {{ rule.finalPrice }}元</h4>
+                <div class="item-actions">
+                  <button type="button" class="btn-text" (click)="showAdjustModal = true">调整价格</button>
+                </div>
+              </div>
+              <div class="item-details">
+                <p><strong>基础价格:</strong>{{ rule.basePrice }}元/人天</p>
+                <p><strong>风格系数:</strong>{{ rule.styleMultiplier }}x</p>
+                <p><strong>服务内容:</strong>{{ rule.description }}</p>
+                <p><strong>包含服务:</strong>{{ getServiceDescription(rule.spaceType, selectedStyleLevel()) }}</p>
+              </div>
+            </div>
+          }
+        </div>
+      </div>
+    }
+
+    <!-- 订单金额 -->
+    <div class="form-group">
+      <label for="orderAmount" class="form-label required">订单金额</label>
+      <div class="input-wrapper">
+        <input
+          id="orderAmount"
+          type="number"
+          formControlName="orderAmount"
+          class="form-input"
+          placeholder="请输入订单金额"
+          min="0.01"
+          step="0.01"
+        />
+        <span class="currency-unit">元</span>
+      </div>
+      @if (orderAmount?.invalid && orderAmount?.touched) {
+        <div class="error-message">
+          @if (orderAmount?.errors?.['required']) {
+            <span>订单金额为必填项</span>
+          }
+          @if (orderAmount?.errors?.['min']) {
+            <span>订单金额必须大于0</span>
+          }
+          @if (orderAmount?.errors?.['pattern']) {
+            <span>请输入有效的金额格式</span>
+          }
+        </div>
+      }
+    </div>
+
+
+
+    <!-- 交付时间 -->
+    <div class="form-group">
+      <label for="deliveryTime" class="form-label required">交付时间</label>
+      <input
+        id="deliveryTime"
+        type="date"
+        formControlName="deliveryTime"
+        class="form-input"
+      />
+      @if (deliveryTime?.invalid && deliveryTime?.touched) {
+        <div class="error-message">
+          <span>交付时间为必填项</span>
+        </div>
+      }
+    </div>
+
+    <!-- 报价明细组件 -->
+    <app-quotation-details 
+      [projectData]="{ customerInfo: {}, requirementInfo: {} }"
+      (dataChange)="onQuotationDataChange($event)">
+    </app-quotation-details>
+  </form>
+</div>
+
+<!-- 报价规则配置弹窗 -->
+@if (showPricingRulesModal()) {
+  <div class="modal-overlay" (click)="hidePricingRules()">
+    <div class="modal-content pricing-rules-modal" (click)="$event.stopPropagation()">
+      <div class="modal-header">
+        <h3>报价规则配置</h3>
+        <button type="button" class="close-btn" (click)="hidePricingRules()">×</button>
+      </div>
+      
+      <div class="modal-body">
+        <!-- 空间类型配置 -->
+        <div class="config-section">
+          <div class="section-header">
+            <h4>空间类型价格配置</h4>
+            <button type="button" class="btn-secondary" (click)="addSpaceType()">添加空间类型</button>
+          </div>
+          <div class="config-list">
+            @for (space of spaceTypeConfig(); track $index; let i = $index) {
+              <div class="config-item">
+                <div class="config-fields">
+                  <input type="text" [(ngModel)]="space.type" placeholder="空间类型" class="field-input">
+                  <input type="number" [(ngModel)]="space.basePrice" placeholder="基础价格" class="field-input">
+                  <input type="text" [(ngModel)]="space.description" placeholder="描述" class="field-input">
+                  <button type="button" class="btn-danger" (click)="removeSpaceType(i)">删除</button>
+                </div>
+              </div>
+            }
+          </div>
+        </div>
+
+        <!-- 风格等级配置 -->
+        <div class="config-section">
+          <div class="section-header">
+            <h4>风格等级系数配置</h4>
+            <button type="button" class="btn-secondary" (click)="addStyleLevel()">添加风格等级</button>
+          </div>
+          <div class="config-list">
+            @for (style of styleLevelConfig(); track $index; let i = $index) {
+              <div class="config-item">
+                <div class="config-fields">
+                  <input type="text" [(ngModel)]="style.level" placeholder="等级代码" class="field-input">
+                  <input type="text" [(ngModel)]="style.name" placeholder="等级名称" class="field-input">
+                  <input type="number" [(ngModel)]="style.multiplier" step="0.1" placeholder="系数" class="field-input">
+                  <input type="text" [(ngModel)]="style.description" placeholder="描述" class="field-input">
+                  <button type="button" class="btn-danger" (click)="removeStyleLevel(i)">删除</button>
+                </div>
+              </div>
+            }
+          </div>
+        </div>
+      </div>
+      
+      <div class="modal-footer">
+        <button type="button" class="btn-secondary" (click)="hidePricingRules()">取消</button>
+        <button type="button" class="btn-primary" (click)="savePricingRules()">保存配置</button>
+      </div>
+    </div>
+  </div>
+}

+ 245 - 0
src/app/pages/designer/project-detail/components/order-creation/order-creation.component.scss

@@ -0,0 +1,245 @@
+/* 订单创建组件样式 */
+.order-creation-container {
+  padding: 20px;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.order-form {
+  display: grid;
+  gap: 16px;
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+
+  label {
+    font-weight: 600;
+    color: #333;
+    font-size: 14px;
+  }
+
+  input, select, textarea {
+    padding: 12px;
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    font-size: 14px;
+    transition: border-color 0.2s ease;
+
+    &:focus {
+      outline: none;
+      border-color: #007aff;
+      box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
+    }
+  }
+
+  textarea {
+    min-height: 80px;
+    resize: vertical;
+  }
+}
+
+.form-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  margin-top: 20px;
+
+  button {
+    padding: 12px 24px;
+    border: none;
+    border-radius: 8px;
+    font-size: 14px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &.primary {
+      background: #007aff;
+      color: white;
+
+      &:hover {
+        background: #0056cc;
+      }
+    }
+
+    &.secondary {
+      background: #f8f9fa;
+      color: #666;
+      border: 1px solid #ddd;
+
+      &:hover {
+        background: #e9ecef;
+      }
+    }
+  }
+}
+
+/* 报价规则配置弹窗样式 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  max-width: 800px;
+  width: 90%;
+  max-height: 80vh;
+  overflow-y: auto;
+
+  &.pricing-rules-modal {
+    max-width: 900px;
+  }
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px;
+  border-bottom: 1px solid #eee;
+
+  h3 {
+    margin: 0;
+    color: #333;
+    font-size: 18px;
+    font-weight: 600;
+  }
+
+  .close-btn {
+    background: none;
+    border: none;
+    font-size: 24px;
+    color: #999;
+    cursor: pointer;
+    padding: 0;
+    width: 30px;
+    height: 30px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    &:hover {
+      color: #666;
+    }
+  }
+}
+
+.modal-body {
+  padding: 20px;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 20px;
+  border-top: 1px solid #eee;
+
+  .btn-primary {
+    background: #007aff;
+    color: white;
+    border: none;
+    padding: 10px 20px;
+    border-radius: 6px;
+    cursor: pointer;
+    font-weight: 500;
+
+    &:hover {
+      background: #0056cc;
+    }
+  }
+
+  .btn-secondary {
+    background: #f8f9fa;
+    color: #666;
+    border: 1px solid #ddd;
+    padding: 10px 20px;
+    border-radius: 6px;
+    cursor: pointer;
+    font-weight: 500;
+
+    &:hover {
+      background: #e9ecef;
+    }
+  }
+}
+
+.config-section {
+  margin-bottom: 30px;
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+
+    h4 {
+      margin: 0;
+      color: #333;
+      font-size: 16px;
+      font-weight: 600;
+    }
+  }
+}
+
+.config-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.config-item {
+  border: 1px solid #eee;
+  border-radius: 8px;
+  padding: 16px;
+  background: #fafafa;
+
+  .config-fields {
+    display: grid;
+    grid-template-columns: 1fr 1fr 2fr auto;
+    gap: 12px;
+    align-items: center;
+
+    .field-input {
+      padding: 8px 12px;
+      border: 1px solid #ddd;
+      border-radius: 6px;
+      font-size: 14px;
+
+      &:focus {
+        outline: none;
+        border-color: #007aff;
+        box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
+      }
+    }
+
+    .btn-danger {
+      background: #dc3545;
+      color: white;
+      border: none;
+      padding: 8px 16px;
+      border-radius: 6px;
+      cursor: pointer;
+      font-size: 12px;
+
+      &:hover {
+        background: #c82333;
+      }
+    }
+  }
+}

+ 319 - 0
src/app/pages/designer/project-detail/components/order-creation/order-creation.component.ts

@@ -0,0 +1,319 @@
+import { Component, Input, Output, EventEmitter, OnInit, signal } from '@angular/core';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { QuotationDetailsComponent, QuotationData } from '../quotation-details/quotation-details.component';
+
+export interface OrderCreationData {
+  orderAmount: number;
+  deliveryTime: string;
+  quotationData?: QuotationData;
+}
+
+// 报价规则接口
+export interface PricingRule {
+  spaceType: string;
+  basePrice: number; // 基础价格(元/人天)
+  styleMultiplier: number; // 风格系数
+  finalPrice: number; // 最终价格
+  description: string; // 服务内容描述
+}
+
+// 空间类型配置
+export interface SpaceTypeConfig {
+  type: string;
+  basePrice: number; // 元/人天
+  description: string;
+}
+
+// 风格等级配置
+export interface StyleLevelConfig {
+  level: string;
+  name: string;
+  multiplier: number;
+  description: string;
+}
+
+@Component({
+  selector: 'app-order-creation',
+  standalone: true,
+  imports: [CommonModule, FormsModule, ReactiveFormsModule, QuotationDetailsComponent],
+  templateUrl: './order-creation.component.html',
+  styleUrls: ['./order-creation.component.scss']
+})
+export class OrderCreationComponent implements OnInit {
+  @Input() initialData?: OrderCreationData;
+  @Output() dataChange = new EventEmitter<OrderCreationData>();
+  @Output() validityChange = new EventEmitter<boolean>();
+
+  orderForm: FormGroup;
+  quotationData?: QuotationData;
+  // 替换为停滞项目筛选的本地状态
+  hideStagnantProjects = false;
+
+  // 报价规则配置
+  showPricingRulesModal = signal(false);
+  showAdjustModal = false; // 添加调整价格模态框状态
+  
+  // 空间类型配置
+  spaceTypeConfig = signal<SpaceTypeConfig[]>([
+    { type: '客餐厅', basePrice: 600, description: '客厅餐厅一体化设计' },
+    { type: '卧室', basePrice: 400, description: '主卧/次卧设计' },
+    { type: '厨房', basePrice: 500, description: '厨房空间设计' },
+    { type: '卫生间', basePrice: 350, description: '卫生间设计' },
+    { type: '书房', basePrice: 450, description: '书房/工作区设计' },
+    { type: '阳台', basePrice: 300, description: '阳台/露台设计' }
+  ]);
+
+  // 风格等级配置
+  styleLevelConfig = signal<StyleLevelConfig[]>([
+    { level: 'standard', name: '标准', multiplier: 1.0, description: '基础设计方案' },
+    { level: 'premium', name: '高端', multiplier: 1.5, description: '精装设计方案,4K单张,6K全景' },
+    { level: 'luxury', name: '奢华', multiplier: 2.0, description: '顶级设计方案,8K渲染' }
+  ]);
+
+  // 当前选择的报价配置
+  selectedSpaceType = signal<string>('');
+  selectedStyleLevel = signal<string>('');
+  
+  // 生成的报价清单
+  generatedQuotation = signal<PricingRule[]>([]);
+
+  constructor(private fb: FormBuilder) {
+    this.orderForm = this.fb.group({
+      orderAmount: ['', [
+        Validators.required, 
+        Validators.min(0.01),
+        Validators.pattern(/^\d+(\.\d{1,2})?$/)
+      ]],
+      deliveryTime: ['', Validators.required]
+    });
+  }
+
+  ngOnInit() {
+    if (this.initialData) {
+      this.orderForm.patchValue(this.initialData);
+      this.quotationData = this.initialData.quotationData;
+    }
+
+    // 监听表单变化
+    this.orderForm.valueChanges.subscribe(value => {
+      this.emitDataChange();
+      this.validityChange.emit(this.isFormValid());
+    });
+
+    // 初始状态发送
+    this.validityChange.emit(this.isFormValid());
+  }
+
+  // 显示报价规则配置
+  showPricingRules(): void {
+    this.showPricingRulesModal.set(true);
+  }
+
+  // 隐藏报价规则配置
+  hidePricingRules(): void {
+    this.showPricingRulesModal.set(false);
+  }
+
+  // 添加空间类型
+  addSpaceType(): void {
+    const current = this.spaceTypeConfig();
+    current.push({ type: '', basePrice: 400, description: '' });
+    this.spaceTypeConfig.set([...current]);
+  }
+
+  // 删除空间类型
+  removeSpaceType(index: number): void {
+    const current = this.spaceTypeConfig();
+    current.splice(index, 1);
+    this.spaceTypeConfig.set([...current]);
+  }
+
+  // 添加风格等级
+  addStyleLevel(): void {
+    const current = this.styleLevelConfig();
+    current.push({ level: '', name: '', multiplier: 1.0, description: '' });
+    this.styleLevelConfig.set([...current]);
+  }
+
+  // 删除风格等级
+  removeStyleLevel(index: number): void {
+    const current = this.styleLevelConfig();
+    current.splice(index, 1);
+    this.styleLevelConfig.set([...current]);
+  }
+
+  // 保存报价规则配置
+  savePricingRules(): void {
+    // 这里应该调用服务保存配置到后端
+    console.log('保存报价规则配置:', {
+      spaceTypes: this.spaceTypeConfig(),
+      styleLevels: this.styleLevelConfig()
+    });
+    this.hidePricingRules();
+  }
+
+  // 生成自动报价
+  generateAutoQuotation(): void {
+    if (!this.selectedSpaceType() || !this.selectedStyleLevel()) {
+      alert('请先选择空间类型和风格等级');
+      return;
+    }
+
+    const spaceConfig = this.spaceTypeConfig().find(s => s.type === this.selectedSpaceType());
+    const styleConfig = this.styleLevelConfig().find(s => s.level === this.selectedStyleLevel());
+
+    if (!spaceConfig || !styleConfig) {
+      alert('配置信息不完整');
+      return;
+    }
+
+    const finalPrice = spaceConfig.basePrice * styleConfig.multiplier;
+    const rule: PricingRule = {
+      spaceType: spaceConfig.type,
+      basePrice: spaceConfig.basePrice,
+      styleMultiplier: styleConfig.multiplier,
+      finalPrice: finalPrice,
+      description: `${styleConfig.name}${spaceConfig.type}项目 - ${spaceConfig.description},${styleConfig.description}`
+    };
+
+    this.generatedQuotation.set([rule]);
+    
+    // 更新订单金额
+    this.orderForm.patchValue({
+      orderAmount: finalPrice
+    });
+
+    // 生成报价数据
+    this.quotationData = {
+      items: [{
+        id: '1',
+        category: '设计服务', // 添加category属性
+        name: `${styleConfig.name}${spaceConfig.type}设计`,
+        quantity: 1,
+        unit: '项',
+        unitPrice: finalPrice,
+        totalPrice: finalPrice,
+        description: rule.description
+      }],
+      totalAmount: finalPrice,
+      materialCost: 0,
+      laborCost: finalPrice,
+      designFee: finalPrice,
+      managementFee: 0
+    };
+
+    this.emitDataChange();
+  }
+
+  // 调整报价
+  adjustQuotation(newPrice: number, reason: string): void {
+    if (this.generatedQuotation().length === 0) {
+      alert('请先生成报价');
+      return;
+    }
+
+    const currentRule = this.generatedQuotation()[0];
+    const adjustedRule: PricingRule = {
+      ...currentRule,
+      finalPrice: newPrice,
+      description: `${currentRule.description} (调整原因: ${reason})`
+    };
+
+    this.generatedQuotation.set([adjustedRule]);
+    
+    // 更新订单金额
+    this.orderForm.patchValue({
+      orderAmount: newPrice
+    });
+
+    // 更新报价数据
+    if (this.quotationData) {
+      this.quotationData.items[0].unitPrice = newPrice;
+      this.quotationData.items[0].totalPrice = newPrice;
+      this.quotationData.totalAmount = newPrice;
+      this.quotationData.laborCost = newPrice;
+      this.quotationData.designFee = newPrice;
+    }
+
+    this.emitDataChange();
+    
+    // 记录调整日志(应该发送到后端)
+    console.log('报价调整记录:', {
+      originalPrice: currentRule.finalPrice,
+      adjustedPrice: newPrice,
+      reason: reason,
+      timestamp: new Date(),
+      spaceType: currentRule.spaceType
+    });
+  }
+
+  // 获取服务内容说明
+  getServiceDescription(spaceType: string, styleLevel: string): string {
+    const spaceConfig = this.spaceTypeConfig().find(s => s.type === spaceType);
+    const styleConfig = this.styleLevelConfig().find(s => s.level === styleLevel);
+    
+    if (!spaceConfig || !styleConfig) return '';
+    
+    const baseServices = ['建模', '渲染', '1次小图修改'];
+    const premiumServices = styleLevel === 'premium' ? ['4K单张交付', '6K全景交付'] : [];
+    const luxuryServices = styleLevel === 'luxury' ? ['8K渲染', '无限次修改'] : [];
+    
+    return [...baseServices, ...premiumServices, ...luxuryServices].join('、');
+  }
+
+  // 处理报价明细数据变化
+  onQuotationDataChange(data: QuotationData) {
+    this.quotationData = data;
+    this.emitDataChange();
+    this.validityChange.emit(this.isFormValid());
+  }
+
+  // 发送数据变化事件
+  private emitDataChange() {
+    const formData = this.orderForm.value;
+    const completeData: OrderCreationData = {
+      ...formData,
+      quotationData: this.quotationData
+    };
+    this.dataChange.emit(completeData);
+  }
+
+  // 检查表单有效性
+  private isFormValid(): boolean {
+    return this.orderForm.valid && !!this.quotationData && this.quotationData.items.length > 0;
+  }
+
+  // 获取表单控件
+  get orderAmount() { return this.orderForm.get('orderAmount'); }
+  get deliveryTime() { return this.orderForm.get('deliveryTime'); }
+
+  // 获取表单数据
+  getFormData(): OrderCreationData {
+    return {
+      ...this.orderForm.value,
+      quotationData: this.quotationData
+    };
+  }
+
+  // 重置表单
+  resetForm() {
+    this.orderForm.reset();
+    this.quotationData = undefined;
+    this.generatedQuotation.set([]);
+    this.selectedSpaceType.set('');
+    this.selectedStyleLevel.set('');
+  }
+
+  // 验证表单
+  validateForm(): boolean {
+    this.orderForm.markAllAsTouched();
+    return this.isFormValid();
+  }
+
+  // 切换停滞项目筛选(仅本地UI状态)
+  toggleStagnantFilter() {
+    this.hideStagnantProjects = !this.hideStagnantProjects;
+  }
+}

+ 252 - 0
src/app/pages/designer/project-detail/components/quotation-details/quotation-details.component.html

@@ -0,0 +1,252 @@
+<div class="quotation-details-container">
+  <div class="section-header">
+    <h3>报价明细</h3>
+    <div class="header-actions">
+      <button type="button" class="btn-secondary" (click)="showAutoQuotation()">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M8 1L10.5 6H15L11 9.5L12.5 15L8 12L3.5 15L5 9.5L1 6H5.5L8 1Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
+        </svg>
+        智能报价
+      </button>
+      <button type="button" class="btn-secondary" (click)="showScriptLibrary()">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M3 3h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" stroke="currentColor" stroke-width="1.5" fill="none"/>
+          <path d="M7 8h4M7 10h2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+        </svg>
+        话术库
+      </button>
+      <button type="button" class="btn-primary" (click)="addQuotationItem()">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M8 3V13M3 8H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+        </svg>
+        添加项目
+      </button>
+    </div>
+  </div>
+
+  <div class="quotation-form">
+    <!-- 报价项目列表 -->
+    <div class="quotation-items">
+      @for (item of quotationData.items; track item.id; let i = $index) {
+        <div class="quotation-item expanded">
+          <div class="item-header">
+            <div class="item-info">
+              <span class="item-name">{{ item.name || '未命名项目' }}</span>
+              <span class="item-category">{{ item.category }}</span>
+            </div>
+            <div class="item-actions">
+              <button type="button" class="btn-icon" (click)="duplicateQuotationItem(i)" title="复制">
+                <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2Zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6ZM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2Z"/>
+                </svg>
+              </button>
+              <button type="button" class="btn-icon btn-danger" (click)="removeQuotationItem(i)" title="删除">
+                <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
+                  <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
+                </svg>
+              </button>
+            </div>
+          </div>
+          
+          <div class="item-details">
+            <div class="form-row">
+              <div class="form-group">
+                <label class="form-label">项目名称</label>
+                <input type="text" [(ngModel)]="item.name" (ngModelChange)="onItemChange()" class="form-input" placeholder="请输入项目名称">
+              </div>
+              <div class="form-group">
+                <label class="form-label">类别</label>
+                <select [(ngModel)]="item.category" (ngModelChange)="onItemChange()" class="form-select">
+                  <option value="客餐厅">客餐厅</option>
+                  <option value="厨房">厨房</option>
+                  <option value="卫生间">卫生间</option>
+                  <option value="卧室">卧室</option>
+                  <option value="阳台">阳台</option>
+                  <option value="其他">其他</option>
+                </select>
+              </div>
+            </div>
+            
+            <div class="form-row">
+              <div class="form-group">
+                <label class="form-label">数量</label>
+                <input type="number" [(ngModel)]="item.quantity" (ngModelChange)="onItemChange()" class="form-input" min="0" step="0.01">
+              </div>
+              <div class="form-group">
+                <label class="form-label">单位</label>
+                <select [(ngModel)]="item.unit" (ngModelChange)="onItemChange()" class="form-select">
+                  <option value="㎡">㎡</option>
+                  <option value="延米">延米</option>
+                  <option value="个">个</option>
+                  <option value="套">套</option>
+                  <option value="项">项</option>
+                </select>
+              </div>
+              <div class="form-group">
+                <label class="form-label">单价(元)</label>
+                <input type="number" [(ngModel)]="item.unitPrice" (ngModelChange)="onItemChange()" class="form-input" min="0" step="0.01">
+              </div>
+              <div class="form-group">
+                <label class="form-label">小计(元)</label>
+                <input type="text" [value]="(item.quantity * item.unitPrice).toFixed(2)" class="form-input" readonly>
+              </div>
+            </div>
+            
+            <div class="form-group">
+              <label class="form-label">项目描述</label>
+              <textarea [(ngModel)]="item.description" (ngModelChange)="onItemChange()" class="form-textarea" rows="2" placeholder="请输入项目描述"></textarea>
+            </div>
+          </div>
+        </div>
+      }
+
+      @if (quotationData.items.length === 0) {
+        <div class="empty-state">
+          <div class="empty-icon">
+            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M24 4L28.5 18H42L32 26.5L36.5 40L24 32L11.5 40L16 26.5L6 18H19.5L24 4Z" stroke="#D1D5DB" stroke-width="2" fill="none"/>
+            </svg>
+          </div>
+          <p>暂无报价项目</p>
+          <p class="empty-hint">点击"添加项目"或"智能报价"开始创建报价清单</p>
+        </div>
+      }
+    </div>
+
+    <!-- 报价汇总 -->
+    @if (quotationData.items.length > 0) {
+      <div class="quotation-summary">
+        <div class="summary-header">
+          <h4>报价汇总</h4>
+        </div>
+        <div class="summary-content">
+          <div class="summary-row">
+            <span class="summary-label">项目总数:</span>
+            <span class="summary-value">{{ quotationData.items.length }} 项</span>
+          </div>
+          <div class="summary-row">
+            <span class="summary-label">材料费用:</span>
+            <span class="summary-value">¥{{ formatAmount(getMaterialCost()) }}</span>
+          </div>
+          <div class="summary-row">
+            <span class="summary-label">人工费用:</span>
+            <span class="summary-value">¥{{ formatAmount(getLaborCost()) }}</span>
+          </div>
+          <div class="summary-row total">
+            <span class="summary-label">合计金额:</span>
+            <span class="summary-value">¥{{ formatAmount(getTotalAmount()) }}</span>
+          </div>
+        </div>
+        <div class="summary-actions">
+          <button type="button" class="btn-primary" (click)="submitForApproval()" [disabled]="quotationData.totalAmount === 0">
+            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M8 1L10.5 6H15L11 9.5L12.5 15L8 12L3.5 15L5 9.5L1 6H5.5L8 1Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
+            </svg>
+            提交财务审核
+          </button>
+        </div>
+      </div>
+    }
+  </div>
+
+
+
+  <!-- 智能报价模态框 -->
+  @if (showAutoQuotationModal) {
+    <div class="modal-overlay" (click)="hideAutoQuotation()">
+      <div class="modal-content" (click)="$event.stopPropagation()">
+        <div class="modal-header">
+          <h4>智能报价生成</h4>
+          <button type="button" class="modal-close" (click)="hideAutoQuotation()">
+            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+            </svg>
+          </button>
+        </div>
+        <div class="modal-body">
+          <div class="form-grid">
+            <div class="form-group">
+              <label class="form-label">空间类型</label>
+              <select [(ngModel)]="autoQuotationParams.spaceType" class="form-select">
+                <option value="">请选择空间类型</option>
+                @for (type of getSpaceTypeOptions(); track type.value) {
+                  <option [value]="type.value">{{ type.label }}</option>
+                }
+              </select>
+            </div>
+            <div class="form-group">
+              <label class="form-label">项目风格</label>
+              <select [(ngModel)]="autoQuotationParams.projectStyle" class="form-select">
+                <option value="">请选择项目风格</option>
+                @for (style of getProjectStyleOptions(); track style.value) {
+                  <option [value]="style.value">{{ style.label }}</option>
+                }
+              </select>
+            </div>
+            <div class="form-group">
+              <label class="form-label">预估工作天数</label>
+              <input type="number" [(ngModel)]="autoQuotationParams.estimatedWorkDays" class="form-input" placeholder="请输入预估工作天数" min="1" max="365">
+            </div>
+            <div class="form-group">
+              <label class="form-label">项目面积 (㎡)</label>
+              <input type="number" [(ngModel)]="autoQuotationParams.projectArea" class="form-input" placeholder="请输入项目面积" min="1">
+            </div>
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn-secondary" (click)="hideAutoQuotation()">取消</button>
+          <button type="button" class="btn-primary" (click)="generateAutoQuotation()" [disabled]="!autoQuotationParams.spaceType || !autoQuotationParams.projectStyle || !autoQuotationParams.estimatedWorkDays">
+            生成报价
+          </button>
+        </div>
+      </div>
+    </div>
+  }
+
+  <!-- 话术库模态框 -->
+  @if (showScriptModal) {
+    <div class="modal-overlay" (click)="hideScriptLibrary()">
+      <div class="modal-content script-modal" (click)="$event.stopPropagation()">
+        <div class="modal-header">
+          <h4>报价话术库</h4>
+          <button type="button" class="modal-close" (click)="hideScriptLibrary()">
+            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+            </svg>
+          </button>
+        </div>
+        <div class="modal-body">
+          <div class="script-categories">
+            @for (script of pricingScripts; track script.id) {
+              <div class="script-item" [class.selected]="selectedScript?.id === script.id" (click)="selectScript(script)">
+                <div class="script-header">
+                  <h5>{{ script.title }}</h5>
+                  <span class="script-category">{{ script.category }}</span>
+                </div>
+                <p class="script-description">{{ script.content.substring(0, 100) }}...</p>
+                @if (selectedScript?.id === script.id) {
+                  <div class="script-content">
+                    <div class="script-text">{{ script.content }}</div>
+                    <div class="script-actions">
+                      <button type="button" class="btn-secondary btn-sm" (click)="copyScript(script); $event.stopPropagation()">
+                        <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+                          <path d="M4.5 4.5V2.5C4.5 2.22386 4.72386 2 5 2H11.5C11.7761 2 12 2.22386 12 2.5V9C12 9.27614 11.7761 9.5 11.5 9.5H9.5" stroke="currentColor" stroke-width="1" fill="none"/>
+                          <rect x="2" y="4.5" width="7.5" height="7.5" rx="0.5" stroke="currentColor" stroke-width="1" fill="none"/>
+                        </svg>
+                        复制
+                      </button>
+                    </div>
+                  </div>
+                }
+              </div>
+            }
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn-secondary" (click)="hideScriptLibrary()">关闭</button>
+        </div>
+      </div>
+    </div>
+  }
+</div>

+ 559 - 0
src/app/pages/designer/project-detail/components/quotation-details/quotation-details.component.scss

@@ -0,0 +1,559 @@
+.quotation-details-container {
+  background: white;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  .summary-actions {
+    margin-top: 16px;
+    padding-top: 16px;
+    border-top: 1px solid #E5E7EB;
+    display: flex;
+    justify-content: flex-end;
+    
+    .btn-primary {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+    }
+  }
+
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #f0f0f0;
+
+  h3 {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1a1a1a;
+  }
+
+  .header-actions {
+    display: flex;
+    gap: 12px;
+  }
+}
+
+.btn-primary, .btn-secondary {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 16px;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  border: none;
+  cursor: pointer;
+  transition: all 0.2s ease;
+
+  svg {
+    width: 16px;
+    height: 16px;
+  }
+}
+
+.btn-primary {
+  background: #3b82f6;
+  color: white;
+
+  &:hover {
+    background: #2563eb;
+  }
+
+  &:disabled {
+    background: #9ca3af;
+    cursor: not-allowed;
+  }
+}
+
+.btn-secondary {
+  background: #f8fafc;
+  color: #475569;
+  border: 1px solid #e2e8f0;
+
+  &:hover {
+    background: #f1f5f9;
+    border-color: #cbd5e1;
+  }
+}
+
+.quotation-items {
+  margin-bottom: 24px;
+}
+
+.quotation-item {
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  margin-bottom: 12px;
+  overflow: hidden;
+  transition: all 0.2s ease;
+
+  &:hover {
+    border-color: #d1d5db;
+  }
+
+  &.expanded {
+    border-color: #3b82f6;
+    box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.1);
+  }
+}
+
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px;
+  cursor: pointer;
+  background: #fafafa;
+  transition: background-color 0.2s ease;
+
+  &:hover {
+    background: #f5f5f5;
+  }
+
+  .expanded & {
+    background: #f8fafc;
+    border-bottom: 1px solid #e5e7eb;
+  }
+}
+
+.item-basic-info {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  flex: 1;
+
+  .item-category {
+    background: #e0f2fe;
+    color: #0369a1;
+    padding: 4px 8px;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+  }
+
+  .item-name {
+    font-weight: 500;
+    color: #374151;
+    min-width: 120px;
+  }
+
+  .item-amount {
+    font-weight: 600;
+    color: #059669;
+    font-size: 16px;
+  }
+}
+
+.item-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.btn-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border: none;
+  background: transparent;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  color: #6b7280;
+
+  &:hover {
+    background: #f3f4f6;
+    color: #374151;
+  }
+
+  &.btn-danger:hover {
+    background: #fef2f2;
+    color: #dc2626;
+  }
+
+  svg {
+    width: 14px;
+    height: 14px;
+
+    &.rotated {
+      transform: rotate(180deg);
+    }
+  }
+}
+
+.item-details {
+  padding: 20px;
+  background: white;
+}
+
+.form-row {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 16px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.form-label {
+  font-size: 14px;
+  font-weight: 500;
+  color: #374151;
+}
+
+.form-input, .form-select, .form-textarea {
+  padding: 10px 12px;
+  border: 1px solid #d1d5db;
+  border-radius: 6px;
+  font-size: 14px;
+  transition: border-color 0.2s ease;
+
+  &:focus {
+    outline: none;
+    border-color: #3b82f6;
+    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+  }
+
+  &[readonly] {
+    background: #f9fafb;
+    color: #6b7280;
+  }
+}
+
+.form-textarea {
+  resize: vertical;
+  min-height: 60px;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 48px 24px;
+  color: #6b7280;
+
+  .empty-icon {
+    margin-bottom: 16px;
+  }
+
+  p {
+    margin: 8px 0;
+    
+    &.empty-hint {
+      font-size: 14px;
+      color: #9ca3af;
+    }
+  }
+}
+
+.quotation-summary {
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  padding: 20px;
+
+  .summary-header {
+    margin-bottom: 16px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #e2e8f0;
+
+    h4 {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #1e293b;
+    }
+  }
+
+  .summary-content {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .summary-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    &.total {
+      padding-top: 12px;
+      border-top: 1px solid #e2e8f0;
+      font-weight: 600;
+      font-size: 16px;
+
+      .summary-value {
+        color: #059669;
+        font-size: 18px;
+      }
+    }
+  }
+
+  .summary-label {
+    color: #64748b;
+    font-size: 14px;
+  }
+
+  .summary-value {
+    color: #1e293b;
+    font-weight: 500;
+  }
+}
+
+// 模态框样式
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  background: white;
+  border-radius: 12px;
+  width: 90%;
+  max-width: 500px;
+  max-height: 90vh;
+  overflow: hidden;
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e5e7eb;
+
+  h3 {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1a1a1a;
+  }
+
+  .btn-close {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 32px;
+    height: 32px;
+    border: none;
+    background: transparent;
+    border-radius: 6px;
+    cursor: pointer;
+    color: #6b7280;
+
+    &:hover {
+      background: #f3f4f6;
+      color: #374151;
+    }
+  }
+}
+
+.modal-body {
+  padding: 24px;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 20px 24px;
+  border-top: 1px solid #e5e7eb;
+  background: #fafafa;
+}
+
+.ai-form {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.loading-spinner {
+  width: 16px;
+  height: 16px;
+  border: 2px solid transparent;
+  border-top: 2px solid currentColor;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .quotation-details-container {
+    padding: 16px;
+  }
+
+  .section-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 16px;
+
+    .header-actions {
+      width: 100%;
+      justify-content: flex-end;
+    }
+  }
+
+  .item-header {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+  }
+
+  .item-basic-info {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+    width: 100%;
+  }
+
+  .form-row {
+    grid-template-columns: 1fr;
+  }
+
+  .modal-content {
+    width: 95%;
+    margin: 20px;
+  }
+}
+
+// 智能报价和话术库模态框样式
+.script-modal {
+  max-width: 800px;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.script-categories {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  max-height: 500px;
+  overflow-y: auto;
+}
+
+.script-item {
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  padding: 16px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+
+  &:hover {
+    border-color: #3b82f6;
+    background-color: #f8faff;
+  }
+
+  &.selected {
+    border-color: #3b82f6;
+    background-color: #f0f7ff;
+  }
+}
+
+.script-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+
+  h5 {
+    margin: 0;
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+}
+
+.script-category {
+  background: #e5e7eb;
+  color: #6b7280;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.script-description {
+  margin: 0 0 12px 0;
+  color: #6b7280;
+  font-size: 14px;
+  line-height: 1.5;
+}
+
+.script-content {
+  border-top: 1px solid #e5e7eb;
+  padding-top: 12px;
+  margin-top: 12px;
+}
+
+.script-text {
+  background: #f9fafb;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  padding: 12px;
+  font-size: 14px;
+  line-height: 1.6;
+  color: #374151;
+  white-space: pre-wrap;
+  margin-bottom: 12px;
+}
+
+.script-actions {
+  display: flex;
+  justify-content: flex-end;
+}
+
+.btn-sm {
+  padding: 6px 12px;
+  font-size: 12px;
+
+  svg {
+    width: 14px;
+    height: 14px;
+  }
+}
+
+// 表单网格布局
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 16px;
+
+  @media (max-width: 768px) {
+    grid-template-columns: 1fr;
+  }
+}}

+ 326 - 0
src/app/pages/designer/project-detail/components/quotation-details/quotation-details.component.ts

@@ -0,0 +1,326 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { WorkHourService } from '../../../../../services/work-hour.service';
+import { QuotationApprovalService } from '../../../../../services/quotation-approval.service';
+import { SpaceType, ProjectStyle, AutoQuotationResult, PricingScript } from '../../../../../models/work-hour.model';
+
+export interface QuotationItem {
+  id: string;
+  category: string;
+  name: string;
+  quantity: number;
+  unit: string;
+  unitPrice: number;
+  totalPrice?: number; // 添加总价属性
+  description: string;
+}
+
+export interface QuotationData {
+  items: QuotationItem[];
+  totalAmount: number;
+  materialCost: number;
+  laborCost: number;
+  designFee: number;
+  managementFee: number;
+}
+
+@Component({
+  selector: 'app-quotation-details',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './quotation-details.component.html',
+  styleUrls: ['./quotation-details.component.scss']
+})
+export class QuotationDetailsComponent implements OnInit {
+  @Input() initialData?: QuotationData;
+  @Input() projectData?: any; // 添加项目数据输入属性
+  @Output() dataChange = new EventEmitter<QuotationData>();
+
+  quotationData: QuotationData = {
+    items: [],
+    totalAmount: 0,
+    materialCost: 0,
+    laborCost: 0,
+    designFee: 0,
+    managementFee: 0
+  };
+
+  // 新增:自动报价相关
+  showAutoQuotationModal = false;
+  autoQuotationParams = {
+    spaceType: '客餐厅' as SpaceType,
+    projectStyle: '现代简约' as ProjectStyle,
+    estimatedWorkDays: 3,
+    projectArea: 100
+  };
+  
+  // 报价话术库
+  showScriptModal = false;
+  pricingScripts: PricingScript[] = [];
+  selectedScript: PricingScript | null = null;
+
+  constructor(
+    private workHourService: WorkHourService,
+    private quotationApprovalService: QuotationApprovalService
+  ) {}
+
+  ngOnInit() {
+    if (this.initialData) {
+      this.quotationData = { ...this.initialData };
+    }
+    
+    // 加载报价话术库
+    this.loadPricingScripts();
+  }
+
+  // 添加报价项目
+  addQuotationItem() {
+    const newItem: QuotationItem = {
+      id: Date.now().toString(),
+      category: '客餐厅',
+      name: '',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 0,
+      description: ''
+    };
+    
+    this.quotationData.items.push(newItem);
+    this.calculateTotal();
+    this.emitDataChange();
+  }
+
+  // 删除报价项目
+  removeQuotationItem(index: number) {
+    this.quotationData.items.splice(index, 1);
+    this.calculateTotal();
+    this.emitDataChange();
+  }
+
+  // 复制报价项目
+  duplicateQuotationItem(index: number) {
+    const originalItem = this.quotationData.items[index];
+    const duplicatedItem: QuotationItem = {
+      ...originalItem,
+      id: Date.now().toString(),
+      name: originalItem.name + ' (副本)'
+    };
+    
+    this.quotationData.items.splice(index + 1, 0, duplicatedItem);
+    this.calculateTotal();
+    this.emitDataChange();
+  }
+
+  // 计算总金额
+  calculateTotal() {
+    const materialCost = this.quotationData.items.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0);
+    this.quotationData.materialCost = materialCost;
+    this.quotationData.laborCost = materialCost * 0.3; // 人工费按材料费30%计算
+    this.quotationData.designFee = materialCost * 0.05; // 设计费按材料费5%计算
+    this.quotationData.managementFee = materialCost * 0.08; // 管理费按材料费8%计算
+    this.quotationData.totalAmount = this.quotationData.materialCost + this.quotationData.laborCost + this.quotationData.designFee + this.quotationData.managementFee;
+  }
+
+  // 获取总金额
+  getTotalAmount(): number {
+    return this.quotationData.totalAmount;
+  }
+
+  // 获取材料费用
+  getMaterialCost(): number {
+    return this.quotationData.materialCost;
+  }
+
+  // 获取人工费用
+  getLaborCost(): number {
+    return this.quotationData.laborCost;
+  }
+
+  // 格式化金额显示
+  formatAmount(amount: number): string {
+    return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+  }
+
+  // 项目数据变化处理
+  onItemChange() {
+    this.calculateTotal();
+    this.emitDataChange();
+  }
+
+  // 发送数据变化事件
+  private emitDataChange() {
+    this.dataChange.emit({ ...this.quotationData });
+  }
+
+
+
+  // 加载报价话术库
+  loadPricingScripts() {
+    this.workHourService.getPricingScripts().subscribe(scripts => {
+      this.pricingScripts = scripts;
+    });
+  }
+
+  // 显示自动报价模态框
+  showAutoQuotation() {
+    this.showAutoQuotationModal = true;
+  }
+
+  // 关闭自动报价模态框
+  hideAutoQuotation() {
+    this.showAutoQuotationModal = false;
+  }
+
+  // 生成自动报价
+  generateAutoQuotation() {
+    this.workHourService.generateAutoQuotation(
+      this.autoQuotationParams.spaceType,
+      this.autoQuotationParams.projectStyle,
+      this.autoQuotationParams.estimatedWorkDays
+    ).subscribe({
+      next: (result: AutoQuotationResult) => {
+        // 将自动报价结果转换为报价项目
+        const autoItem: QuotationItem = {
+          id: Date.now().toString(),
+          category: result.spaceType,
+          name: `${result.projectStyle}${result.spaceType}设计`,
+          quantity: result.estimatedWorkDays,
+          unit: '人天',
+          unitPrice: result.finalPrice / result.estimatedWorkDays,
+          description: `包含服务:${result.serviceIncludes.join('、')}`
+        };
+
+        // 添加到报价项目列表
+        this.quotationData.items.push(autoItem);
+        this.calculateTotal();
+        this.emitDataChange();
+        
+        this.hideAutoQuotation();
+      },
+      error: (error) => {
+        console.error('自动报价生成失败:', error);
+      }
+    });
+  }
+
+  // 显示话术库模态框
+  showScriptLibrary() {
+    this.showScriptModal = true;
+  }
+
+  // 关闭话术库模态框
+  hideScriptLibrary() {
+    this.showScriptModal = false;
+    this.selectedScript = null;
+  }
+
+  // 选择话术
+  selectScript(script: PricingScript) {
+    this.selectedScript = script;
+    
+    // 记录话术使用
+    if (this.projectData?.customerInfo?.name) {
+      this.quotationApprovalService.recordScriptUsage(
+        'current_quotation', // 这里应该是实际的报价ID
+        script.id,
+        'current_user', // 这里应该是当前用户ID
+        '当前用户' // 这里应该是当前用户名
+      ).subscribe();
+    }
+  }
+
+  // 复制话术内容
+  copyScript(script: PricingScript) {
+    navigator.clipboard.writeText(script.content).then(() => {
+      console.log('话术内容已复制到剪贴板');
+      
+      // 记录话术使用
+      if (this.projectData?.customerInfo?.name) {
+        this.quotationApprovalService.recordScriptUsage(
+          'current_quotation',
+          script.id,
+          'current_user',
+          '当前用户'
+        ).subscribe();
+      }
+    });
+  }
+  
+  copyScriptContent() {
+    if (this.selectedScript) {
+      navigator.clipboard.writeText(this.selectedScript.content).then(() => {
+        console.log('话术内容已复制到剪贴板');
+        
+        // 记录话术使用
+        if (this.projectData?.customerInfo?.name) {
+          this.quotationApprovalService.recordScriptUsage(
+            'current_quotation',
+            this.selectedScript!.id,
+            'current_user',
+            '当前用户'
+          ).subscribe();
+        }
+      });
+    }
+  }
+
+  // 提交报价审核
+  submitForApproval() {
+    if (!this.projectData?.customerInfo?.name || this.quotationData.totalAmount === 0) {
+      console.warn('报价信息不完整,无法提交审核');
+      return;
+    }
+
+    const submissionData = {
+      quotationId: `q${Date.now()}`,
+      projectId: this.projectData.projectId || `p${Date.now()}`,
+      projectName: this.projectData.requirementInfo?.projectName || '未命名项目',
+      customerName: this.projectData.customerInfo.name,
+      submittedBy: '当前用户', // 这里应该是当前用户名
+      totalAmount: this.quotationData.totalAmount,
+      scriptUsed: this.selectedScript?.id
+    };
+
+    this.quotationApprovalService.submitQuotationForApproval(submissionData).subscribe({
+      next: (approval) => {
+        console.log('报价已提交审核:', approval);
+        // 可以显示成功提示
+      },
+      error: (error) => {
+        console.error('提交审核失败:', error);
+        // 可以显示错误提示
+      }
+    });
+  }
+
+  // 获取空间类型选项
+  getSpaceTypeOptions(): { value: SpaceType; label: string }[] {
+    return [
+      { value: '客餐厅', label: '客餐厅' },
+      { value: '卧室', label: '卧室' },
+      { value: '厨房', label: '厨房' },
+      { value: '卫生间', label: '卫生间' },
+      { value: '书房', label: '书房' },
+      { value: '阳台', label: '阳台' },
+      { value: '玄关', label: '玄关' },
+      { value: '别墅', label: '别墅' }
+    ];
+  }
+
+  // 获取项目风格选项
+  getProjectStyleOptions(): { value: ProjectStyle; label: string }[] {
+    return [
+      { value: '现代简约', label: '现代简约' },
+      { value: '北欧', label: '北欧' },
+      { value: '新中式', label: '新中式' },
+      { value: '美式', label: '美式' },
+      { value: '欧式', label: '欧式' },
+      { value: '日式', label: '日式' },
+      { value: '工业风', label: '工业风' },
+      { value: '轻奢', label: '轻奢' }
+    ];
+  }
+
+
+}

+ 0 - 295
src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav-styles.scss

@@ -1,295 +0,0 @@
-@use '../../../../../shared/styles/_ios-theme.scss' as *;
-
-// 项目人员标签页样式
-.members-tab-content {
-  .main-content-layout {
-    display: grid;
-    grid-template-columns: auto 1fr;
-    gap: $ios-spacing-lg;
-    align-items: start;
-  }
-
-  .members-content {
-    background: $ios-background;
-    border-radius: $ios-radius-lg;
-    padding: $ios-spacing-lg;
-    box-shadow: $ios-shadow-sm;
-  }
-
-  .members-header {
-    margin-bottom: $ios-spacing-lg;
-    
-    h2 {
-      color: $ios-text-primary;
-      font-size: $ios-font-size-lg;
-      font-weight: 600;
-      margin: 0 0 $ios-spacing-xs 0;
-    }
-
-    .members-count {
-      color: $ios-text-secondary;
-      font-size: $ios-font-size-sm;
-      margin: 0;
-    }
-  }
-
-  .members-grid {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
-    gap: $ios-spacing-md;
-  }
-
-  .member-card {
-    background: $ios-background;
-    border: 1px solid $ios-border;
-    border-radius: $ios-radius-md;
-    padding: $ios-spacing-md;
-    transition: all 0.2s ease;
-
-    &:hover {
-      box-shadow: $ios-shadow-md;
-      transform: translateY(-2px);
-    }
-
-    .member-avatar {
-      width: 60px;
-      height: 60px;
-      border-radius: 50%;
-      margin-bottom: $ios-spacing-md;
-      object-fit: cover;
-      border: 2px solid $ios-border;
-    }
-
-    .member-info {
-      text-align: center;
-
-      .member-name {
-        color: $ios-text-primary;
-        font-size: $ios-font-size-md;
-        font-weight: 600;
-        margin: 0 0 $ios-spacing-xs 0;
-      }
-
-      .member-role {
-        color: $ios-text-secondary;
-        font-size: $ios-font-size-sm;
-        margin: 0 0 $ios-spacing-sm 0;
-      }
-
-      .member-stats {
-        display: flex;
-        justify-content: space-between;
-        margin-top: $ios-spacing-sm;
-
-        .stat-item {
-          text-align: center;
-          flex: 1;
-
-          .stat-value {
-            color: $ios-text-secondary;
-            font-size: $ios-font-size-xs;
-            margin: 0;
-          }
-
-          .stat-label {
-            background: $ios-border;
-            color: white;
-            font-size: $ios-font-size-xs;
-            padding: 2px 6px;
-            border-radius: 10px;
-            margin-top: 2px;
-          }
-
-          &.active .stat-label {
-            background: $ios-primary;
-          }
-        }
-      }
-    }
-
-    .member-actions {
-      margin-top: $ios-spacing-md;
-      display: flex;
-      gap: $ios-spacing-sm;
-
-      .action-btn {
-        flex: 1;
-        padding: $ios-spacing-xs $ios-spacing-sm;
-        border: none;
-        border-radius: $ios-radius-sm;
-        font-size: $ios-font-size-xs;
-        cursor: pointer;
-        transition: all 0.2s ease;
-
-        &.primary {
-          background: $ios-primary;
-          color: white;
-
-          &:hover {
-            opacity: 0.9;
-          }
-        }
-
-        &.secondary {
-          background: $ios-background;
-          border: 1px solid $ios-border;
-          color: $ios-text-primary;
-
-          &:hover {
-            background: $ios-background-secondary;
-          }
-        }
-      }
-    }
-  }
-}
-
-// 垂直导航栏样式
-.vertical-nav-container {
-  .nav-section {
-    margin-bottom: $ios-spacing-md; // 减小节间距
-
-    .section-title {
-      color: $ios-text-secondary;
-      font-size: 10px; // 减小标题字体
-      font-weight: 600;
-      text-transform: uppercase;
-      letter-spacing: 0.3px; // 减小字母间距
-      margin: 0 0 $ios-spacing-xs 0; // 减小下边距
-      padding: 0 $ios-spacing-sm; // 减小内边距
-    }
-
-    .nav-items {
-      list-style: none;
-      padding: 0;
-      margin: 0;
-
-      .nav-item {
-        margin-bottom: 2px; // 减小项间距
-
-        .nav-link {
-          display: flex;
-          align-items: center;
-          padding: $ios-spacing-xs $ios-spacing-sm; // 减小内边距
-          color: $ios-text-primary;
-          text-decoration: none;
-          border-radius: $ios-radius-sm; // 减小圆角
-          transition: all 0.2s ease;
-          font-size: $ios-font-size-xs; // 减小字体
-          line-height: 1.2; // 减小行高
-
-          &:hover {
-            background: $ios-background-secondary;
-          }
-
-          &.active {
-            background: $ios-primary;
-            color: white;
-            font-weight: 500;
-
-            .nav-icon {
-              color: white;
-            }
-          }
-
-          .nav-icon {
-            margin-right: $ios-spacing-xs; // 减小图标间距
-            font-size: 12px; // 减小图标尺寸
-            width: 12px;
-            text-align: center;
-            flex-shrink: 0;
-          }
-
-          .nav-text {
-            flex: 1;
-            white-space: nowrap;
-            overflow: hidden;
-            text-overflow: ellipsis;
-          }
-
-          .nav-badge {
-            background: $ios-border;
-            color: white;
-            font-size: 9px; // 减小徽标字体
-            padding: 1px 4px; // 减小徽标内边距
-            border-radius: 6px; // 减小徽标圆角
-            margin-left: auto;
-            min-width: 14px;
-            text-align: center;
-          }
-
-          &.active .nav-badge {
-            background: rgba(255, 255, 255, 0.2);
-          }
-        }
-      }
-    }
-  }
-}
-
-// 响应式设计
-@media (max-width: 768px) {
-  .members-tab-content {
-    .main-content-layout {
-      grid-template-columns: 1fr;
-      gap: $ios-spacing-md;
-    }
-
-    .members-grid {
-      grid-template-columns: 1fr;
-    }
-  }
-
-  .vertical-nav-container {
-    .nav-section {
-      .nav-items {
-        .nav-item {
-          .nav-link {
-            padding: $ios-spacing-md;
-            font-size: $ios-font-size-md;
-
-            .nav-icon {
-              font-size: 18px;
-              width: 18px;
-            }
-          }
-        }
-      }
-    }
-  }
-}
-
-// 加载状态
-.loading-state {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 200px;
-  color: $ios-text-secondary;
-  font-size: $ios-font-size-sm;
-}
-
-// 空状态
-.empty-state {
-  text-align: center;
-  padding: $ios-spacing-xxl;
-  color: $ios-text-secondary;
-
-  .empty-icon {
-    font-size: 48px;
-    margin-bottom: $ios-spacing-md;
-    opacity: 0.5;
-  }
-
-  .empty-title {
-    font-size: $ios-font-size-lg;
-    font-weight: 600;
-    margin: 0 0 $ios-spacing-sm 0;
-  }
-
-  .empty-description {
-    font-size: $ios-font-size-sm;
-    margin: 0;
-    line-height: 1.5;
-  }
-}

+ 0 - 14
src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav.component.html

@@ -1,14 +0,0 @@
-<div class="horizontal-tab-nav">
-  <div class="tab-container" #tabContainer>
-    @for (item of navItems; track item.id) {
-      <button 
-        class="tab-item"
-        [class.active]="isActive(item.id)"
-        (click)="onTabClick(item.id)"
-        type="button">
-        <span class="tab-text">{{ item.name }}</span>
-      </button>
-    }
-  </div>
-  <div class="tab-indicator" [style.transform]="'translateX(' + getIndicatorPosition() + 'px)'"></div>
-</div>

+ 0 - 148
src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav.component.scss

@@ -1,148 +0,0 @@
-@use '../../../../../shared/styles/_ios-theme.scss' as *;
-
-// 横向滑动条式选项卡导航
-.horizontal-tab-nav {
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-  background: $ios-background;
-  border-radius: $ios-radius-lg;
-  overflow: hidden;
-  box-shadow: $ios-shadow-sm;
-}
-
-.tab-container {
-  display: flex;
-  position: relative;
-  background: rgba(0, 122, 255, 0.05);
-  border-radius: $ios-radius-lg;
-  padding: 4px;
-}
-
-.tab-item {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 12px 16px;
-  border: none;
-  background: transparent;
-  color: $ios-text-secondary;
-  font-size: $ios-font-size-md;
-  font-weight: $ios-font-weight-medium;
-  cursor: pointer;
-  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-  border-radius: $ios-radius-md;
-  position: relative;
-  z-index: 2;
-  min-width: 120px;
-
-  &:hover {
-    color: $ios-primary;
-    background: rgba(0, 122, 255, 0.08);
-  }
-
-  &.active {
-    color: $ios-primary;
-    font-weight: $ios-font-weight-semibold;
-  }
-
-  .tab-text {
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    transition: all 0.3s ease;
-  }
-}
-
-// 滑动指示器
-.tab-indicator {
-  position: absolute;
-  top: 4px;
-  left: 4px;
-  height: calc(100% - 8px);
-  width: 120px;
-  background: $ios-background;
-  border-radius: $ios-radius-md;
-  box-shadow: 0 2px 8px rgba(0, 122, 255, 0.15);
-  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-  z-index: 1;
-}
-
-// 响应式设计
-@media (max-width: 768px) {
-  .horizontal-tab-nav {
-    margin: 0 -8px;
-  }
-  
-  .tab-item {
-    padding: 10px 12px;
-    font-size: $ios-font-size-sm;
-    min-width: 100px;
-  }
-  
-  .tab-indicator {
-    width: 100px;
-  }
-}
-
-@media (max-width: 480px) {
-  .tab-item {
-    padding: 8px 10px;
-    font-size: 13px;
-    min-width: 80px;
-  }
-  
-  .tab-indicator {
-    width: 80px;
-  }
-}
-
-// 保持向后兼容的垂直导航样式(如果需要)
-.vertical-nav {
-  display: flex;
-  flex-direction: column;
-  gap: $ios-spacing-xs;
-  padding: $ios-spacing-sm;
-  background: $ios-background;
-  border-radius: $ios-radius-md;
-  border: 1px solid $ios-border;
-  box-shadow: $ios-shadow-sm;
-  min-width: 160px;
-  max-width: 200px;
-  width: 100%;
-
-  .nav-item {
-    display: flex;
-    align-items: center;
-    padding: $ios-spacing-xs $ios-spacing-sm;
-    border-radius: $ios-radius-sm;
-    cursor: pointer;
-    transition: all 0.2s ease;
-    color: $ios-text-primary;
-    text-decoration: none;
-    font-size: $ios-font-size-sm;
-    font-weight: $ios-font-weight-regular;
-    line-height: 1.2;
-    background: transparent;
-    border: none;
-
-    &:hover {
-      background-color: $ios-background-secondary;
-    }
-
-    &.active {
-      background-color: $ios-primary;
-      color: white;
-      font-weight: $ios-font-weight-medium;
-    }
-
-    .nav-text {
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      flex: 1;
-    }
-  }
-}

+ 0 - 56
src/app/pages/designer/project-detail/components/vertical-nav/vertical-nav.component.ts

@@ -1,56 +0,0 @@
-import { Component, Input, Output, EventEmitter, AfterViewInit, ElementRef, ViewChild } from '@angular/core';
-import { CommonModule } from '@angular/common';
-
-export interface NavItem {
-  id: 'progress' | 'members' | 'files';
-  name: string;
-  icon?: string;
-}
-
-@Component({
-  selector: 'app-vertical-nav',
-  standalone: true,
-  imports: [CommonModule],
-  templateUrl: './vertical-nav.component.html',
-  styleUrls: ['./vertical-nav.component.scss']
-})
-export class VerticalNavComponent implements AfterViewInit {
-  @Input() activeTab: 'progress' | 'members' | 'files' = 'progress';
-  @Input() navItems: NavItem[] = [
-    { id: 'progress', name: '项目进度' },
-    { id: 'members', name: '项目人员' },
-    { id: 'files', name: '项目文件' }
-  ];
-  
-  @Output() tabChange = new EventEmitter<'progress' | 'members' | 'files'>();
-  @ViewChild('tabContainer', { static: false }) tabContainer!: ElementRef;
-
-  private tabWidth = 120; // 默认选项卡宽度
-
-  ngAfterViewInit(): void {
-    // 动态计算选项卡宽度
-    this.calculateTabWidth();
-  }
-
-  onTabClick(tabId: 'progress' | 'members' | 'files'): void {
-    this.tabChange.emit(tabId);
-  }
-
-  isActive(tabId: 'progress' | 'members' | 'files'): boolean {
-    return this.activeTab === tabId;
-  }
-
-  // 计算滑动指示器的位置
-  getIndicatorPosition(): number {
-    const activeIndex = this.navItems.findIndex(item => item.id === this.activeTab);
-    return activeIndex * this.tabWidth;
-  }
-
-  // 动态计算选项卡宽度
-  private calculateTabWidth(): void {
-    if (this.tabContainer) {
-      const containerWidth = this.tabContainer.nativeElement.offsetWidth;
-      this.tabWidth = (containerWidth - 8) / this.navItems.length; // 减去padding
-    }
-  }
-}

+ 477 - 389
src/app/pages/designer/project-detail/project-detail.html

@@ -1,4 +1,4 @@
-<!-- 只展示修改处,未变更部分用占位注释表示 -->
+<!-- 只展示修改处,未变更部分用占位注释表示 -->
 <div class="project-detail-container designer-page">
   <!-- 项目标题栏 -->
   <div class="project-header card">
@@ -16,11 +16,16 @@
     
     <!-- 导航按钮区域 - 移动到标题左侧 -->
     <div class="header-nav">
-      <app-vertical-nav 
-        [activeTab]="activeTab" 
-        (tabChange)="switchTab($event)"
-        class="header-nav-tabs">
-      </app-vertical-nav>
+      <div class="header-nav-tabs">
+        @for (tab of tabs; track tab.id) {
+          <button 
+            class="nav-tab" 
+            [class.active]="activeTab === tab.id"
+            (click)="switchTab(tab.id)">
+            {{ tab.name }}
+          </button>
+        }
+      </div>
     </div>
 
     <!-- 四个环节圆圈导航 -->
@@ -180,6 +185,33 @@
   </div>
 }
 
+<!-- 设计师日历弹窗 -->
+@if (showDesignerCalendar) {
+  <div class="image-preview-modal" (click)="closeDesignerCalendar()">
+    <div class="modal-backdrop"></div>
+    <div class="modal-content" (click)="$event.stopPropagation()">
+      <div class="modal-header">
+        <h3>选择设计师与排期</h3>
+        <button class="close-btn" (click)="closeDesignerCalendar()">
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <line x1="18" y1="6" x2="6" y2="18"></line>
+            <line x1="6" y1="6" x2="18" y2="18"></line>
+          </svg>
+        </button>
+      </div>
+      <div class="modal-body">
+        <app-designer-calendar
+          [designers]="calendarDesigners"
+          [selectedDate]="selectedCalendarDate"
+          [projectGroups]="calendarGroups"
+          (designerSelected)="onCalendarDesignerSelected($event)"
+          (assignmentRequested)="onCalendarAssignmentRequested($event)"
+        ></app-designer-calendar>
+      </div>
+    </div>
+  </div>
+}
+
   <!-- 提醒消息弹窗 -->
   @if (reminderMessage) {
     <div class="reminder-popup">
@@ -593,13 +625,14 @@
                     </div>
                   }
                   
-                  <!-- 小图时间 -->
-                  @if (getEstimatedSmallImageTime()) {
-                    <div class="info-item">
-                      <label>预计时间</label>
-                      <span>{{ getEstimatedSmallImageTime() }}</span>
-                    </div>
-                  }
+                  <!-- 订单金额 -->
+                  <!-- 订单金额 -->
+                  <div class="info-item">
+                    <label>订单金额</label>
+                    <span>¥{{ orderAmount || 0 }}</span>
+                  </div>
+                  
+
                 </div>
                 
                 <!-- 开始分析按钮 -->
@@ -627,7 +660,197 @@
           <div class="right-column">
               <!-- 移除顶部四个圆圈,直接展示阶段内容 -->
               
-              <!-- 串式流程:10个阶段横向排列(保持) -->
+              <!-- 新增:客户信息右侧 2x2 售后模块布局 -->
+              @if (expandedSection === 'aftercare') {
+              <div class="aftercare-grid">
+                
+                <!-- 1/4:尾款结算 -->
+                <div class="aftercare-module settlement-module card">
+                  <div class="module-header">
+                    <h2>💰 尾款结算</h2>
+                    <p class="module-description">技术完成验收后自动发起尾款结算流程,支持多种支付方式自动化处理</p>
+                  </div>
+                  <div class="settlement-automation">
+                    <div class="automation-features">
+                      <div class="feature-card">
+                        <div class="feature-icon">🤖</div>
+                        <div class="feature-content">
+                          <h4>自动触发流程</h4>
+                          <p>技术验收完成后自动发起尾款结算申请</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">🔓</div>
+                        <div class="feature-content">
+                          <h4>自动解密发送</h4>
+                          <p>小程序支付自动解密并发送大图给客户</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">📱</div>
+                        <div class="feature-content">
+                          <h4>凭证智能识别</h4>
+                          <p>上传微信/支付宝截图自动提取金额和支付方式</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">🔔</div>
+                        <div class="feature-content">
+                          <h4>自动通知</h4>
+                          <p>支付完成后自动发送"尾款已到账,大图已解锁"通知</p>
+                        </div>
+                      </div>
+                    </div>
+                    <div class="settlement-status">
+                      <app-settlement-card [settlements]="settlements"></app-settlement-card>
+                    </div>
+                    @if (canEditSection('aftercare')) {
+                      <div class="automation-actions">
+                        <button 
+                          class="primary-btn automation-btn"
+                          (click)="initiateAutoSettlement()"
+                          [disabled]="isAutoSettling">
+                          @if (isAutoSettling) { <span class="loading-spinner"></span> 自动化处理中... } @else { 🚀 启动自动化结算 }
+                        </button>
+                        <button class="secondary-btn" (click)="uploadPaymentProof()"><span class="upload-icon">📎</span> 上传支付凭证</button>
+                      </div>
+                    }
+                  </div>
+                </div>
+                
+                <!-- 2/4:全景图合成 -->
+                <div class="aftercare-module panoramic-module card">
+                  <div class="module-header">
+                    <h2>🖼️ 全景图合成</h2>
+                    <p class="module-description">集成内部全景图合成服务器,技术上传图片自动生成漫游式全景链接</p>
+                  </div>
+                  <div class="panoramic-synthesis">
+                    <div class="panoramic-features">
+                      <div class="feature-card">
+                        <div class="feature-icon">🖥️</div>
+                        <div class="feature-content">
+                          <h4>KR Panel集成</h4>
+                          <p>集成专业全景图合成工具,确保合成质量</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">📸</div>
+                        <div class="feature-content">
+                          <h4>智能空间标注</h4>
+                          <p>技术上传图片并标注空间名称(如"客厅-角度1")</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">🔗</div>
+                        <div class="feature-content">
+                          <h4>自动生成链接</h4>
+                          <p>自动生成漫游式全景链接并发送给客户</p>
+                        </div>
+                      </div>
+                    </div>
+                    <div class="recent-syntheses">
+                      @if (panoramicSyntheses && panoramicSyntheses.length > 0) {
+                        <div class="panoramic-card-list">
+                          @for (item of panoramicSyntheses; track item.id) {
+                            <app-panoramic-synthesis-card
+                              [synthesis]="item"
+                              [showActions]="true">
+                            </app-panoramic-synthesis-card>
+                          }
+                        </div>
+                      } @else {
+                        <div class="desc">暂无最近合成记录</div>
+                      }
+                    </div>
+                    @if (canEditSection('aftercare')) {
+                      <div class="panoramic-actions">
+                        <button class="primary-btn" (click)="startPanoramicSynthesis()">开始合成</button>
+                        <button class="secondary-btn" (click)="viewPanoramicGallery()">查看全景图库</button>
+                      </div>
+                    }
+                  </div>
+                </div>
+                
+                <!-- 3/4:客户评价 -->
+                <div class="aftercare-module review-module card">
+                  <div class="module-header">
+                    <h2>⭐ 客户评价</h2>
+                    <p class="module-description">邀请客户进行多维度评价并生成评价链接</p>
+                  </div>
+                  <div class="customer-review-enhanced">
+                    <div class="review-form-container">
+                      <app-customer-review-form
+                        [projectName]="project?.name || ''"
+                        [designerName]="getCurrentDesignerName()"
+                        (reviewSubmitted)="onReviewSubmitted($event)"
+                        (reviewSaved)="onReviewSaved($event)">
+                      </app-customer-review-form>
+                    </div>
+                    <div class="review-card-container" style="margin-top:12px;">
+                      <app-customer-review-card [feedbacks]="feedbacks"></app-customer-review-card>
+                    </div>
+                    @if (canEditSection('aftercare')) {
+                      <div class="review-actions">
+                        <button class="primary-btn" (click)="confirmCustomerReview()">✅ 确认评价完成</button>
+                        <button class="secondary-btn" (click)="generateReviewLink()">🔗 生成评价链接</button>
+                      </div>
+                    }
+                  </div>
+                </div>
+                
+                <!-- 4/4:投诉处理 -->
+                <div class="aftercare-module complaint-module card">
+                  <div class="module-header">
+                    <h2>📋 投诉处理</h2>
+                    <p class="module-description">支持人工创建和关键词自动抓取投诉,提升处理效率</p>
+                  </div>
+                  <div class="complaint-management-enhanced">
+                    <div class="complaint-features">
+                      <div class="feature-card">
+                        <div class="feature-icon">👥</div>
+                        <div class="feature-content">
+                          <h4>人工创建</h4>
+                          <p>组长或客服人工创建投诉记录</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">🔍</div>
+                        <div class="feature-content">
+                          <h4>关键词抓取</h4>
+                          <p>自动监测企业微信群关键词(不满意、投诉、退款)</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">🏷️</div>
+                        <div class="feature-content">
+                          <h4>智能标注</h4>
+                          <p>自动标注投诉环节和核心问题</p>
+                        </div>
+                      </div>
+                      <div class="feature-card">
+                        <div class="feature-icon">📊</div>
+                        <div class="feature-content">
+                          <h4>实时更新</h4>
+                          <p>处理进度实时更新至系统</p>
+                        </div>
+                      </div>
+                    </div>
+                    <div class="complaint-content">
+                      <app-complaint-card [complaints]="exceptionHistories"></app-complaint-card>
+                    </div>
+                    @if (canEditSection('aftercare')) {
+                      <div class="complaint-actions">
+                        <button class="primary-btn" (click)="createComplaintManually()">📝 人工创建投诉</button>
+                        <button class="secondary-btn" (click)="setupKeywordMonitoring()">⚙️ 设置关键词监测</button>
+                        <button class="primary-btn" (click)="confirmComplaint()">✅ 确认投诉处理完成</button>
+                      </div>
+                    }
+                  </div>
+                </div>
+              </div>
+              }
+               
+               <!-- 串式流程:10个阶段横向排列(保持) -->
               <div class="stage-progress-container">
                 @for (stage of getVisibleStages(); track stage) {
                   <div class="vertical-stage-block" [attr.id]="stageToAnchor(stage)" [class.active]="getStageStatus(stage) === 'active'">
@@ -645,6 +868,24 @@
                           (orderCreated)="onConsultationOrderSubmit($event)"
                           (projectCreated)="onProjectCreated($event)"
                         ></app-consultation-order-panel>
+                        <div class="order-creation-extra" style="margin-top:16px;">
+                          <div class="info-item">
+                            <label>订单金额</label>
+                            <span>¥{{ orderAmount || 0 }}</span>
+                          </div>
+                          <app-quotation-details
+                            [initialData]="quotationData"
+                            (dataChange)="onQuotationDataChange($event)"
+                          ></app-quotation-details>
+                          <div class="designer-assignment-container" style="margin-top:16px;">
+                            <app-designer-assignment
+                              [quotationItems]="quotationData.items"
+                              [initialAssignment]="designerAssignmentData"
+                              (assignmentChange)="onDesignerAssignmentChange($event)"
+                              (designerClick)="onDesignerClick($event)"
+                            ></app-designer-assignment>
+                          </div>
+                        </div>
                       } @else if (stage === '需求沟通') {
                         <app-requirements-confirm-card 
                           (requirementConfirmed)="syncRequirementKeyInfo($event)"
@@ -744,398 +985,244 @@
                             <div class="empty-tip" style="color:#888;">暂无色彩分析结果</div>
                           }
                         </div>
-                      } @else if (stage === '建模' || stage === '软装' || stage === '渲染' || stage === '后期') {
-                        <!-- 横向折叠面板布局 -->
-                        <div class="delivery-execution-panel">
-                          <div class="delivery-processes-container">
-                            @for (process of deliveryProcesses; track process.name) {
-                              @if ((stage === '建模' && process.type === 'modeling') || 
-                                   (stage === '软装' && process.type === 'softDecor') || 
-                                   (stage === '渲染' && process.type === 'rendering') ||
-                                   (stage === '后期' && process.type === 'postProcess')) {
-                                <div class="delivery-process-card" [class.expanded]="process.isExpanded">
-                                  <!-- 流程标题 -->
-                                  <div class="process-header" (click)="toggleProcess(process.id)">
-                                    <h4>{{ process.name }}</h4>
-                                    <div class="expand-icon" [class.rotated]="process.isExpanded">
-                                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                        <polyline points="6,9 12,15 18,9"></polyline>
-                                      </svg>
-                                    </div>
-                                  </div>
-                                  
-                                  <!-- 空间列表 -->
-                                  <div class="spaces-list">
-                                    @for (space of process.spaces; track space.id) {
-                                      <div class="space-item" 
-                                           [class.active]="space.isExpanded"
-                                           (click)="toggleSpace(process.id, space.id)">
-                                        <span class="space-name">{{ space.name }}</span>
-                                        @if (canEditSection('delivery')) {
-                                          <button class="remove-space-btn" 
-                                                  (click)="$event.stopPropagation(); removeSpace(process.id, space.id)"
-                                                  title="删除空间">
-                                            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                      } @else if (stage === '建模') {
+                        <div class="upload-section">
+                          <div class="upload-header">
+                            <h4>上传白模图片</h4>
+                            <span class="hint">支持:JPG/PNG,不强制4K</span>
+                          </div>
+                          @if (canEditSection('delivery')) {
+                            <div class="upload-dropzone" 
+                                 (click)="whiteModelImages.length === 0 ? triggerFileInput('whiteModel') : null"
+                                 (dragover)="whiteModelImages.length === 0 ? onDragOver($event) : null"
+                                 (dragleave)="whiteModelImages.length === 0 ? onDragLeave($event) : null"
+                                 (drop)="whiteModelImages.length === 0 ? onFileDrop($event, 'whiteModel') : null"
+                                 [class.drag-over]="isDragOver && whiteModelImages.length === 0"
+                                 [class.has-images]="whiteModelImages.length > 0">
+                              @if (whiteModelImages.length === 0) {
+                                <div class="upload-icon"></div>
+                                <div class="upload-text">点击此处或拖拽文件到此处上传</div>
+                                <div class="upload-hint">支持 JPG、PNG 格式,单个文件最大 10MB</div>
+                              } @else {
+                                <div class="uploaded-images-grid">
+                                  @for (img of whiteModelImages; track img.id) {
+                                    <div class="uploaded-image-item" (click)="previewImage(img)">
+                                      <img [src]="img.url" [alt]="img.name" />
+                                      <div class="image-overlay">
+                                        <div class="image-name">{{ img.name }}</div>
+                                        <div class="image-actions">
+                                          <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
+                                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                              <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+                                              <circle cx="12" cy="12" r="3"></circle>
+                                            </svg>
+                                          </button>
+                                          <button class="remove-btn" (click)="$event.stopPropagation(); removeWhiteModelImage(img.id)">
+                                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                                               <line x1="18" y1="6" x2="6" y2="18"></line>
                                               <line x1="6" y1="6" x2="18" y2="18"></line>
                                             </svg>
                                           </button>
-                                        }
-                                      </div>
-                                    }
-                                    
-                                    <!-- 添加空间按钮 -->
-                                    @if (canEditSection('delivery')) {
-                                      @if (showAddSpaceInput[process.id]) {
-                                        <div class="add-space-input">
-                                          <input type="text" 
-                                                 [(ngModel)]="newSpaceName[process.id]"
-                                                 placeholder="输入空间名称"
-                                                 (keyup.enter)="addSpace(process.id)"
-                                                 (keyup.escape)="cancelAddSpace(process.id)"
-                                                 #spaceInput>
-                                          <button class="confirm-btn" (click)="addSpace(process.id)">确认</button>
-                                          <button class="cancel-btn" (click)="cancelAddSpace(process.id)">取消</button>
                                         </div>
-                                      } @else {
-                                        <button class="add-space-btn" (click)="showAddSpaceForm(process.id)">
-                                          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                            <line x1="12" y1="5" x2="12" y2="19"></line>
-                                            <line x1="5" y1="12" x2="19" y2="12"></line>
-                                          </svg>
-                                          添加空间
-                                        </button>
-                                      }
-                                    }
-                                  </div>
-                                  
-                                  <!-- 展开的内容区域 -->
-                                  @if (process.isExpanded) {
-                                    <div class="process-content">
-                                      @for (space of process.spaces; track space.id) {
-                                        @if (space.isExpanded) {
-                                          <div class="space-content">
-                                            <div class="space-content-header">
-                                              <h5>{{ space.name }} - {{ process.name }}</h5>
-                                            </div>
-                                            
-                                            @if (process.type === 'modeling') {
-                                              <!-- 建模内容 -->
-                                              <div class="modeling-content">
-                                                <div class="upload-section">
-                                                  <div class="upload-header">
-                                                    <h6>上传白模图片</h6>
-                                                    <span class="hint">支持:JPG/PNG,不强制4K</span>
-                                                  </div>
-                                                  @if (canEditSection('delivery')) {
-                                                    <div class="upload-dropzone" 
-                                                         (click)="!process.content[space.id]?.images || process.content[space.id].images.length === 0 ? triggerSpaceFileInput(process.id, space.id) : null"
-                                                         [class.has-images]="(process.content[space.id]?.images?.length || 0) > 0">
-                                                      @if (!process.content[space.id]?.images || process.content[space.id].images.length === 0) {
-                                                        <div class="upload-icon"></div>
-                                                        <div class="upload-text">点击此处上传白模图片</div>
-                                                        <div class="upload-hint">支持 JPG、PNG 格式,单个文件最大 10MB</div>
-                                                      } @else {
-                                                        <div class="uploaded-images-grid">
-                                                          @for (img of process.content[space.id].images; track img.id) {
-                                                            <div class="uploaded-image-item" (click)="previewImage(img)">
-                                                              <img [src]="img.url" [alt]="img.name" />
-                                                              <div class="image-overlay">
-                                                                <div class="image-name">{{ img.name }}</div>
-                                                                <div class="image-actions">
-                                                                  <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                                                      <circle cx="12" cy="12" r="3"></circle>
-                                                                    </svg>
-                                                                  </button>
-                                                                  <button class="remove-btn" (click)="$event.stopPropagation(); removeSpaceImage(process.id, space.id, img.id)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <line x1="18" y1="6" x2="6" y2="18"></line>
-                                                                      <line x1="6" y1="6" x2="18" y2="18"></line>
-                                                                    </svg>
-                                                                  </button>
-                                                                </div>
-                                                              </div>
-                                                            </div>
-                                                          }
-                                                          <div class="add-more-btn" (click)="triggerSpaceFileInput(process.id, space.id)">
-                                                            <div class="add-icon">+</div>
-                                                            <div class="add-text">添加更多</div>
-                                                          </div>
-                                                        </div>
-                                                      }
-                                                    </div>
-                                                  }
-                                                </div>
-
-                                                
-                                                <!-- 确认上传按钮 -->
-                                                @if (canEditSection('delivery') && (process.content[space.id]?.images?.length || 0) > 0) {
-                                                  <div class="upload-actions">
-                                                    <button class="primary-btn" (click)="confirmWhiteModelUpload()">
-                                                      确认上传
-                                                    </button>
-                                                  </div>
-                                                }
-                                              </div>
-                                            } @else if (process.type === 'softDecor') {
-                                              <!-- 软装内容 -->
-                                              <div class="softdecor-content">
-                                                <div class="upload-section">
-                                                  <div class="upload-header">
-                                                    <h6>上传软装小图</h6>
-                                                    <span class="hint">建议 ≤1MB 的 JPG/PNG 小图</span>
-                                                  </div>
-                                                  @if (canEditSection('delivery')) {
-                                                    <div class="upload-dropzone" 
-                                                         (click)="!process.content[space.id]?.images || process.content[space.id].images.length === 0 ? triggerSpaceFileInput(process.id, space.id) : null"
-                                                         [class.has-images]="(process.content[space.id]?.images?.length || 0) > 0">
-                                                      @if (!process.content[space.id]?.images || process.content[space.id].images.length === 0) {
-                                                        <div class="upload-icon"></div>
-                                                        <div class="upload-text">点击此处上传软装图片</div>
-                                                        <div class="upload-hint">支持 JPG、PNG 格式,建议文件大小 ≤1MB</div>
-                                                      } @else {
-                                                        <div class="uploaded-images-grid">
-                                                          @for (img of process.content[space.id].images; track img.id) {
-                                                            <div class="uploaded-image-item" (click)="previewImage(img)">
-                                                              <img [src]="img.url" [alt]="img.name" />
-                                                              <div class="image-overlay">
-                                                                <div class="image-name">{{ img.name }}</div>
-                                                                <div class="image-actions">
-                                                                  <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                                                      <circle cx="12" cy="12" r="3"></circle>
-                                                                    </svg>
-                                                                  </button>
-                                                                  <button class="remove-btn" (click)="$event.stopPropagation(); removeSpaceImage(process.id, space.id, img.id)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <line x1="18" y1="6" x2="6" y2="18"></line>
-                                                                      <line x1="6" y1="6" x2="18" y2="18"></line>
-                                                                    </svg>
-                                                                  </button>
-                                                                </div>
-                                                              </div>
-                                                            </div>
-                                                          }
-                                                          <div class="add-more-btn" (click)="triggerSpaceFileInput(process.id, space.id)">
-                                                            <div class="add-icon">+</div>
-                                                            <div class="add-text">添加更多</div>
-                                                          </div>
-                                                        </div>
-                                                      }
-                                                    </div>
-                                                  }
-                                                </div>
-                                                
-                                                <!-- 软装阶段确认上传按钮 -->
-                                                @if (canEditSection('delivery') && (process.content[space.id]?.images?.length || 0) > 0) {
-                                                  <div class="upload-actions">
-                                                    <button class="primary-btn" (click)="confirmSoftDecorUpload()">
-                                                      确认上传
-                                                    </button>
-                                                  </div>
-                                                }
-                                              </div>
-                                            } @else if (process.type === 'rendering') {
-                                              <!-- 渲染内容 -->
-                                              <div class="rendering-content">
-                                                <div class="upload-section">
-                                                  <div class="upload-header">
-                                                    <h6>上传渲染图片</h6>
-                                                    <span class="hint">建议 ≥4K 分辨率的 JPG/PNG 图片</span>
-                                                  </div>
-                                                  @if (canEditSection('delivery')) {
-                                                    <div class="upload-dropzone" 
-                                                         (click)="!process.content[space.id]?.images || process.content[space.id].images.length === 0 ? triggerSpaceFileInput(process.id, space.id) : null"
-                                                         [class.has-images]="(process.content[space.id]?.images?.length || 0) > 0">
-                                                      @if (!process.content[space.id]?.images || process.content[space.id].images.length === 0) {
-                                                        <div class="upload-icon"></div>
-                                                        <div class="upload-text">点击此处上传渲染图片</div>
-                                                        <div class="upload-hint">支持 JPG、PNG 格式,建议 4K 分辨率</div>
-                                                      } @else {
-                                                        <div class="uploaded-images-grid">
-                                                          @for (img of process.content[space.id].images; track img.id) {
-                                                            <div class="uploaded-image-item" (click)="previewImage(img)">
-                                                              <img [src]="img.url" [alt]="img.name" />
-                                                              <div class="image-overlay">
-                                                                <div class="image-name">{{ img.name }}</div>
-                                                                <div class="image-actions">
-                                                                  <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                                                      <circle cx="12" cy="12" r="3"></circle>
-                                                                    </svg>
-                                                                  </button>
-                                                                  <button class="remove-btn" (click)="$event.stopPropagation(); removeSpaceImage(process.id, space.id, img.id)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <line x1="18" y1="6" x2="6" y2="18"></line>
-                                                                      <line x1="6" y1="6" x2="18" y2="18"></line>
-                                                                    </svg>
-                                                                  </button>
-                                                                </div>
-                                                              </div>
-                                                            </div>
-                                                          }
-                                                          <div class="add-more-btn" (click)="triggerSpaceFileInput(process.id, space.id)">
-                                                            <div class="add-icon">+</div>
-                                                            <div class="add-text">添加更多</div>
-                                                          </div>
-                                                        </div>
-                                                      }
-                                                    </div>
-                                                  }
-                                                </div>
-                                                
-                                                <div class="render-progress-info">
-                                                  <h6>渲染进度</h6>
-                                                  <div class="progress-details">
-                                                    <div class="progress-item">
-                                                      <span class="label">状态:</span>
-                                                      <span class="value">{{ process.content[space.id]?.status || '待开始' }}</span>
-                                                    </div>
-                                                    <div class="progress-item">
-                                                      <span class="label">完成度:</span>
-                                                      <span class="value">{{ process.content[space.id]?.progress || 0 }}%</span>
-                                                    </div>
-                                                    <div class="progress-item">
-                                                      <span class="label">更新时间:</span>
-                                                      <span class="value">{{ process.content[space.id]?.lastUpdated || '暂无' }}</span>
-                                                    </div>
-                                                  </div>
-                                                  @if (process.content[space.id]?.notes) {
-                                                    <div class="progress-notes">
-                                                      <span class="label">备注:</span>
-                                                      <span class="value">{{ process.content[space.id].notes }}</span>
-                                                    </div>
-                                                  }
-                                                </div>
-                                                
-                                                <!-- 渲染阶段确认上传按钮 -->
-                                                @if (canEditSection('delivery') && (process.content[space.id]?.images?.length || 0) > 0) {
-                                                  <div class="upload-actions">
-                                                    <button class="primary-btn" (click)="confirmRenderUpload()">
-                                                      确认上传
-                                                    </button>
-                                                  </div>
-                                                }
-                                              </div>
-                                            } @else if (process.type === 'postProcess') {
-                                              <!-- 后期内容 -->
-                                              <div class="postprocess-content">
-                                                <div class="upload-section">
-                                                  <div class="upload-header">
-                                                    <h6>上传后期图片</h6>
-                                                    <span class="hint">建议 ≥4K 分辨率的 JPG/PNG 图片</span>
-                                                  </div>
-                                                  @if (canEditSection('delivery')) {
-                                                    <div class="upload-dropzone" 
-                                                         (click)="!process.content[space.id]?.images || process.content[space.id].images.length === 0 ? triggerSpaceFileInput(process.id, space.id) : null"
-                                                         [class.has-images]="(process.content[space.id]?.images?.length || 0) > 0">
-                                                      @if (!process.content[space.id]?.images || process.content[space.id].images.length === 0) {
-                                                        <div class="upload-icon"></div>
-                                                        <div class="upload-text">点击此处上传后期图片</div>
-                                                        <div class="upload-hint">支持 JPG、PNG 格式,建议 4K 分辨率</div>
-                                                      } @else {
-                                                        <div class="uploaded-images-grid">
-                                                          @for (img of process.content[space.id].images; track img.id) {
-                                                            <div class="uploaded-image-item" (click)="previewImage(img)">
-                                                              <img [src]="img.url" [alt]="img.name" />
-                                                              <div class="image-overlay">
-                                                                <div class="image-name">{{ img.name }}</div>
-                                                                <div class="image-actions">
-                                                                  <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
-                                                                      <circle cx="12" cy="12" r="3"></circle>
-                                                                    </svg>
-                                                                  </button>
-                                                                  <button class="remove-btn" (click)="$event.stopPropagation(); removeSpaceImage(process.id, space.id, img.id)">
-                                                                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-                                                                      <line x1="18" y1="6" x2="6" y2="18"></line>
-                                                                      <line x1="6" y1="6" x2="18" y2="18"></line>
-                                                                    </svg>
-                                                                  </button>
-                                                                </div>
-                                                              </div>
-                                                            </div>
-                                                          }
-                                                          <div class="add-more-btn" (click)="triggerSpaceFileInput(process.id, space.id)">
-                                                            <div class="add-icon">+</div>
-                                                            <div class="add-text">添加更多</div>
-                                                          </div>
-                                                        </div>
-                                                      }
-                                                    </div>
-                                                  }
-                                                </div>
-                                                
-                                                <!-- 后期阶段确认上传按钮 -->
-                                                @if (canEditSection('delivery') && (process.content[space.id]?.images?.length || 0) > 0) {
-                                                  <div class="upload-actions">
-                                                    <button class="primary-btn" (click)="confirmPostProcessUpload()">
-                                                      确认上传
-                                                    </button>
-                                                  </div>
-                                                }
-                                              </div>
-                                            }
-                                          </div>
-                                        }
-                                      }
+                                      </div>
                                     </div>
                                   }
+                                  <div class="add-more-btn" (click)="triggerFileInput('whiteModel')">
+                                    <div class="add-icon">+</div>
+                                    <div class="add-text">添加更多</div>
+                                  </div>
                                 </div>
                               }
+                              <input type="file" 
+                                     id="whiteModelFileInput"
+                                     accept="{{allowedImageTypes}}" 
+                                     multiple 
+                                     (change)="onWhiteModelSelected($event)" 
+                                     style="display: none;" />
+                            </div>
+                          }
+                          <div class="upload-actions">
+                            @if (canEditSection('delivery')) {
+                              <button class="primary-btn" [disabled]="whiteModelImages.length===0" (click)="confirmWhiteModelUpload()">确认上传</button>
                             }
+                            @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('white')">同步图片信息</button> }
+                            @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
                           </div>
                         </div>
-                      } @else if (stage === '尾款结算') {
-                        <div class="aftercare-section-container">
-                          <app-settlement-card [settlements]="settlements"></app-settlement-card>
+                        <div class="model-check-section">
+                          <h4>模型差异检查清单</h4>
+                          <div class="checklist">
+                            @for (item of modelCheckItems; track item.id) {
+                              <div class="checklist-item">
+                                <label class="checklist-label">
+                                  <input type="checkbox" class="custom-checkbox" [checked]="item.isPassed" (change)="updateModelCheckItem(item.id, $any($event.target).checked)" [disabled]="!canEditSection('delivery')">
+                                  <span class="checklist-text">{{ item.name }}</span>
+                                </label>
+                                <span class="check-status">{{ item.isPassed ? '已通过' : '待处理' }}</span>
+                              </div>
+                            }
+                          </div>
                         </div>
-                        @if (canEditSection('aftercare')) {
-                          <div class="upload-actions">
-                            <button 
-                              class="primary-btn" 
-                              [class.completed]="isSettlementCompleted"
-                              [disabled]="isConfirmingSettlement"
-                              (click)="confirmSettlement()">
-                              @if (isConfirmingSettlement) {
-                                <span class="loading-spinner"></span>
-                                确认中...
-                              } @else if (isSettlementCompleted) {
-                                <span class="check-icon">✓</span>
-                                已确认完成
-                              } @else {
-                                确认尾款结算完成
+                      } @else if (stage === '软装') {
+                        <div class="softdecor-section">
+                          <h4>软装清单素材</h4>
+                          <div class="upload-section">
+                            <div class="upload-header">
+                              <h4>上传软装小图</h4>
+                              <span class="hint">建议 ≤1MB 的 JPG/PNG 小图</span>
+                            </div>
+                            @if (canEditSection('delivery')) {
+                              <div class="upload-dropzone" 
+                                   (click)="softDecorImages.length === 0 ? triggerFileInput('softDecor') : null"
+                                   (dragover)="softDecorImages.length === 0 ? onDragOver($event) : null"
+                                   (dragleave)="softDecorImages.length === 0 ? onDragLeave($event) : null"
+                                   (drop)="softDecorImages.length === 0 ? onFileDrop($event, 'softDecor') : null"
+                                   [class.drag-over]="isDragOver && softDecorImages.length === 0"
+                                   [class.has-images]="softDecorImages.length > 0">
+                                @if (softDecorImages.length === 0) {
+                                  <div class="upload-icon"></div>
+                                  <div class="upload-text">点击此处或拖拽文件到此处上传</div>
+                                  <div class="upload-hint">支持 JPG、PNG 格式,建议文件大小 ≤1MB</div>
+                                } @else {
+                                  <div class="uploaded-images-grid">
+                                    @for (img of softDecorImages; track img.id) {
+                                      <div class="uploaded-image-item" (click)="previewImage(img)">
+                                        <img [src]="img.url" [alt]="img.name" />
+                                        <div class="image-overlay">
+                                          <div class="image-name">{{ img.name }}</div>
+                                          <div class="image-actions">
+                                            <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
+                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+                                                <circle cx="12" cy="12" r="3"></circle>
+                                              </svg>
+                                            </button>
+                                            <button class="remove-btn" (click)="$event.stopPropagation(); removeSoftDecorImage(img.id)">
+                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                <line x1="18" y1="6" x2="6" y2="18"></line>
+                                                <line x1="6" y1="6" x2="18" y2="18"></line>
+                                              </svg>
+                                            </button>
+                                          </div>
+                                        </div>
+                                      </div>
+                                    }
+                                    <div class="add-more-btn" (click)="triggerFileInput('softDecor')">
+                                      <div class="add-icon">+</div>
+                                      <div class="add-text">添加更多</div>
+                                    </div>
+                                  </div>
+                                }
+                                <input type="file" 
+                                       id="softDecorFileInput"
+                                       accept="{{allowedImageTypes}}" 
+                                       multiple 
+                                       (change)="onSoftDecorSmallPicsSelected($event)" 
+                                       style="display: none;" />
+                              </div>
+                            }
+                            <div class="upload-actions">
+                              @if (canEditSection('delivery')) {
+                                <button class="primary-btn" [disabled]="softDecorImages.length===0" (click)="confirmSoftDecorUpload()">确认上传</button>
                               }
-                            </button>
-                            <button class="secondary-btn" (click)="uploadPaymentProof()">
-                              <span class="upload-icon">📎</span>
-                              上传支付凭证
-                            </button>
+                              @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('soft')">同步图片信息</button> }
+                              @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
+                            </div>
                           </div>
-                        }
-                      } @else if (stage === '客户评价') {
-                        <div class="aftercare-section-container">
-                          <app-customer-review-card [feedbacks]="feedbacks"></app-customer-review-card>
                         </div>
-                        @if (canEditSection('aftercare')) {
-                          <div class="upload-actions">
-                            <button class="primary-btn" (click)="confirmCustomerReview()">确认客户评价完成</button>
+                      } @else if (stage === '渲染') {
+                        <div class="render-progress-section">
+                          @if (isLoadingRenderProgress) {
+                            <div class="loading-state">
+                              <div class="loading-spinner"></div>
+                              <div>正在加载渲染进度...</div>
+                            </div>
+                          }
+                          @if (errorLoadingRenderProgress) {
+                            <div class="error-state">
+                              <div>渲染进度加载失败</div>
+                              <button class="secondary-btn" (click)="retryLoadRenderProgress()">重试</button>
+                            </div>
+                          }
+                          @if (!isLoadingRenderProgress && !errorLoadingRenderProgress && renderProgress) {
+                            <div class="progress-info" style="display:flex;gap:16px;align-items:center;margin:12px 0;">
+                              <span>状态:{{ renderProgress.status }}</span>
+                              <span>完成度:{{ renderProgress.completionRate }}%</span>
+                              <span>预计剩余:{{ renderProgress.estimatedTimeRemaining }} 小时</span>
+                            </div>
+                          }
+                          <div class="upload-section">
+                            <div class="upload-header">
+                              <h4>上传渲染大图</h4>
+                              <span class="hint">需满足4K标准(最长边 ≥ 4000px)</span>
+                            </div>
+                            @if (canEditSection('delivery')) {
+                              <div class="upload-dropzone" 
+                                   (click)="renderLargeImages.length === 0 ? triggerFileInput('render') : null"
+                                   (dragover)="renderLargeImages.length === 0 ? onDragOver($event) : null"
+                                   (dragleave)="renderLargeImages.length === 0 ? onDragLeave($event) : null"
+                                   (drop)="renderLargeImages.length === 0 ? onFileDrop($event, 'render') : null"
+                                   [class.drag-over]="isDragOver && renderLargeImages.length === 0"
+                                   [class.has-images]="renderLargeImages.length > 0">
+                                @if (renderLargeImages.length === 0) {
+                                  <div class="upload-icon"></div>
+                                  <div class="upload-text">点击此处或拖拽文件到此处上传</div>
+                                  <div class="upload-hint">支持 JPG、PNG 格式,需满足4K标准(最长边 ≥ 4000px)</div>
+                                } @else {
+                                  <div class="uploaded-images-grid">
+                                    @for (img of renderLargeImages; track img.id) {
+                                      <div class="uploaded-image-item" (click)="previewImage(img)">
+                                        <img [src]="img.url" [alt]="img.name" />
+                                        <div class="image-overlay">
+                                          <div class="image-name">{{ img.name }}</div>
+                                          <div class="image-actions">
+                                            <button class="preview-btn" (click)="$event.stopPropagation(); previewImage(img)">
+                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+                                                <circle cx="12" cy="12" r="3"></circle>
+                                              </svg>
+                                            </button>
+                                            <button class="remove-btn" (click)="$event.stopPropagation(); removeRenderLargeImage(img.id)">
+                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                <line x1="18" y1="6" x2="6" y2="18"></line>
+                                                <line x1="6" y1="6" x2="18" y2="18"></line>
+                                              </svg>
+                                            </button>
+                                            <button class="download-btn" (click)="$event.stopPropagation(); downloadImage(img)" [disabled]="img.locked">
+                                              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+                                                <path d="M12 3v12m0 0l-4-4m4 4l4-4" stroke-linecap="round" stroke-linejoin="round"></path>
+                                                <path d="M5 21h14a2 2 0 0 0 2-2v-4" stroke-linecap="round" stroke-linejoin="round"></path>
+                                              </svg>
+                                            </button>
+                                          </div>
+                                        </div>
+                                      </div>
+                                    }
+                                    <div class="add-more-btn" (click)="triggerFileInput('render')">
+                                      <div class="add-icon">+</div>
+                                      <div class="add-text">添加更多</div>
+                                    </div>
+                                  </div>
+                                }
+                                <input type="file" 
+                                       id="renderFileInput"
+                                       accept="{{allowedImageTypes}}" 
+                                       multiple 
+                                       (change)="onRenderLargePicsSelected($event)" 
+                                       style="display: none;" />
+                              </div>
+                            }
+                            <div class="upload-actions">
+                              @if (canEditSection('delivery')) {
+                                <button class="primary-btn" [disabled]="renderLargeImages.length===0" (click)="confirmRenderUpload()">确认上传</button>
+                              }
+                              @if (isTeamLeaderView()) { <button class="secondary-btn" (click)="syncUploadedImages('render')">同步图片信息</button> }
+                              @if (!canEditSection('delivery')) { <span class="desc">只读</span> }
+                            </div>
                           </div>
-                        }
-                      } @else if (stage === '投诉处理') {
-                        <div class="aftercare-section-container">
-                          <app-complaint-card [complaints]="exceptionHistories"></app-complaint-card>
                         </div>
-                        @if (canEditSection('aftercare')) {
-                          <div class="upload-actions">
-                            <button class="primary-btn" (click)="confirmComplaint()">确认投诉处理完成</button>
-                          </div>
-                        }
+                      } @else if (stage === '尾款结算') {
+                        <!-- 原尾款结算售后 tab 容器已移除,根据用户要求不再使用 -->
                       }
                     </div>
                   </div>
@@ -1145,7 +1232,8 @@
         </div>
       </div>
     }
-    
+
+
     <!-- 项目人员标签页 -->
     @if (isActiveTab('members')) {
       <div class="members-tab-content">

Разница между файлами не показана из-за своего большого размера
+ 1645 - 1354
src/app/pages/designer/project-detail/project-detail.scss


+ 564 - 189
src/app/pages/designer/project-detail/project-detail.ts

@@ -3,21 +3,29 @@ import { CommonModule } from '@angular/common';
 import { ActivatedRoute, Router } from '@angular/router';
 import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ProjectService } from '../../../services/project.service';
+import { PaymentVoucherRecognitionService } from '../../../services/payment-voucher-recognition.service';
 import {
   Project,
   RenderProgress,
   CustomerFeedback,
   DesignerChange,
   Settlement,
-  ProjectStage
+  ProjectStage,
+  PanoramicSynthesis,
+  ModelCheckItem
 } from '../../../models/project.model';
 import { ConsultationOrderPanelComponent } from '../../../shared/components/consultation-order-panel/consultation-order-panel.component';
 import { RequirementsConfirmCardComponent } from '../../../shared/components/requirements-confirm-card/requirements-confirm-card';
 import { SettlementCardComponent } from '../../../shared/components/settlement-card/settlement-card';
 import { CustomerReviewCardComponent } from '../../../shared/components/customer-review-card/customer-review-card';
+import { CustomerReviewFormComponent } from '../../../shared/components/customer-review-form/customer-review-form';
 import { ComplaintCardComponent } from '../../../shared/components/complaint-card/complaint-card';
+import { PanoramicSynthesisCardComponent } from '../../../shared/components/panoramic-synthesis-card/panoramic-synthesis-card';
+import { QuotationDetailsComponent, QuotationData } from './components/quotation-details/quotation-details.component';
+import { DesignerAssignmentComponent, DesignerAssignmentData, Designer as AssignmentDesigner } from './components/designer-assignment/designer-assignment.component';
+// 引入客户服务模块的设计师日历组件
+import { DesignerCalendarComponent, Designer as CalendarDesigner, ProjectGroup as CalendarProjectGroup } from '../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
 
-import { VerticalNavComponent } from './components/vertical-nav/vertical-nav.component';
 import { ColorAnalysisResult } from '../../../shared/services/color-analysis.service';
 
 interface ExceptionHistory {
@@ -213,7 +221,7 @@ interface DeliveryProcess {
 @Component({
   selector: 'app-project-detail',
   standalone: true,
-  imports: [CommonModule, FormsModule, ReactiveFormsModule, ConsultationOrderPanelComponent, RequirementsConfirmCardComponent, SettlementCardComponent, CustomerReviewCardComponent, ComplaintCardComponent, VerticalNavComponent],
+  imports: [CommonModule, FormsModule, ReactiveFormsModule, ConsultationOrderPanelComponent, RequirementsConfirmCardComponent, SettlementCardComponent, CustomerReviewCardComponent, CustomerReviewFormComponent, ComplaintCardComponent, PanoramicSynthesisCardComponent, QuotationDetailsComponent, DesignerAssignmentComponent, DesignerCalendarComponent],
   templateUrl: './project-detail.html',
   styleUrls: ['./project-detail.scss', './debug-styles.scss']
 })
@@ -238,6 +246,14 @@ export class ProjectDetail implements OnInit, OnDestroy {
   // 新增:尾款结算完成状态
   isSettlementCompleted: boolean = false;
   isConfirmingSettlement: boolean = false;
+  isSettlementInitiated: boolean = false;
+
+  // 新增:自动化功能状态跟踪
+  miniprogramPaymentStatus: 'active' | 'inactive' = 'inactive';
+  voucherRecognitionCount: number = 0;
+  notificationsSent: number = 0;
+  isAutoSettling: boolean = false;
+  isPaymentVerified: boolean = false;
   
   // 客户信息卡片展开/收起状态
   isCustomerInfoExpanded: boolean = false;
@@ -273,8 +289,8 @@ export class ProjectDetail implements OnInit, OnDestroy {
   sections: Array<{ key: SectionKey; label: string; stages: ProjectStage[] }> = [
     { key: 'order', label: '订单创建', stages: ['订单创建'] },
     { key: 'requirements', label: '确认需求', stages: ['需求沟通', '方案确认'] },
-    { key: 'delivery', label: '交付执行', stages: ['建模', '软装', '渲染', '后期'] },
-    { key: 'aftercare', label: '售后', stages: ['尾款结算', '客户评价', '投诉处理'] }
+    { key: 'delivery', label: '交付执行', stages: ['建模', '软装', '渲染'] },
+    { key: 'aftercare', label: '售后', stages: [] }
   ];
   expandedSection: SectionKey | null = null;
   // 渲染异常反馈相关属性
@@ -289,7 +305,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
   
   // 标签页相关
   activeTab: 'progress' | 'members' | 'files' = 'progress';
-  tabs = [
+  tabs: Array<{ id: 'progress' | 'members' | 'files'; name: string }> = [
     { id: 'progress', name: '项目进度' },
     { id: 'members', name: '项目成员' },
     { id: 'files', name: '项目文件' }
@@ -318,7 +334,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
   // 增加审核状态reviewStatus与是否已同步synced标记(仅由组长操作)
   whiteModelImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
   softDecorImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
-  renderLargeImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
+  renderLargeImages: Array<{ id: string; name: string; url: string; size?: string; locked?: boolean; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
   postProcessImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
   showRenderUploadModal: boolean = false;
   pendingRenderLargeItems: Array<{ id: string; name: string; url: string; file: File }> = [];
@@ -326,6 +342,15 @@ export class ProjectDetail implements OnInit, OnDestroy {
   // 视图上下文:根据路由前缀识别角色视角(客服/设计师/组长)
   roleContext: 'customer-service' | 'designer' | 'team-leader' = 'designer';
 
+  // ============ 模型检查项数据 ============
+  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: '' }
+  ];
+
   // ============ 交付执行板块数据 ============
   deliveryProcesses: DeliveryProcess[] = [
     {
@@ -404,6 +429,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
     private router: Router,
     private fb: FormBuilder,
     private cdr: ChangeDetectorRef,
+    private paymentVoucherService: PaymentVoucherRecognitionService,
   ) {}
 
   // 切换标签页
@@ -1662,8 +1688,13 @@ export class ProjectDetail implements OnInit, OnDestroy {
   showImagePreview: boolean = false;
   previewImageData: any = null;
 
-  // 图片预览方法
+  // 图片预览方法(含渲染大图加锁校验)
   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;
   }
@@ -1674,6 +1705,11 @@ export class ProjectDetail implements OnInit, OnDestroy {
   }
 
   downloadImage(img: any): void {
+    const isRenderLarge = !!this.renderLargeImages.find(i => i.id === img?.id);
+    if (isRenderLarge && img?.locked) {
+      alert('该渲染大图已加锁,需完成尾款结算并上传/识别支付凭证后方可下载。');
+      return;
+    }
     if (img) {
       const link = document.createElement('a');
       link.href = img.url;
@@ -1931,7 +1967,8 @@ export class ProjectDetail implements OnInit, OnDestroy {
         id: item.id, 
         name: item.name, 
         url: item.url, 
-        size: this.formatFileSize(f.size) 
+        size: this.formatFileSize(f.size),
+        locked: true 
       });
     }
     input.value = '';
@@ -2541,22 +2578,128 @@ export class ProjectDetail implements OnInit, OnDestroy {
     return Math.round((completedCount / totalCount) * 100);
   }
 
-  // 获取预估小图时间
-  getEstimatedSmallImageTime(): string {
-    // 根据需求复杂度计算预估时间
-    const baseTime = 2; // 基础时间2小时
-    let additionalTime = 0;
+  // 订单金额
+  orderAmount: number = 0;
+  
+  // 报价明细
+  quotationDetails: Array<{
+    id: string;
+    room: string;
+    amount: number;
+    description?: string;
+  }> = [];
+
+  // AI生成报价明细
+  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);
+  }
+
+  // 添加报价明细项
+  addQuotationItem(): void {
+    this.quotationDetails.push({
+      id: `quote_${Date.now()}`,
+      room: '',
+      amount: 0,
+      description: ''
+    });
+  }
+
+  // 删除报价明细项
+  removeQuotationItem(id: string): void {
+    this.quotationDetails = this.quotationDetails.filter(item => item.id !== id);
+    this.updateOrderAmount();
+  }
 
-    // 根据材质复杂度增加时间
-    if (this.requirementKeyInfo.materialWeights.woodRatio > 0) additionalTime += 0.5;
-    if (this.requirementKeyInfo.materialWeights.fabricRatio > 0) additionalTime += 0.5;
-    if (this.requirementKeyInfo.materialWeights.metalRatio > 0) additionalTime += 0.5;
+  // 更新订单总金额
+  updateOrderAmount(): void {
+    this.orderAmount = this.quotationDetails.reduce((total, item) => total + item.amount, 0);
+  }
 
-    // 根据空间复杂度增加时间
-    if (this.requirementKeyInfo.spaceStructure.aspectRatio > 2) additionalTime += 1;
+  // 报价组件数据
+  quotationData: QuotationData = {
+    items: [],
+    totalAmount: 0,
+    materialCost: 0,
+    laborCost: 0,
+    designFee: 0,
+    managementFee: 0
+  };
 
-    const totalTime = baseTime + additionalTime;
-    return `${totalTime}小时`;
+  // 设计师指派数据
+  designerAssignmentData?: DesignerAssignmentData;
+
+  // 设计师日历弹窗状态与数据
+  showDesignerCalendar: boolean = false;
+  selectedCalendarDate: Date = new Date();
+  calendarDesigners: CalendarDesigner[] = [];
+  calendarGroups: CalendarProjectGroup[] = [];
+
+  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;
+  }
+
+  closeDesignerCalendar(): void {
+    this.showDesignerCalendar = false;
+  }
+
+  onCalendarDesignerSelected(designer: CalendarDesigner): void {
+    this.selectedDesigner = designer;
+    this.closeDesignerCalendar();
+  }
+
+  onCalendarAssignmentRequested(designer: CalendarDesigner): void {
+    this.selectedDesigner = designer;
+    this.closeDesignerCalendar();
+  }
+
+  private mapAssignmentDesignerToCalendar(d: AssignmentDesigner): CalendarDesigner {
+    return {
+      id: d.id,
+      name: d.name,
+      avatar: d.avatar,
+      groupId: d.teamId,
+      groupName: d.teamName,
+      isLeader: !!d.isTeamLeader,
+      status: d.status === 'idle' ? 'available' : 'busy',
+      currentProjects: Math.max(0, d.recentOrders || 0),
+      lastOrderDate: d.lastOrderDate,
+      idleDays: Math.max(0, d.idleDays || 0),
+      completedThisMonth: Math.max(0, d.recentOrders || 0),
+      averageCycle: 15,
+      upcomingEvents: (d.reviewDates || []).map((dateStr, idx) => ({
+        id: `${d.id}-review-${idx}`,
+        date: new Date(dateStr),
+        title: '对图评审',
+        type: 'review',
+        projectId: undefined,
+        duration: 2
+      })),
+      workload: Math.max(0, d.workload || 0),
+      nextAvailableDate: new Date()
+    };
   }
 
   // 处理咨询订单表单提交
@@ -2569,11 +2712,14 @@ export class ProjectDetail implements OnInit, OnDestroy {
     // 保存订单创建数据
     this.orderCreationData = formData;
     
-    // 更新projectData以便传递给子组件
+    // 更新projectData以便传递给子组件(集成报价与指派信息)
     this.projectData = {
       customerInfo: formData.customerInfo,
       requirementInfo: formData.requirementInfo,
-      preferenceTags: formData.preferenceTags
+      preferenceTags: formData.preferenceTags,
+      quotation: this.quotationData ? { ...this.quotationData } : undefined,
+      assignment: this.designerAssignmentData ? { ...this.designerAssignmentData } : undefined,
+      orderAmount: this.quotationData?.totalAmount ?? this.orderAmount ?? 0
     };
     
     // 实时更新左侧客户信息显示
@@ -2584,6 +2730,10 @@ export class ProjectDetail implements OnInit, OnDestroy {
       // 客服端创建的订单需要同步到设计师端
       this.syncOrderToDesignerView(formData);
     }
+
+    // 根据报价数据更新订单金额
+    this.orderAmount = this.quotationData?.totalAmount ?? this.orderAmount ?? 0;
+    this.updateOrderAmount();
     
     // 触发变更检测以更新UI
     this.cdr.detectChanges();
@@ -2627,7 +2777,6 @@ export class ProjectDetail implements OnInit, OnDestroy {
           area: formData.requirementInfo.area,
           houseType: formData.requirementInfo.houseType,
           smallImageTime: formData.requirementInfo.smallImageTime,
-          largeImageTime: formData.requirementInfo.largeImageTime,
           spaceRequirements: formData.requirementInfo.spaceRequirements,
           designAngles: formData.requirementInfo.designAngles,
           specialAreaHandling: formData.requirementInfo.specialAreaHandling,
@@ -2652,7 +2801,11 @@ export class ProjectDetail implements OnInit, OnDestroy {
         demandType: formData.customerInfo.demandType,
         preferenceTags: formData.preferenceTags,
         followUpStatus: formData.customerInfo.followUpStatus
-      }
+      },
+      // 新增:报价与指派信息(可选)
+      quotation: this.quotationData ? { ...this.quotationData } : undefined,
+      assignment: this.designerAssignmentData ? { ...this.designerAssignmentData } : undefined,
+      orderAmount: this.quotationData?.totalAmount ?? this.orderAmount ?? 0
     };
 
     // 调用项目服务创建项目
@@ -3207,206 +3360,428 @@ export class ProjectDetail implements OnInit, OnDestroy {
     const uploadingMessage = `正在上传支付凭证:${file.name}...`;
     console.log(uploadingMessage);
     
-    // 模拟上传API调用
+    // 使用支付凭证识别服务处理上传
+    const settlementId = this.project?.id || 'default_settlement';
+    
+    this.paymentVoucherService.processPaymentVoucherUpload(file, settlementId).subscribe({
+      next: (result) => {
+        if (result.success && result.recognitionResult) {
+          const recognition = result.recognitionResult;
+          
+          // 更新识别计数
+          this.voucherRecognitionCount++;
+          
+          // 显示识别结果
+          const successMessage = `
+            支付凭证识别成功!
+            支付方式:${recognition.paymentMethod}
+            支付金额:¥${recognition.amount}
+            交易号:${recognition.transactionNumber}
+            置信度:${(recognition.confidence * 100).toFixed(1)}%
+          `;
+          
+          alert(successMessage);
+          console.log('支付凭证识别完成', recognition);
+          
+          // 自动标记验证通过并解锁渲染大图
+          this.isPaymentVerified = true;
+          this.renderLargeImages = this.renderLargeImages.map(img => ({ ...img, locked: false }));
+          
+          // 触发自动通知流程
+          this.triggerPaymentCompletedNotification(recognition);
+          
+        } else {
+          const errorMessage = `支付凭证识别失败:${result.error || '未知错误'}`;
+          alert(errorMessage);
+          console.error('支付凭证识别失败', result.error);
+        }
+      },
+      error: (error) => {
+        const errorMessage = `支付凭证处理出错:${error.message || '网络错误'}`;
+        alert(errorMessage);
+        console.error('支付凭证处理出错', error);
+      }
+    });
+  }
+
+  /**
+   * 触发支付完成通知流程
+   */
+  private triggerPaymentCompletedNotification(recognition: any): void {
+    console.log('触发支付完成自动通知流程...');
+    
+    // 模拟调用通知服务发送多渠道通知
+    this.sendMultiChannelNotifications(recognition);
+    
+    // 模拟发送通知
     setTimeout(() => {
-      // 模拟成功上传
-      const successMessage = `支付凭证上传成功:${file.name}`;
-      alert(successMessage);
-      console.log('支付凭证上传完成', {
-        fileName: file.name,
-        fileSize: file.size,
-        fileType: file.type,
-        uploadTime: new Date().toISOString()
-      });
-    }, 2000);
+      const notificationMessage = `
+        🎉 尾款已到账,大图已解锁!
+        
+        支付信息:
+        • 支付方式:${recognition.paymentMethod}
+        • 支付金额:¥${recognition.amount}
+        • 处理时间:${new Date().toLocaleString()}
+        
+        📱 系统已自动发送通知至:
+        • 短信通知:138****8888
+        • 微信通知:已推送至微信
+        • 邮件通知:customer@example.com
+        
+        🖼️ 高清渲染图下载链接已发送
+        您现在可以下载4K高清渲染图了!
+      `;
+      
+      alert(notificationMessage);
+      console.log('自动通知发送完成');
+    }, 1000);
+  }
+
+  /**
+   * 发送多渠道通知
+   */
+  private sendMultiChannelNotifications(recognition: any): void {
+    console.log('开始发送多渠道通知...');
+    
+    // 更新通知发送计数
+    this.notificationsSent++;
+    
+    // 模拟发送短信通知
+    console.log('📱 发送短信通知: 尾款已到账,大图已解锁');
+    
+    // 模拟发送微信通知
+    console.log('💬 发送微信通知: 支付成功,高清图片已准备就绪');
+    
+    // 模拟发送邮件通知
+    console.log('📧 发送邮件通知: 包含下载链接的详细通知');
+    
+    // 模拟发送应用内通知
+    console.log('🔔 发送应用内通知: 实时推送支付状态更新');
+    
+    // 模拟通知发送结果
+    setTimeout(() => {
+      console.log('✅ 所有渠道通知发送完成');
+      console.log(`通知发送统计: 短信✅ 微信✅ 邮件✅ 应用内✅ (总计: ${this.notificationsSent} 次)`);
+    }, 500);
+  }
+
+  // 获取当前设计师名称
+  getCurrentDesignerName(): string {
+    // 这里应该从用户服务或认证信息中获取当前用户名称
+    // 暂时返回一个默认值
+    return '张设计师';
   }
 
-  // 横向折叠面板相关方法
+  // ==================== 售后相关变量 ====================
+
+  // 售后标签页控制
+  activeAftercareTab: string = 'services'; // 当前激活的售后标签页
   
-  // 切换流程展开状态(独立控制每个流程)
-  toggleProcess(processId: string): void {
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    if (process) {
-      // 只切换当前点击的流程状态
-      process.isExpanded = !process.isExpanded;
-    }
+  // 售后状态管理
+  afterSalesStage: string = '未开始'; // 售后阶段:未开始、进行中、已完成
+  afterSalesStatus: 'pending' | 'in_progress' | 'completed' | 'cancelled' = 'pending'; // 售后状态
+  afterSalesProgress: number = 0; // 售后进度百分比
+  
+  // 售后服务数据
+  afterSalesServices: Array<{
+    id: string;
+    type: string; // 服务类型:维修、保养、咨询、投诉处理
+    description: string;
+    assignedTo: string; // 指派给的人员
+    scheduledDate?: Date; // 计划服务日期
+    completedDate?: Date; // 完成日期
+    status: 'pending' | 'scheduled' | 'in_progress' | 'completed' | 'cancelled';
+    priority: 'low' | 'medium' | 'high';
+  }> = [];
+
+  // 售后评价和反馈
+  afterSalesRating: number = 0; // 售后评分(0-5)
+  afterSalesFeedback: string = ''; // 售后反馈内容
+  afterSalesFeedbackDate?: Date; // 反馈日期
+  
+  // 售后时间跟踪
+  afterSalesResponseTime?: Date; // 首次响应时间
+  afterSalesStartTime?: Date; // 售后开始时间
+  afterSalesCompletionTime?: Date; // 售后完成时间
+  afterSalesTotalDuration: number = 0; // 总耗时(小时)
+
+  // 售后联系人信息
+  afterSalesContact: {
+    name: string;
+    phone: string;
+    wechat?: string;
+    email?: string;
+  } = {
+    name: '',
+    phone: ''
+  };
+
+  // 售后问题记录
+  afterSalesIssues: Array<{
+    id: string;
+    title: string;
+    description: string;
+    severity: 'low' | 'medium' | 'high' | 'critical';
+    reportedDate: Date;
+    resolvedDate?: Date;
+    status: 'reported' | 'investigating' | 'resolved' | 'closed';
+  }> = [];
+
+  // 售后文件记录
+  afterSalesDocuments: Array<{
+    id: string;
+    name: string;
+    type: string; // 文件类型:合同、报告、照片、视频
+    uploadDate: Date;
+    url: string;
+  }> = [];
+
+  // 售后费用记录
+  afterSalesCosts: Array<{
+    id: string;
+    description: string;
+    amount: number;
+    category: 'material' | 'labor' | 'transportation' | 'other';
+    date: Date;
+    status: 'pending' | 'approved' | 'paid' | 'rejected';
+  }> = [];
+
+  // 售后沟通记录
+  afterSalesCommunications: Array<{
+    id: string;
+    type: 'phone' | 'wechat' | 'email' | 'visit';
+    date: Date;
+    summary: string;
+    participants: string[];
+  }> = [];
+
+  // 切换售后标签页
+  switchAftercareTab(tab: string): void {
+    this.activeAftercareTab = tab;
+    console.log('切换到售后标签页:', tab);
+  }
+
+  // ==================== 自动结算相关 ====================
+  
+  // 启动自动结算
+  initiateAutoSettlement(): void {
+    if (this.isAutoSettling) return;
+    
+    this.isAutoSettling = true;
+    console.log('启动自动化结算流程...');
+    
+    // 模拟启动各个自动化功能
+    setTimeout(() => {
+      // 启动小程序支付监听
+      this.miniprogramPaymentStatus = 'active';
+      this.isSettlementInitiated = true;
+      
+      console.log('✅ 自动化结算已启动');
+      console.log('🟢 小程序支付监听已激活');
+      console.log('🔍 支付凭证智能识别已就绪');
+      console.log('📱 自动通知系统已就绪');
+      
+      this.isAutoSettling = false;
+      
+      // 显示启动成功消息
+      alert(`🚀 自动化结算已成功启动!
+
+✅ 已启动功能:
+• 小程序支付自动监听
+• 支付凭证智能识别  
+• 多渠道自动通知
+• 大图自动解锁
+
+系统将自动处理后续支付流程。`);
+      
+    }, 2000);
   }
 
-  // 切换空间展开状态
-  toggleSpace(processId: string, spaceId: string): void {
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    if (process) {
-      const space = process.spaces.find(s => s.id === spaceId);
-      if (space) {
-        space.isExpanded = !space.isExpanded;
-        // 如果展开,关闭同流程下的其他空间
-        if (space.isExpanded) {
-          process.spaces.forEach(s => {
-            if (s.id !== spaceId) {
-              s.isExpanded = false;
-            }
-          });
+  // ==================== 全景图合成相关 ====================
+  
+  // 全景图合成数据
+  panoramicSyntheses: PanoramicSynthesis[] = [];
+
+  // 启动全景图合成
+  startPanoramicSynthesis(): void {
+    const synthesis: PanoramicSynthesis = {
+      id: 'panoramic-' + Date.now(),
+      projectId: this.projectId,
+      projectName: this.project?.name || '未知项目',
+      spaces: [],
+      status: 'processing',
+      quality: 'high',
+      createdAt: new Date(),
+      updatedAt: new Date()
+    };
+    
+    this.panoramicSyntheses.push(synthesis);
+    console.log('启动全景图合成:', synthesis);
+    
+    // 模拟合成进度
+    const progressInterval = setInterval(() => {
+      const currentSynthesis = this.panoramicSyntheses.find(s => s.id === synthesis.id);
+      if (currentSynthesis) {
+        // 模拟进度更新
+        if (currentSynthesis.status === 'processing') {
+          currentSynthesis.status = 'completed';
+          currentSynthesis.completedAt = new Date();
+          currentSynthesis.updatedAt = new Date();
+          currentSynthesis.previewUrl = 'https://via.placeholder.com/1920x1080';
+          currentSynthesis.downloadUrl = 'https://via.placeholder.com/1920x1080';
+          currentSynthesis.renderTime = 120;
+          currentSynthesis.fileSize = 1024 * 1024 * 50; // 50MB
+          clearInterval(progressInterval);
+          console.log('全景图合成完成:', currentSynthesis);
         }
       }
-    }
+    }, 2000);
   }
 
-  // 显示添加空间表单
-  showAddSpaceForm(processId: string): void {
-    this.showAddSpaceInput[processId] = true;
-    this.newSpaceName[processId] = '';
+  // 查看全景图画廊
+  viewPanoramicGallery(): void {
+    console.log('打开全景图画廊');
+    // 这里可以打开一个模态框或导航到画廊页面
+    alert('全景图画廊功能开发中...');
   }
 
-  // 添加新空间
-  addSpace(processId: string): void {
-    if (!this.newSpaceName[processId]?.trim()) {
-      return;
-    }
+  // ==================== 评价统计相关 ====================
+  
+  // 评价统计数据
+  reviewStats: {
+    overallScore: number;
+    timelinessScore: number;
+    qualityScore: number;
+    communicationScore: number;
+  } = {
+    overallScore: 4.8,
+    timelinessScore: 4.7,
+    qualityScore: 4.9,
+    communicationScore: 4.6
+  };
 
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    if (process) {
-      const newSpace: DeliverySpace = {
-        id: 'space-' + Date.now(),
-        name: this.newSpaceName[processId].trim(),
-        isExpanded: false,
-        order: process.spaces.length + 1
-      };
-      
-      process.spaces.push(newSpace);
-      
-      // 初始化空间内容
-      process.content[newSpace.id] = {
-        images: [],
-        progress: 0,
-        status: 'pending',
-        notes: '',
-        lastUpdated: new Date()
-      };
-      
-      // 重置表单
-      this.newSpaceName[processId] = '';
-      this.showAddSpaceInput[processId] = false;
-    }
+  // ==================== 客户评价相关 ====================
+  
+  // 生成评价链接
+  generateReviewLink(): void {
+    console.log('生成客户评价链接');
+    const reviewLink = `https://review.example.com/project/${this.projectId}`;
+    navigator.clipboard.writeText(reviewLink).then(() => {
+      alert('评价链接已复制到剪贴板!');
+    }).catch(() => {
+      alert(`评价链接:${reviewLink}`);
+    });
   }
 
-  // 取消添加空间
-  cancelAddSpace(processId: string): void {
-    this.showAddSpaceInput[processId] = false;
-    this.newSpaceName[processId] = '';
+  // ==================== 投诉管理相关 ====================
+  
+  // 手动创建投诉
+  createComplaintManually(): void {
+    console.log('手动创建投诉');
+    // 这里可以打开一个模态框来创建投诉
+    alert('手动创建投诉功能开发中...');
   }
 
-  // 删除空间
-  removeSpace(processId: string, spaceId: string): void {
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    if (process) {
-      // 确认删除
-      if (confirm('确定要删除这个空间吗?')) {
-        process.spaces = process.spaces.filter(s => s.id !== spaceId);
-        delete process.content[spaceId];
-      }
-    }
+  // 设置关键词监控
+  setupKeywordMonitoring(): void {
+    console.log('设置关键词监控');
+    // 这里可以打开关键词监控设置界面
+    alert('关键词监控设置功能开发中...');
   }
 
-  // 触发空间文件上传
-  triggerSpaceFileInput(processId: string, spaceId: string): void {
-    const inputId = `space-file-input-${processId}-${spaceId}`;
-    const input = document.getElementById(inputId) as HTMLInputElement;
-    if (input) {
-      input.click();
-    }
+  // 处理评价表单提交
+  onReviewSubmitted(reviewData: any): void {
+    console.log('客户评价已提交:', reviewData);
+    
+    // 这里应该调用API将评价数据保存到服务器
+    // 模拟API调用
+    setTimeout(() => {
+      alert('客户评价提交成功!评价数据已保存。');
+      
+      // 更新本地反馈数据
+      const newFeedback: CustomerFeedback = {
+        id: Date.now().toString(),
+        customerName: '客户', // 应该从项目信息中获取
+        rating: reviewData.overallRating,
+        content: reviewData.improvementSuggestions || '无具体建议',
+        createdAt: new Date(),
+        status: '已解决',
+        isSatisfied: reviewData.overallRating >= 4,
+        projectId: this.projectId
+      };
+      
+      this.feedbacks = [...this.feedbacks, newFeedback];
+      
+      // 自动标记客户评价完成
+      this.confirmCustomerReview();
+    }, 1000);
+  }
+
+  // 处理评价表单保存草稿
+  onReviewSaved(reviewData: any): void {
+    console.log('客户评价草稿已保存:', reviewData);
+    
+    // 这里应该调用API将草稿数据保存到服务器
+    // 模拟API调用
+    setTimeout(() => {
+      alert('评价草稿保存成功!您可以稍后继续完善。');
+    }, 500);
   }
 
+  // ============ 缺少的方法实现 ============
+  
   // 处理空间文件选择
   onSpaceFileSelected(event: Event, processId: string, spaceId: string): void {
     const input = event.target as HTMLInputElement;
-    const files = input.files;
-    
-    if (files && files.length > 0) {
-      const process = this.deliveryProcesses.find(p => p.id === processId);
-      if (process && process.content[spaceId]) {
-        Array.from(files).forEach(file => {
-          // 验证文件类型
-          if (!file.type.startsWith('image/')) {
-            alert(`文件 ${file.name} 不是有效的图片格式`);
-            return;
-          }
-          
-          // 验证文件大小(限制为10MB)
-          if (file.size > 10 * 1024 * 1024) {
-            alert(`文件 ${file.name} 大小超过10MB限制`);
-            return;
-          }
-          
-          const imageItem = {
-            id: 'img-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
-            name: file.name,
-            url: URL.createObjectURL(file),
-            size: this.formatFileSize(file.size),
-            reviewStatus: 'pending' as const
-          };
-          
-          process.content[spaceId].images.push(imageItem);
-          process.content[spaceId].lastUpdated = new Date();
+    if (!input.files || input.files.length === 0) return;
+    
+    const files = Array.from(input.files);
+    const process = this.deliveryProcesses.find(p => p.id === processId);
+    if (!process || !process.content[spaceId]) return;
+    
+    files.forEach(file => {
+      if (/\.(jpg|jpeg|png)$/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)
         });
-        
-        // 触发变更检测以更新界面
-        this.cdr.detectChanges();
       }
-    }
+    });
     
-    // 重置input
+    // 清空输入
     input.value = '';
   }
 
+
+
+  // 更新模型检查项状态
+  updateModelCheckItem(itemId: string, isPassed: boolean): void {
+    const item = this.modelCheckItems.find(i => i.id === itemId);
+    if (item) {
+      item.isPassed = isPassed;
+      console.log(`模型检查项 ${item.name} 状态更新为: ${isPassed ? '已通过' : '待处理'}`);
+    }
+  }
+
   // 删除空间图片
   removeSpaceImage(processId: string, spaceId: string, imageId: string): void {
     const process = this.deliveryProcesses.find(p => p.id === processId);
     if (process && process.content[spaceId]) {
-      const imageIndex = process.content[spaceId].images.findIndex(img => img.id === imageId);
+      const images = process.content[spaceId].images;
+      const imageIndex = images.findIndex(img => img.id === imageId);
       if (imageIndex > -1) {
-        const image = process.content[spaceId].images[imageIndex];
-        // 释放URL对象
-        URL.revokeObjectURL(image.url);
+        // 释放URL资源
+        const image = images[imageIndex];
+        if (image.url && image.url.startsWith('blob:')) {
+          URL.revokeObjectURL(image.url);
+        }
         // 从数组中移除
-        process.content[spaceId].images.splice(imageIndex, 1);
-        process.content[spaceId].lastUpdated = new Date();
+        images.splice(imageIndex, 1);
+        console.log(`已删除空间图片: ${processId}/${spaceId}/${imageId}`);
       }
     }
   }
-
-  // 获取流程状态文本
-  getProcessStatusText(processId: string): string {
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    if (!process) return '';
-    
-    const totalSpaces = process.spaces.length;
-    const completedSpaces = process.spaces.filter(space => {
-      const content = process.content[space.id];
-      return content && content.status === 'completed';
-    }).length;
-    
-    if (completedSpaces === 0) return '未开始';
-    if (completedSpaces === totalSpaces) return '已完成';
-    return `进行中 (${completedSpaces}/${totalSpaces})`;
-  }
-
-  // 获取流程进度百分比
-  getProcessProgress(processId: string): number {
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    if (!process || process.spaces.length === 0) return 0;
-    
-    const totalSpaces = process.spaces.length;
-    const completedSpaces = process.spaces.filter(space => {
-      const content = process.content[space.id];
-      return content && content.status === 'completed';
-    }).length;
-    
-    return Math.round((completedSpaces / totalSpaces) * 100);
-  }
-
-  // 获取空间内容
-  getSpaceContent(processId: string, spaceId: string): any {
-    const process = this.deliveryProcesses.find(p => p.id === processId);
-    return process?.content[spaceId] || null;
-  }
 }

+ 88 - 0
src/app/pages/finance/dashboard/dashboard.html

@@ -19,10 +19,98 @@
       <div class="action-icon generate-report">📊</div>
       <div class="action-label">生成日接单表</div>
     </button>
+    <button class="action-btn" (click)="handleQuickAction('quotationApproval')">
+      <div class="action-icon quotation-approval">✅</div>
+      <div class="action-label">报价审核</div>
+    </button>
+    <button class="action-btn work-hour-btn" (click)="toggleWorkHourModule()">
+      <div class="action-icon work-hour">⏱️</div>
+      <div class="action-label">工时统计</div>
+    </button>
   </div>
 
   <!-- 主内容区域 -->
   <div class="dashboard-content">
+    <!-- 工时统计模块 -->
+    <div class="work-hour-module" *ngIf="showWorkHourModule()">
+      <div class="section-header">
+        <h2>有效工时统计</h2>
+        <button class="close-btn" (click)="toggleWorkHourModule()">×</button>
+      </div>
+      
+      <!-- 工时概览卡片 -->
+      <div class="work-hour-overview" *ngIf="workHourDashboard()">
+        <div class="overview-card">
+          <div class="card-icon">👥</div>
+          <div class="card-content">
+            <div class="card-value">{{ workHourDashboard()!.totalDesigners }}</div>
+            <div class="card-label">设计师总数</div>
+          </div>
+        </div>
+        
+        <div class="overview-card">
+          <div class="card-icon">📋</div>
+          <div class="card-content">
+            <div class="card-value">{{ workHourDashboard()!.totalProjects }}</div>
+            <div class="card-label">项目总数</div>
+          </div>
+        </div>
+        
+        <div class="overview-card">
+          <div class="card-icon">⏰</div>
+          <div class="card-content">
+            <div class="card-value">{{ formatWorkHours(workHourDashboard()!.averageEffectiveHours) }}</div>
+            <div class="card-label">平均有效工时</div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 绩效等级分布 -->
+      <div class="performance-distribution" *ngIf="workHourDashboard()">
+        <h3>绩效等级分布</h3>
+        <div class="performance-chart">
+          <div class="performance-item" *ngFor="let level of ['S', 'A', 'B', 'C']">
+            <div class="level-indicator" [style.background-color]="getPerformanceLevelColor(level)">
+              {{ level }}
+            </div>
+            <div class="level-info">
+              <div class="level-count">{{ getPerformanceLevelCount(level) }}人</div>
+              <div class="level-desc">{{ getPerformanceLevelDescription(level) }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 月度统计表格 -->
+      <div class="monthly-stats" *ngIf="monthlyStats().length > 0">
+        <h3>设计师月度统计</h3>
+        <div class="stats-table">
+          <div class="table-header">
+            <div class="header-cell">设计师</div>
+            <div class="header-cell">实际工时</div>
+            <div class="header-cell">有效工时</div>
+            <div class="header-cell">完成项目</div>
+            <div class="header-cell">客户满意度</div>
+            <div class="header-cell">绩效等级</div>
+          </div>
+          <div class="table-body">
+            <div class="table-row" *ngFor="let stat of monthlyStats()">
+              <div class="table-cell">{{ stat.designerName }}</div>
+              <div class="table-cell">{{ formatWorkHours(stat.totalActualHours) }}</div>
+              <div class="table-cell">{{ formatWorkHours(stat.totalEffectiveHours) }}</div>
+              <div class="table-cell">{{ stat.completedProjects }}个</div>
+              <div class="table-cell">{{ stat.averageCustomerSatisfaction.toFixed(1) }}分</div>
+              <div class="table-cell">
+                <span class="performance-badge" [style.background-color]="getPerformanceLevelColor(stat.performanceLevel)">
+                  {{ stat.performanceLevel }}
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <!-- 数据概览卡片 -->
     <div class="stats-cards">
       <div class="stat-card">

+ 230 - 0
src/app/pages/finance/dashboard/dashboard.scss

@@ -318,4 +318,234 @@
 
 .action-btn:nth-child(3) {
   animation-delay: 0.2s;
+}
+
+.action-btn:nth-child(4) {
+  animation-delay: 0.3s;
+}
+
+/* 工时统计模块样式 */
+.work-hour-module {
+  background-color: #ffffff;
+  border-radius: 16px;
+  padding: 24px;
+  margin-bottom: 24px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
+  animation: slideInUp 0.5s ease-out;
+}
+
+.work-hour-module .section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.work-hour-module h2 {
+  font-size: 20px;
+  font-weight: 600;
+  color: #1c1c1e;
+  margin: 0;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 24px;
+  color: #8e8e93;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 8px;
+  transition: background-color 0.2s ease;
+}
+
+.close-btn:hover {
+  background-color: #f2f2f7;
+}
+
+/* 工时概览卡片 */
+.work-hour-overview {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.overview-card {
+  display: flex;
+  align-items: center;
+  padding: 16px;
+  background-color: #f8f9fa;
+  border-radius: 12px;
+  border: 1px solid #e9ecef;
+}
+
+.card-icon {
+  font-size: 24px;
+  margin-right: 12px;
+}
+
+.card-content {
+  flex: 1;
+}
+
+.card-value {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1c1c1e;
+  margin-bottom: 4px;
+}
+
+.card-label {
+  font-size: 12px;
+  color: #6c6c70;
+}
+
+/* 绩效等级分布 */
+.performance-distribution {
+  margin-bottom: 24px;
+}
+
+.performance-distribution h3 {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1c1c1e;
+  margin-bottom: 16px;
+}
+
+.performance-chart {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+  gap: 12px;
+}
+
+.performance-item {
+  display: flex;
+  align-items: center;
+  padding: 12px;
+  background-color: #f8f9fa;
+  border-radius: 8px;
+}
+
+.level-indicator {
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-weight: 600;
+  font-size: 14px;
+  margin-right: 12px;
+}
+
+.level-info {
+  flex: 1;
+}
+
+.level-count {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1c1c1e;
+  margin-bottom: 2px;
+}
+
+.level-desc {
+  font-size: 12px;
+  color: #6c6c70;
+}
+
+/* 月度统计表格 */
+.monthly-stats h3 {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1c1c1e;
+  margin-bottom: 16px;
+}
+
+.stats-table {
+  background-color: #f8f9fa;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.table-header {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
+  background-color: #e9ecef;
+  padding: 12px 0;
+}
+
+.header-cell {
+  padding: 0 16px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #495057;
+  text-align: center;
+}
+
+.table-body {
+  max-height: 300px;
+  overflow-y: auto;
+}
+
+.table-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
+  padding: 12px 0;
+  border-bottom: 1px solid #dee2e6;
+}
+
+.table-row:last-child {
+  border-bottom: none;
+}
+
+.table-cell {
+  padding: 0 16px;
+  font-size: 14px;
+  color: #495057;
+  text-align: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.performance-badge {
+  padding: 4px 8px;
+  border-radius: 12px;
+  color: white;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+/* 工时按钮特殊样式 */
+.work-hour-btn {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.work-hour-btn .action-icon {
+  color: white;
+}
+
+.work-hour-btn .action-label {
+  color: white;
+}
+
+.work-hour-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
+}
+
+/* 动画效果 */
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
 }

+ 76 - 2
src/app/pages/finance/dashboard/dashboard.ts

@@ -1,7 +1,9 @@
 import { Component, OnInit, signal } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { RouterModule } from '@angular/router';
+import { RouterModule, Router } from '@angular/router';
 import { AuthService } from '../../../services/auth.service';
+import { WorkHourService } from '../../../services/work-hour.service';
+import { WorkHourDashboardData, MonthlyWorkHourStats, PerformanceLevel } from '../../../models/work-hour.model';
 
 @Component({
   selector: 'app-dashboard',
@@ -33,11 +35,19 @@ export class Dashboard implements OnInit {
   // 用户角色
   userRole = signal('teamLead'); // teamLead 或 juniorMember
 
-  constructor(private authService: AuthService) {}
+  // 工时统计数据
+  workHourDashboard = signal<WorkHourDashboardData | null>(null);
+  monthlyStats = signal<MonthlyWorkHourStats[]>([]);
+  showWorkHourModule = signal(false);
+
+  constructor(private authService: AuthService, private workHourService: WorkHourService, private router: Router) {}
 
   ngOnInit(): void {
     // 初始化用户角色
     this.initializeUserRole();
+    
+    // 加载工时统计数据
+    this.loadWorkHourData();
   }
   
   // 初始化用户角色
@@ -114,6 +124,9 @@ export class Dashboard implements OnInit {
       case 'generateReport':
         window.location.href = '/finance/reports';
         break;
+      case 'quotationApproval':
+        this.router.navigate(['/finance/quotation-approval']);
+        break;
     }
   }
 
@@ -121,4 +134,65 @@ export class Dashboard implements OnInit {
   formatAmount(amount: number): string {
     return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
   }
+
+  // 加载工时统计数据
+  loadWorkHourData(): void {
+    this.workHourService.getDashboardData().subscribe(data => {
+      this.workHourDashboard.set(data);
+    });
+
+    this.workHourService.getMonthlyStats().subscribe(stats => {
+      this.monthlyStats.set(stats);
+    });
+  }
+
+  // 切换工时模块显示
+  toggleWorkHourModule(): void {
+    this.showWorkHourModule.set(!this.showWorkHourModule());
+  }
+
+  // 获取绩效等级颜色
+  getPerformanceLevelColor(level: string): string {
+    const colors: Record<string, string> = {
+      'S': '#ff6b6b',
+      'A': '#4ecdc4', 
+      'B': '#45b7d1',
+      'C': '#96ceb4'
+    };
+    return colors[level] || '#ccc';
+  }
+
+  // 获取绩效等级人数
+  getPerformanceLevelCount(level: string): number {
+    const dashboard = this.workHourDashboard();
+    if (!dashboard) return 0;
+    
+    const distribution = dashboard.performanceDistribution as Record<string, number>;
+    return distribution[level] || 0;
+  }
+
+  // 获取绩效等级描述
+  getPerformanceLevelDescription(level: string): string {
+    const descriptions: Record<string, string> = {
+      'S': '卓越表现',
+      'A': '优秀表现',
+      'B': '良好表现', 
+      'C': '待提升'
+    };
+    return descriptions[level] || '未知';
+  }
+
+  // 格式化工时显示
+  formatWorkHours(hours: number): string {
+    const days = Math.floor(hours / 8);
+    const remainingHours = hours % 8;
+    
+    if (days > 0 && remainingHours > 0) {
+      return `${days}天${remainingHours}小时`;
+    } else if (days > 0) {
+      return `${days}天`;
+    } else {
+      return `${remainingHours}小时`;
+    }
+  }
 }

+ 303 - 0
src/app/pages/finance/quotation-approval/quotation-approval.component.html

@@ -0,0 +1,303 @@
+<div class="quotation-approval-container">
+  <!-- 页面头部 -->
+  <div class="page-header">
+    <div class="header-content">
+      <h2>报价审核管理</h2>
+      <p class="header-description">管理和审核设计师提交的项目报价</p>
+    </div>
+    <div class="header-actions">
+      <button type="button" class="btn-secondary" (click)="showStatistics()">
+        <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M2 14V10M8 14V6M14 14V2M2 10L8 6L14 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+        统计报表
+      </button>
+    </div>
+  </div>
+
+  <!-- 统计卡片 -->
+  <div class="stats-cards">
+    <div class="stat-card">
+      <div class="stat-icon pending">
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
+          <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-value">{{ approvalStats.pending }}</div>
+        <div class="stat-label">待审核</div>
+      </div>
+    </div>
+    
+    <div class="stat-card">
+      <div class="stat-icon approved">
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-value">{{ approvalStats.approved }}</div>
+        <div class="stat-label">已通过</div>
+      </div>
+    </div>
+    
+    <div class="stat-card">
+      <div class="stat-icon rejected">
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
+          <line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" stroke-width="2"/>
+          <line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" stroke-width="2"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-value">{{ approvalStats.rejected }}</div>
+        <div class="stat-label">已拒绝</div>
+      </div>
+    </div>
+    
+    <div class="stat-card">
+      <div class="stat-icon revision">
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+          <path d="M3 3v5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+        </svg>
+      </div>
+      <div class="stat-content">
+        <div class="stat-value">{{ approvalStats.revisionRequired }}</div>
+        <div class="stat-label">需修改</div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 筛选区域 -->
+  <div class="filter-section">
+    <div class="filter-row">
+      <div class="filter-group">
+        <label class="filter-label">状态筛选</label>
+        <select [(ngModel)]="statusFilter" (ngModelChange)="applyFilters()" class="filter-select">
+          <option value="all">全部状态</option>
+          <option value="pending">待审核</option>
+          <option value="approved">已通过</option>
+          <option value="rejected">已拒绝</option>
+          <option value="revision_required">需修改</option>
+        </select>
+      </div>
+      
+      <div class="filter-group">
+        <label class="filter-label">关键词搜索</label>
+        <input type="text" [(ngModel)]="searchKeyword" (ngModelChange)="applyFilters()" 
+               class="filter-input" placeholder="搜索项目名称、客户名称或提交人">
+      </div>
+      
+      <div class="filter-group">
+        <label class="filter-label">提交日期</label>
+        <div class="date-range">
+          <input type="date" [(ngModel)]="dateRange.start" (ngModelChange)="applyFilters()" class="filter-input">
+          <span class="date-separator">至</span>
+          <input type="date" [(ngModel)]="dateRange.end" (ngModelChange)="applyFilters()" class="filter-input">
+        </div>
+      </div>
+      
+      <div class="filter-actions">
+        <button type="button" class="btn-secondary" (click)="resetFilters()">重置</button>
+      </div>
+    </div>
+  </div>
+
+  <!-- 审核列表 -->
+  <div class="approval-list">
+    <div class="list-header">
+      <h3>审核列表 ({{ filteredApprovals.length }})</h3>
+    </div>
+    
+    @if (filteredApprovals.length === 0) {
+      <div class="empty-state">
+        <div class="empty-icon">
+          <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <path d="M24 4L28.5 18H42L32 26.5L36.5 40L24 32L11.5 40L16 26.5L6 18H19.5L24 4Z" stroke="#D1D5DB" stroke-width="2" fill="none"/>
+          </svg>
+        </div>
+        <p>暂无符合条件的审核项目</p>
+        <p class="empty-hint">调整筛选条件或等待新的报价提交</p>
+      </div>
+    } @else {
+      <div class="approval-table">
+        <div class="table-header">
+          <div class="table-cell">项目信息</div>
+          <div class="table-cell">客户信息</div>
+          <div class="table-cell">报价金额</div>
+          <div class="table-cell">提交时间</div>
+          <div class="table-cell">状态</div>
+          <div class="table-cell">操作</div>
+        </div>
+        
+        @for (approval of filteredApprovals; track approval.quotationId) {
+          <div class="table-row">
+            <div class="table-cell">
+              <div class="project-info">
+                <div class="project-name">{{ approval.projectName }}</div>
+                <div class="project-id">ID: {{ approval.quotationId }}</div>
+              </div>
+            </div>
+            <div class="table-cell">
+              <div class="customer-info">
+                <div class="customer-name">{{ approval.customerName }}</div>
+                <div class="submitted-by">提交人: {{ approval.submittedBy }}</div>
+              </div>
+            </div>
+            <div class="table-cell">
+              <div class="amount">¥{{ formatAmount(approval.totalAmount) }}</div>
+            </div>
+            <div class="table-cell">
+              <div class="date">{{ formatDate(approval.submittedAt) }}</div>
+            </div>
+            <div class="table-cell">
+              <span class="status-badge" [class]="getStatusClass(approval.status)">
+                {{ getStatusLabel(approval.status) }}
+              </span>
+            </div>
+            <div class="table-cell">
+              <button type="button" class="btn-primary btn-sm" (click)="viewApprovalDetails(approval)">
+                审核
+              </button>
+            </div>
+          </div>
+        }
+      </div>
+    }
+  </div>
+
+  <!-- 审核详情模态框 -->
+  @if (showApprovalModal && selectedApproval) {
+    <div class="modal-overlay" (click)="closeApprovalModal()">
+      <div class="modal-content approval-modal" (click)="$event.stopPropagation()">
+        <div class="modal-header">
+          <h4>报价审核 - {{ selectedApproval.projectName }}</h4>
+          <button type="button" class="modal-close" (click)="closeApprovalModal()">
+            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+            </svg>
+          </button>
+        </div>
+        
+        <div class="modal-body">
+          <div class="approval-details">
+            <div class="detail-section">
+              <h5>基本信息</h5>
+              <div class="detail-grid">
+                <div class="detail-item">
+                  <label>项目名称</label>
+                  <span>{{ selectedApproval.projectName }}</span>
+                </div>
+                <div class="detail-item">
+                  <label>客户名称</label>
+                  <span>{{ selectedApproval.customerName }}</span>
+                </div>
+                <div class="detail-item">
+                  <label>提交人</label>
+                  <span>{{ selectedApproval.submittedBy }}</span>
+                </div>
+                <div class="detail-item">
+                  <label>报价金额</label>
+                  <span class="amount-highlight">¥{{ formatAmount(selectedApproval.totalAmount) }}</span>
+                </div>
+                <div class="detail-item">
+                  <label>提交时间</label>
+                  <span>{{ formatDate(selectedApproval.submittedAt) }}</span>
+                </div>
+                <div class="detail-item">
+                  <label>当前状态</label>
+                  <span class="status-badge" [class]="getStatusClass(selectedApproval.status)">
+                    {{ getStatusLabel(selectedApproval.status) }}
+                  </span>
+                </div>
+              </div>
+            </div>
+            
+            @if (selectedApproval.scriptUsed) {
+              <div class="detail-section">
+                <h5>使用话术</h5>
+                <div class="script-info">
+                  <span class="script-id">话术ID: {{ selectedApproval.scriptUsed }}</span>
+                </div>
+              </div>
+            }
+            
+            <div class="detail-section">
+              <h5>审核意见</h5>
+              <textarea [(ngModel)]="newComment" class="comment-textarea" 
+                        placeholder="请输入审核意见..." rows="4"></textarea>
+            </div>
+          </div>
+        </div>
+        
+        <div class="modal-footer">
+          <button type="button" class="btn-secondary" (click)="closeApprovalModal()">取消</button>
+          @if (selectedApproval.status === 'pending') {
+            <button type="button" class="btn-warning" (click)="requestRevision()" 
+                    [disabled]="!newComment.trim()">要求修改</button>
+            <button type="button" class="btn-danger" (click)="rejectQuotation()" 
+                    [disabled]="!newComment.trim()">拒绝</button>
+            <button type="button" class="btn-success" (click)="approveQuotation()">通过</button>
+          }
+        </div>
+      </div>
+    </div>
+  }
+
+  <!-- 统计报表模态框 -->
+  @if (showStatsModal) {
+    <div class="modal-overlay" (click)="closeStatsModal()">
+      <div class="modal-content stats-modal" (click)="$event.stopPropagation()">
+        <div class="modal-header">
+          <h4>话术使用统计</h4>
+          <button type="button" class="modal-close" (click)="closeStatsModal()">
+            <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
+            </svg>
+          </button>
+        </div>
+        
+        <div class="modal-body">
+          <div class="stats-content">
+            <div class="stats-summary">
+              <div class="summary-item">
+                <label>平均审核时间</label>
+                <span class="summary-value">{{ approvalStats.averageApprovalTime }} 小时</span>
+              </div>
+              <div class="summary-item">
+                <label>总审核数量</label>
+                <span class="summary-value">{{ approvalStats.total }}</span>
+              </div>
+            </div>
+            
+            @if (scriptUsageStats.length > 0) {
+              <div class="script-stats">
+                <h5>话术使用排行</h5>
+                <div class="stats-list">
+                  @for (stat of scriptUsageStats; track stat.scriptId) {
+                    <div class="stat-item">
+                      <div class="stat-info">
+                        <span class="script-title">{{ stat.scriptTitle }}</span>
+                        <span class="usage-count">使用 {{ stat.usageCount }} 次</span>
+                      </div>
+                      <div class="stat-bar">
+                        <div class="bar-fill" [style.width.%]="(stat.usageCount / scriptUsageStats[0].usageCount) * 100"></div>
+                      </div>
+                    </div>
+                  }
+                </div>
+              </div>
+            }
+          </div>
+        </div>
+        
+        <div class="modal-footer">
+          <button type="button" class="btn-secondary" (click)="closeStatsModal()">关闭</button>
+        </div>
+      </div>
+    </div>
+  }
+</div>

+ 582 - 0
src/app/pages/finance/quotation-approval/quotation-approval.component.scss

@@ -0,0 +1,582 @@
+.quotation-approval-container {
+  padding: 24px;
+  background-color: #F9FAFB;
+  min-height: 100vh;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 24px;
+  
+  .header-content {
+    h2 {
+      margin: 0 0 8px 0;
+      font-size: 28px;
+      font-weight: 600;
+      color: #111827;
+    }
+    
+    .header-description {
+      margin: 0;
+      color: #6B7280;
+      font-size: 16px;
+    }
+  }
+  
+  .header-actions {
+    display: flex;
+    gap: 12px;
+  }
+}
+
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+  gap: 20px;
+  margin-bottom: 32px;
+}
+
+.stat-card {
+  background: white;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  
+  .stat-icon {
+    width: 48px;
+    height: 48px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    
+    &.pending {
+      background-color: #FEF3C7;
+      color: #D97706;
+    }
+    
+    &.approved {
+      background-color: #D1FAE5;
+      color: #059669;
+    }
+    
+    &.rejected {
+      background-color: #FEE2E2;
+      color: #DC2626;
+    }
+    
+    &.revision {
+      background-color: #E0E7FF;
+      color: #5B21B6;
+    }
+  }
+  
+  .stat-content {
+    .stat-value {
+      font-size: 32px;
+      font-weight: 700;
+      color: #111827;
+      line-height: 1;
+      margin-bottom: 4px;
+    }
+    
+    .stat-label {
+      font-size: 14px;
+      color: #6B7280;
+      font-weight: 500;
+    }
+  }
+}
+
+.filter-section {
+  background: white;
+  border-radius: 12px;
+  padding: 24px;
+  margin-bottom: 24px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.filter-row {
+  display: flex;
+  gap: 20px;
+  align-items: flex-end;
+  flex-wrap: wrap;
+}
+
+.filter-group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  min-width: 200px;
+  
+  .filter-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: #374151;
+  }
+  
+  .filter-select,
+  .filter-input {
+    padding: 10px 12px;
+    border: 1px solid #D1D5DB;
+    border-radius: 8px;
+    font-size: 14px;
+    
+    &:focus {
+      outline: none;
+      border-color: #3B82F6;
+      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+    }
+  }
+  
+  .date-range {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    .date-separator {
+      color: #6B7280;
+      font-size: 14px;
+    }
+  }
+}
+
+.filter-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.approval-list {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.list-header {
+  padding: 20px 24px;
+  border-bottom: 1px solid #E5E7EB;
+  
+  h3 {
+    margin: 0;
+    font-size: 18px;
+    font-weight: 600;
+    color: #111827;
+  }
+}
+
+.empty-state {
+  padding: 60px 24px;
+  text-align: center;
+  
+  .empty-icon {
+    margin-bottom: 16px;
+    opacity: 0.5;
+  }
+  
+  p {
+    margin: 8px 0;
+    color: #6B7280;
+    
+    &.empty-hint {
+      font-size: 14px;
+    }
+  }
+}
+
+.approval-table {
+  .table-header {
+    display: grid;
+    grid-template-columns: 2fr 2fr 1.5fr 1.5fr 1fr 1fr;
+    gap: 16px;
+    padding: 16px 24px;
+    background-color: #F9FAFB;
+    border-bottom: 1px solid #E5E7EB;
+    font-weight: 600;
+    font-size: 14px;
+    color: #374151;
+  }
+  
+  .table-row {
+    display: grid;
+    grid-template-columns: 2fr 2fr 1.5fr 1.5fr 1fr 1fr;
+    gap: 16px;
+    padding: 20px 24px;
+    border-bottom: 1px solid #F3F4F6;
+    align-items: center;
+    
+    &:hover {
+      background-color: #F9FAFB;
+    }
+  }
+  
+  .table-cell {
+    font-size: 14px;
+  }
+}
+
+.project-info {
+  .project-name {
+    font-weight: 500;
+    color: #111827;
+    margin-bottom: 4px;
+  }
+  
+  .project-id {
+    font-size: 12px;
+    color: #6B7280;
+  }
+}
+
+.customer-info {
+  .customer-name {
+    font-weight: 500;
+    color: #111827;
+    margin-bottom: 4px;
+  }
+  
+  .submitted-by {
+    font-size: 12px;
+    color: #6B7280;
+  }
+}
+
+.amount {
+  font-weight: 600;
+  color: #059669;
+  font-size: 16px;
+}
+
+.date {
+  color: #6B7280;
+}
+
+.status-badge {
+  display: inline-block;
+  padding: 4px 12px;
+  border-radius: 20px;
+  font-size: 12px;
+  font-weight: 500;
+  
+  &.status-pending {
+    background-color: #FEF3C7;
+    color: #D97706;
+  }
+  
+  &.status-approved {
+    background-color: #D1FAE5;
+    color: #059669;
+  }
+  
+  &.status-rejected {
+    background-color: #FEE2E2;
+    color: #DC2626;
+  }
+  
+  &.status-revision {
+    background-color: #E0E7FF;
+    color: #5B21B6;
+  }
+}
+
+// 模态框样式
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+  max-height: 90vh;
+  overflow-y: auto;
+  
+  &.approval-modal {
+    width: 90%;
+    max-width: 800px;
+  }
+  
+  &.stats-modal {
+    width: 90%;
+    max-width: 600px;
+  }
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24px;
+  border-bottom: 1px solid #E5E7EB;
+  
+  h4 {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: #111827;
+  }
+  
+  .modal-close {
+    background: none;
+    border: none;
+    padding: 8px;
+    cursor: pointer;
+    border-radius: 6px;
+    color: #6B7280;
+    
+    &:hover {
+      background-color: #F3F4F6;
+      color: #374151;
+    }
+  }
+}
+
+.modal-body {
+  padding: 24px;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 24px;
+  border-top: 1px solid #E5E7EB;
+}
+
+.approval-details {
+  .detail-section {
+    margin-bottom: 32px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    h5 {
+      margin: 0 0 16px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #111827;
+    }
+  }
+  
+  .detail-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+    gap: 16px;
+  }
+  
+  .detail-item {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    
+    label {
+      font-size: 12px;
+      font-weight: 500;
+      color: #6B7280;
+      text-transform: uppercase;
+      letter-spacing: 0.05em;
+    }
+    
+    span {
+      font-size: 14px;
+      color: #111827;
+      
+      &.amount-highlight {
+        font-weight: 600;
+        color: #059669;
+        font-size: 18px;
+      }
+    }
+  }
+  
+  .script-info {
+    padding: 12px 16px;
+    background-color: #F3F4F6;
+    border-radius: 8px;
+    
+    .script-id {
+      font-size: 14px;
+      color: #6B7280;
+      font-family: monospace;
+    }
+  }
+  
+  .comment-textarea {
+    width: 100%;
+    padding: 12px;
+    border: 1px solid #D1D5DB;
+    border-radius: 8px;
+    font-size: 14px;
+    resize: vertical;
+    
+    &:focus {
+      outline: none;
+      border-color: #3B82F6;
+      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+    }
+  }
+}
+
+.stats-content {
+  .stats-summary {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+    gap: 20px;
+    margin-bottom: 32px;
+    
+    .summary-item {
+      text-align: center;
+      padding: 20px;
+      background-color: #F9FAFB;
+      border-radius: 8px;
+      
+      label {
+        display: block;
+        font-size: 14px;
+        color: #6B7280;
+        margin-bottom: 8px;
+      }
+      
+      .summary-value {
+        font-size: 24px;
+        font-weight: 700;
+        color: #111827;
+      }
+    }
+  }
+  
+  .script-stats {
+    h5 {
+      margin: 0 0 16px 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #111827;
+    }
+  }
+  
+  .stats-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+  
+  .stat-item {
+    .stat-info {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
+      
+      .script-title {
+        font-weight: 500;
+        color: #111827;
+      }
+      
+      .usage-count {
+        font-size: 14px;
+        color: #6B7280;
+      }
+    }
+    
+    .stat-bar {
+      height: 8px;
+      background-color: #E5E7EB;
+      border-radius: 4px;
+      overflow: hidden;
+      
+      .bar-fill {
+        height: 100%;
+        background-color: #3B82F6;
+        transition: width 0.3s ease;
+      }
+    }
+  }
+}
+
+// 按钮样式
+.btn-primary,
+.btn-secondary,
+.btn-success,
+.btn-warning,
+.btn-danger {
+  padding: 10px 16px;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  transition: all 0.2s ease;
+  
+  &:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+  
+  &.btn-sm {
+    padding: 6px 12px;
+    font-size: 12px;
+  }
+}
+
+.btn-primary {
+  background-color: #3B82F6;
+  color: white;
+  
+  &:hover:not(:disabled) {
+    background-color: #2563EB;
+  }
+}
+
+.btn-secondary {
+  background-color: #F3F4F6;
+  color: #374151;
+  
+  &:hover:not(:disabled) {
+    background-color: #E5E7EB;
+  }
+}
+
+.btn-success {
+  background-color: #059669;
+  color: white;
+  
+  &:hover:not(:disabled) {
+    background-color: #047857;
+  }
+}
+
+.btn-warning {
+  background-color: #D97706;
+  color: white;
+  
+  &:hover:not(:disabled) {
+    background-color: #B45309;
+  }
+}
+
+.btn-danger {
+  background-color: #DC2626;
+  color: white;
+  
+  &:hover:not(:disabled) {
+    background-color: #B91C1C;
+  }
+}

+ 274 - 0
src/app/pages/finance/quotation-approval/quotation-approval.component.ts

@@ -0,0 +1,274 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { QuotationApprovalService } from '../../../services/quotation-approval.service';
+import { QuotationApproval, QuotationApprovalStatus, CommunicationRecord, ScriptUsageStats } from '../../../services/quotation-approval.service';
+
+@Component({
+  selector: 'app-quotation-approval',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './quotation-approval.component.html',
+  styleUrls: ['./quotation-approval.component.scss']
+})
+export class QuotationApprovalComponent implements OnInit {
+  quotationApprovals: QuotationApproval[] = [];
+  filteredApprovals: QuotationApproval[] = [];
+  scriptUsageStats: ScriptUsageStats[] = [];
+  
+  // 筛选条件
+  statusFilter: QuotationApprovalStatus | 'all' = 'all';
+  searchKeyword = '';
+  dateRange = {
+    start: '',
+    end: ''
+  };
+  
+  // 统计数据
+  approvalStats = {
+    total: 0,
+    pending: 0,
+    approved: 0,
+    rejected: 0,
+    revisionRequired: 0,
+    averageApprovalTime: 0
+  };
+  
+  // 选中的审核项
+  selectedApproval: QuotationApproval | null = null;
+  
+  // 沟通记录
+  communicationRecords: CommunicationRecord[] = [];
+  newComment = '';
+  
+  // 模态框状态
+  showApprovalModal = false;
+  showStatsModal = false;
+
+  constructor(private quotationApprovalService: QuotationApprovalService) {}
+
+  ngOnInit() {
+    this.loadQuotationApprovals();
+    this.loadScriptUsageStats();
+    this.loadApprovalStats();
+  }
+
+  // 加载报价审核列表
+  loadQuotationApprovals() {
+    this.quotationApprovalService.getQuotationApprovals().subscribe({
+      next: (approvals: QuotationApproval[]) => {
+        this.quotationApprovals = approvals;
+        this.applyFilters();
+      },
+      error: (error: any) => {
+        console.error('加载报价审核列表失败:', error);
+      }
+    });
+  }
+
+  // 加载话术使用统计
+  loadScriptUsageStats() {
+    this.quotationApprovalService.getScriptUsageStats().subscribe({
+      next: (stats: ScriptUsageStats[]) => {
+        this.scriptUsageStats = stats;
+      },
+      error: (error: any) => {
+        console.error('加载话术使用统计失败:', error);
+      }
+    });
+  }
+
+  // 加载审核统计
+  loadApprovalStats() {
+    this.quotationApprovalService.getApprovalStats().subscribe({
+      next: (stats: any) => {
+        this.approvalStats = stats;
+      },
+      error: (error: any) => {
+        console.error('加载审核统计失败:', error);
+      }
+    });
+  }
+
+  // 应用筛选条件
+  applyFilters() {
+    this.filteredApprovals = this.quotationApprovals.filter(approval => {
+      // 状态筛选
+      if (this.statusFilter !== 'all' && approval.status !== this.statusFilter) {
+        return false;
+      }
+      
+      // 关键词搜索
+      if (this.searchKeyword) {
+        const keyword = this.searchKeyword.toLowerCase();
+        if (!approval.projectName.toLowerCase().includes(keyword) &&
+            !approval.customerName.toLowerCase().includes(keyword) &&
+            !approval.submittedBy.toLowerCase().includes(keyword)) {
+          return false;
+        }
+      }
+      
+      // 日期范围筛选
+      if (this.dateRange.start) {
+        const startDate = new Date(this.dateRange.start);
+        if (new Date(approval.submittedAt) < startDate) {
+          return false;
+        }
+      }
+      
+      if (this.dateRange.end) {
+        const endDate = new Date(this.dateRange.end);
+        endDate.setHours(23, 59, 59, 999);
+        if (new Date(approval.submittedAt) > endDate) {
+          return false;
+        }
+      }
+      
+      return true;
+    });
+  }
+
+  // 重置筛选条件
+  resetFilters() {
+    this.statusFilter = 'all';
+    this.searchKeyword = '';
+    this.dateRange = { start: '', end: '' };
+    this.applyFilters();
+  }
+
+  // 查看审核详情
+  viewApprovalDetails(approval: QuotationApproval) {
+    this.selectedApproval = approval;
+    this.loadCommunicationRecords(approval.quotationId);
+    this.showApprovalModal = true;
+  }
+
+  // 加载沟通记录
+  loadCommunicationRecords(quotationId: string) {
+    // 这里应该调用服务获取沟通记录
+    // 暂时使用模拟数据
+    this.communicationRecords = [];
+  }
+
+  // 审批报价
+  approveQuotation() {
+    if (!this.selectedApproval) return;
+    
+    this.quotationApprovalService.approveQuotation(
+      this.selectedApproval.quotationId,
+      'current_user',
+      this.newComment
+    ).subscribe({
+      next: (updatedApproval) => {
+        this.updateApprovalInList(updatedApproval);
+        this.closeApprovalModal();
+        this.loadApprovalStats();
+      },
+      error: (error) => {
+        console.error('审批失败:', error);
+      }
+    });
+  }
+
+  // 拒绝报价
+  rejectQuotation() {
+    if (!this.selectedApproval) return;
+    
+    this.quotationApprovalService.rejectQuotation(
+      this.selectedApproval.quotationId,
+      'current_user',
+      this.newComment
+    ).subscribe({
+      next: (updatedApproval) => {
+        this.updateApprovalInList(updatedApproval);
+        this.closeApprovalModal();
+        this.loadApprovalStats();
+      },
+      error: (error) => {
+        console.error('拒绝失败:', error);
+      }
+    });
+  }
+
+  // 要求修改
+  requestRevision() {
+    if (!this.selectedApproval) return;
+    
+    this.quotationApprovalService.requestRevision(
+      this.selectedApproval.quotationId,
+      'current_user',
+      this.newComment
+    ).subscribe({
+      next: (updatedApproval) => {
+        this.updateApprovalInList(updatedApproval);
+        this.closeApprovalModal();
+        this.loadApprovalStats();
+      },
+      error: (error) => {
+        console.error('要求修改失败:', error);
+      }
+    });
+  }
+
+  // 更新列表中的审核项
+  updateApprovalInList(updatedApproval: QuotationApproval) {
+    const index = this.quotationApprovals.findIndex(a => a.quotationId === updatedApproval.quotationId);
+    if (index !== -1) {
+      this.quotationApprovals[index] = updatedApproval;
+      this.applyFilters();
+    }
+  }
+
+  // 关闭审核模态框
+  closeApprovalModal() {
+    this.showApprovalModal = false;
+    this.selectedApproval = null;
+    this.newComment = '';
+    this.communicationRecords = [];
+  }
+
+  // 显示统计模态框
+  showStatistics() {
+    this.showStatsModal = true;
+  }
+
+  // 关闭统计模态框
+  closeStatsModal() {
+    this.showStatsModal = false;
+  }
+
+  // 格式化金额
+  formatAmount(amount: number): string {
+    return amount.toLocaleString('zh-CN', {
+      minimumFractionDigits: 2,
+      maximumFractionDigits: 2
+    });
+  }
+
+  // 格式化日期
+  formatDate(date: string | Date): string {
+    return new Date(date).toLocaleString('zh-CN');
+  }
+
+  // 获取状态标签
+  getStatusLabel(status: QuotationApprovalStatus): string {
+    const statusMap = {
+      'pending': '待审核',
+      'approved': '已通过',
+      'rejected': '已拒绝',
+      'revision_required': '需修改'
+    };
+    return statusMap[status] || status;
+  }
+
+  // 获取状态样式类
+  getStatusClass(status: QuotationApprovalStatus): string {
+    const classMap = {
+      'pending': 'status-pending',
+      'approved': 'status-approved',
+      'rejected': 'status-rejected',
+      'revision_required': 'status-revision'
+    };
+    return classMap[status] || '';
+  }
+}

+ 351 - 0
src/app/services/ai-quotation.service.ts

@@ -0,0 +1,351 @@
+import { Injectable } from '@angular/core';
+import { Observable, of, delay } from 'rxjs';
+
+export interface AIQuotationParams {
+  area: number;
+  style: string;
+  level: string;
+  specialRequirements?: string;
+}
+
+export interface AIQuotationItem {
+  category: string;
+  name: string;
+  quantity: number;
+  unit: string;
+  unitPrice: number;
+  description?: string;
+}
+
+export interface AIQuotationResult {
+  items: AIQuotationItem[];
+  totalAmount: number;
+  suggestions?: string[];
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AIQuotationService {
+  
+  // 装修风格价格系数
+  private styleMultipliers: { [key: string]: number } = {
+    '现代简约': 1.0,
+    '北欧风格': 1.1,
+    '中式风格': 1.3,
+    '欧式风格': 1.4,
+    '美式风格': 1.2,
+    '工业风格': 1.1,
+    '地中海风格': 1.2,
+    '日式风格': 1.15
+  };
+
+  // 装修档次价格系数
+  private levelMultipliers: { [key: string]: number } = {
+    '经济型': 0.8,
+    '舒适型': 1.0,
+    '豪华型': 1.5,
+    '奢华型': 2.0
+  };
+
+  // 基础报价模板
+  private baseQuotationTemplate: AIQuotationItem[] = [
+    {
+      category: '客餐厅',
+      name: '墙面乳胶漆',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 35,
+      description: '包含基层处理、刮腻子、刷底漆面漆'
+    },
+    {
+      category: '客餐厅',
+      name: '地面瓷砖铺贴',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 80,
+      description: '包含瓷砖、水泥沙浆、人工费'
+    },
+    {
+      category: '客餐厅',
+      name: '吊顶造型',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 120,
+      description: '石膏板吊顶,包含龙骨、石膏板、乳胶漆'
+    },
+    {
+      category: '厨房',
+      name: '厨房墙地砖',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 90,
+      description: '防滑地砖、墙面瓷砖铺贴'
+    },
+    {
+      category: '厨房',
+      name: '橱柜定制',
+      quantity: 1,
+      unit: '延米',
+      unitPrice: 1200,
+      description: '包含地柜、吊柜、台面、五金配件'
+    },
+    {
+      category: '卫生间',
+      name: '卫生间防水',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 60,
+      description: '聚氨酯防水涂料,包工包料'
+    },
+    {
+      category: '卫生间',
+      name: '卫浴洁具',
+      quantity: 1,
+      unit: '套',
+      unitPrice: 2500,
+      description: '马桶、洗手盆、花洒、龙头等'
+    },
+    {
+      category: '卧室',
+      name: '卧室地板',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 150,
+      description: '复合地板,包含踢脚线安装'
+    },
+    {
+      category: '卧室',
+      name: '衣柜定制',
+      quantity: 1,
+      unit: '㎡',
+      unitPrice: 800,
+      description: '整体衣柜,包含五金配件'
+    },
+    {
+      category: '水电改造',
+      name: '水电改造',
+      quantity: 1,
+      unit: '项',
+      unitPrice: 8000,
+      description: '全屋水电线路改造,包含材料人工'
+    }
+  ];
+
+  constructor() { }
+
+  /**
+   * 生成AI智能报价
+   * @param params 报价参数
+   * @returns 报价结果
+   */
+  generateQuotation(params: AIQuotationParams): Observable<AIQuotationResult> {
+    // 模拟AI处理延迟
+    return of(this.calculateQuotation(params)).pipe(
+      delay(2000) // 模拟2秒的AI处理时间
+    );
+  }
+
+  /**
+   * 计算报价
+   * @param params 报价参数
+   * @returns 报价结果
+   */
+  private calculateQuotation(params: AIQuotationParams): AIQuotationResult {
+    const { area, style, level, specialRequirements } = params;
+    
+    // 获取价格系数
+    const styleMultiplier = this.styleMultipliers[style] || 1.0;
+    const levelMultiplier = this.levelMultipliers[level] || 1.0;
+    
+    // 计算各项目数量和价格
+    const items: AIQuotationItem[] = this.baseQuotationTemplate.map(template => {
+      let quantity = area;
+      let unitPrice = template.unitPrice * styleMultiplier * levelMultiplier;
+      
+      // 根据项目类型调整数量
+      switch (template.category) {
+        case '客餐厅':
+          quantity = Math.round(area * 0.4); // 客餐厅占40%面积
+          break;
+        case '厨房':
+          if (template.name.includes('橱柜')) {
+            quantity = Math.round(area * 0.08); // 橱柜按延米计算
+          } else {
+            quantity = Math.round(area * 0.1); // 厨房占10%面积
+          }
+          break;
+        case '卫生间':
+          if (template.name.includes('洁具')) {
+            quantity = Math.ceil(area / 50); // 每50㎡一套洁具
+          } else {
+            quantity = Math.round(area * 0.08); // 卫生间占8%面积
+          }
+          break;
+        case '卧室':
+          if (template.name.includes('衣柜')) {
+            quantity = Math.round(area * 0.15); // 衣柜按投影面积
+          } else {
+            quantity = Math.round(area * 0.3); // 卧室占30%面积
+          }
+          break;
+        case '水电改造':
+          quantity = 1; // 水电改造按项目计算
+          unitPrice = area * 80 * levelMultiplier; // 按面积计算水电改造费用
+          break;
+      }
+      
+      return {
+        ...template,
+        quantity: Math.max(1, quantity),
+        unitPrice: Math.round(unitPrice)
+      };
+    });
+
+    // 根据特殊需求调整报价
+    if (specialRequirements) {
+      this.adjustForSpecialRequirements(items, specialRequirements);
+    }
+
+    // 计算总金额
+    const totalAmount = items.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0);
+
+    // 生成建议
+    const suggestions = this.generateSuggestions(params, totalAmount);
+
+    return {
+      items,
+      totalAmount: Math.round(totalAmount),
+      suggestions
+    };
+  }
+
+  /**
+   * 根据特殊需求调整报价
+   * @param items 报价项目
+   * @param specialRequirements 特殊需求
+   */
+  private adjustForSpecialRequirements(items: AIQuotationItem[], specialRequirements: string): void {
+    const requirements = specialRequirements.toLowerCase();
+    
+    // 智能家居需求
+    if (requirements.includes('智能') || requirements.includes('智能家居')) {
+      items.push({
+        category: '智能家居',
+        name: '智能家居系统',
+        quantity: 1,
+        unit: '套',
+        unitPrice: 15000,
+        description: '包含智能开关、智能门锁、智能窗帘等'
+      });
+    }
+
+    // 中央空调需求
+    if (requirements.includes('中央空调') || requirements.includes('空调')) {
+      items.push({
+        category: '暖通设备',
+        name: '中央空调系统',
+        quantity: 1,
+        unit: '套',
+        unitPrice: 25000,
+        description: '包含主机、管道、出风口等'
+      });
+    }
+
+    // 地暖需求
+    if (requirements.includes('地暖') || requirements.includes('采暖')) {
+      items.push({
+        category: '暖通设备',
+        name: '地暖系统',
+        quantity: 1,
+        unit: '套',
+        unitPrice: 18000,
+        description: '包含地暖管道、分水器、温控器等'
+      });
+    }
+
+    // 新风系统需求
+    if (requirements.includes('新风') || requirements.includes('通风')) {
+      items.push({
+        category: '暖通设备',
+        name: '新风系统',
+        quantity: 1,
+        unit: '套',
+        unitPrice: 12000,
+        description: '包含新风主机、管道、出风口等'
+      });
+    }
+
+    // 定制家具需求
+    if (requirements.includes('定制') || requirements.includes('家具')) {
+      items.push({
+        category: '定制家具',
+        name: '全屋定制家具',
+        quantity: 1,
+        unit: '套',
+        unitPrice: 30000,
+        description: '包含鞋柜、电视柜、书柜等定制家具'
+      });
+    }
+  }
+
+  /**
+   * 生成装修建议
+   * @param params 报价参数
+   * @param totalAmount 总金额
+   * @returns 建议列表
+   */
+  private generateSuggestions(params: AIQuotationParams, totalAmount: number): string[] {
+    const suggestions: string[] = [];
+    const { area, style, level } = params;
+    
+    // 预算建议
+    const budgetPerSqm = totalAmount / area;
+    if (budgetPerSqm > 2000) {
+      suggestions.push('当前预算较高,建议优化材料选择以控制成本');
+    } else if (budgetPerSqm < 800) {
+      suggestions.push('当前预算偏低,建议适当提升材料档次以保证质量');
+    }
+
+    // 风格建议
+    if (style === '现代简约') {
+      suggestions.push('现代简约风格注重功能性,建议选择简洁大方的材料和色彩');
+    } else if (style === '中式风格') {
+      suggestions.push('中式风格建议选用实木材料,注重对称和层次感');
+    } else if (style === '北欧风格') {
+      suggestions.push('北欧风格建议使用浅色系,注重自然光线和简约线条');
+    }
+
+    // 档次建议
+    if (level === '经济型') {
+      suggestions.push('经济型装修建议重点投入在基础工程,后期可逐步升级软装');
+    } else if (level === '豪华型' || level === '奢华型') {
+      suggestions.push('高端装修建议选择知名品牌材料,注重细节工艺和环保性能');
+    }
+
+    // 面积建议
+    if (area < 60) {
+      suggestions.push('小户型建议采用开放式设计,合理利用每一寸空间');
+    } else if (area > 150) {
+      suggestions.push('大户型建议做好功能分区,可考虑增加储物空间和休闲区域');
+    }
+
+    return suggestions;
+  }
+
+  /**
+   * 获取装修风格列表
+   * @returns 风格列表
+   */
+  getStyleOptions(): string[] {
+    return Object.keys(this.styleMultipliers);
+  }
+
+  /**
+   * 获取装修档次列表
+   * @returns 档次列表
+   */
+  getLevelOptions(): string[] {
+    return Object.keys(this.levelMultipliers);
+  }
+}

+ 820 - 0
src/app/services/auto-settlement.service.ts

@@ -0,0 +1,820 @@
+import { Injectable, signal, computed, inject } from '@angular/core';
+import { BehaviorSubject, Observable, of, timer, interval, forkJoin } from 'rxjs';
+import { map, switchMap, catchError } from 'rxjs/operators';
+import { Settlement } from '../models/project.model';
+import { ProjectService } from './project.service';
+import { PaymentVoucherRecognitionService, PaymentVoucherRecognitionResult } from './payment-voucher-recognition.service';
+import { MiniprogramPaymentService, MiniprogramPaymentResult } from './miniprogram-payment.service';
+import { NotificationService, NotificationType, NotificationChannel } from './notification.service';
+
+export interface AutoSettlementRule {
+  id: string;
+  name: string;
+  enabled: boolean;
+  conditions: SettlementCondition[];
+  actions: SettlementAction[];
+  priority: number;
+  description?: string;
+}
+
+export interface SettlementCondition {
+  type: 'projectType' | 'amountRange' | 'customerTier' | 'overdueDays' | 'paymentMethod';
+  operator: 'equals' | 'greaterThan' | 'lessThan' | 'between' | 'contains';
+  value: any;
+}
+
+export interface SettlementAction {
+  type: 'sendReminder' | 'applyDiscount' | 'extendDueDate' | 'autoConfirm' | 'notifyManager';
+  params: any;
+}
+
+export interface SettlementReminder {
+  id: string;
+  settlementId: string;
+  type: 'email' | 'sms' | 'wechat' | 'system';
+  content: string;
+  sentAt: Date;
+  status: 'pending' | 'sent' | 'failed';
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AutoSettlementService {
+  private scheduledProcesses = new Map<string, any>();
+  private paymentRecognitionService = inject(PaymentVoucherRecognitionService);
+  private miniprogramPaymentService = inject(MiniprogramPaymentService);
+  private notificationService = inject(NotificationService);
+  
+  constructor(private projectService: ProjectService) {
+    // 启动小程序支付自动化监听
+    this.initializeMiniprogramPaymentAutomation();
+  }
+  
+  private rules = signal<AutoSettlementRule[]>([
+    {
+      id: 'rule-1',
+      name: '小额自动确认',
+      enabled: true,
+      priority: 1,
+      conditions: [
+        { type: 'amountRange', operator: 'lessThan', value: 5000 }
+      ],
+      actions: [
+        { type: 'autoConfirm', params: { immediate: true } }
+      ],
+      description: '金额小于5000元时自动确认结算'
+    },
+    {
+      id: 'rule-2',
+      name: '逾期提醒',
+      enabled: true,
+      priority: 2,
+      conditions: [
+        { type: 'overdueDays', operator: 'greaterThan', value: 7 }
+      ],
+      actions: [
+        { type: 'sendReminder', params: { channels: ['wechat', 'sms'], frequency: 'daily' } }
+      ],
+      description: '逾期7天以上时每天发送提醒'
+    },
+    {
+      id: 'rule-3',
+      name: 'VIP客户优惠',
+      enabled: true,
+      priority: 3,
+      conditions: [
+        { type: 'customerTier', operator: 'equals', value: 'vip' },
+        { type: 'overdueDays', operator: 'greaterThan', value: 15 }
+      ],
+      actions: [
+        { type: 'applyDiscount', params: { percentage: 5, maxAmount: 1000 } }
+      ],
+      description: 'VIP客户逾期15天以上时提供5%折扣'
+    }
+  ]);
+
+  private reminders = signal<SettlementReminder[]>([]);
+  private isProcessing = signal(false);
+
+  // 获取所有规则
+  getRules(): Observable<AutoSettlementRule[]> {
+    return of(this.rules());
+  }
+
+  // 添加新规则
+  addRule(rule: AutoSettlementRule): void {
+    this.rules.update(rules => [...rules, rule]);
+  }
+
+  // 更新规则
+  updateRule(ruleId: string, updates: Partial<AutoSettlementRule>): void {
+    this.rules.update(rules => 
+      rules.map(rule => rule.id === ruleId ? { ...rule, ...updates } : rule)
+    );
+  }
+
+  // 删除规则
+  deleteRule(ruleId: string): void {
+    this.rules.update(rules => rules.filter(rule => rule.id !== ruleId));
+  }
+
+  // 处理结算自动化
+  processSettlementAutomation(settlement: Settlement): Observable<boolean> {
+    this.isProcessing.set(true);
+    
+    return of(this.rules())
+      .pipe(
+        map(rules => rules.filter(rule => rule.enabled)),
+        map(enabledRules => {
+          let processed = false;
+          
+          // 按优先级排序处理规则
+          enabledRules.sort((a, b) => a.priority - b.priority);
+          
+          for (const rule of enabledRules) {
+            if (this.checkConditions(rule.conditions, settlement)) {
+              this.executeActions(rule.actions, settlement);
+              processed = true;
+              
+              // 高优先级规则可能中断后续规则执行
+              if (rule.priority >= 10) {
+                break;
+              }
+            }
+          }
+          
+          return processed;
+        }),
+        switchMap(processed => {
+          this.isProcessing.set(false);
+          return of(processed);
+        })
+      );
+  }
+
+  // 检查条件是否满足
+  private checkConditions(conditions: SettlementCondition[], settlement: Settlement): boolean {
+    return conditions.every(condition => {
+      switch (condition.type) {
+        case 'amountRange':
+          return this.checkAmountCondition(condition, settlement.amount || 0);
+        case 'overdueDays':
+          const overdueDays = this.calculateOverdueDays(settlement);
+          return this.checkNumericCondition(condition, overdueDays);
+        case 'customerTier':
+          // 简化实现,实际中需要从客户服务获取层级信息
+          return condition.operator === 'equals' && condition.value === 'vip';
+        default:
+          return false;
+      }
+    });
+  }
+
+  // 执行动作
+  private executeActions(actions: SettlementAction[], settlement: Settlement): void {
+    actions.forEach(action => {
+      switch (action.type) {
+        case 'sendReminder':
+          this.sendReminder(settlement, action.params);
+          break;
+        case 'applyDiscount':
+          this.applyDiscount(settlement, action.params);
+          break;
+        case 'autoConfirm':
+          this.autoConfirmSettlement(settlement, action.params);
+          break;
+        case 'notifyManager':
+          this.notifyManager(settlement, action.params);
+          break;
+      }
+    });
+  }
+
+  // 发送提醒
+  private sendReminder(settlement: Settlement, params: any): void {
+    const reminder: SettlementReminder = {
+      id: `reminder-${Date.now()}`,
+      settlementId: settlement.id,
+      type: 'system',
+      content: `结算提醒:项目 ${settlement.projectName} 的 ${settlement.amount} 元结算${this.calculateOverdueDays(settlement) > 0 ? '已逾期' : '待处理'}`,
+      sentAt: new Date(),
+      status: 'sent'
+    };
+    
+    this.reminders.update(reminders => [...reminders, reminder]);
+    
+    // 实际实现中这里会调用消息服务发送到不同渠道
+    console.log('发送结算提醒:', reminder);
+  }
+
+  // 应用折扣
+  private applyDiscount(settlement: Settlement, params: any): void {
+    const discountAmount = Math.min(
+      (settlement.amount || 0) * (params.percentage / 100),
+      params.maxAmount || 0
+    );
+    
+    console.log(`为结算 ${settlement.id} 应用折扣: ${discountAmount}元`);
+    // 实际实现中需要更新结算金额
+  }
+
+  // 自动确认结算
+  private autoConfirmSettlement(settlement: Settlement, params: any): void {
+    console.log(`自动确认结算: ${settlement.id}`);
+    // 实际实现中需要调用结算服务确认结算
+  }
+
+  // 通知经理
+  private notifyManager(settlement: Settlement, params: any): void {
+    console.log(`通知经理处理大额结算: ${settlement.id}, 金额: ${settlement.amount}`);
+  }
+
+  // 计算逾期天数
+  private calculateOverdueDays(settlement: Settlement): number {
+    if (settlement.status === '已结算') return 0;
+    
+    const dueDate = settlement.dueDate || new Date(settlement.createdAt.getTime() + 30 * 24 * 60 * 60 * 1000);
+    const today = new Date();
+    const diffTime = today.getTime() - dueDate.getTime();
+    return Math.max(0, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
+  }
+
+  // 检查金额条件
+  private checkAmountCondition(condition: SettlementCondition, amount: number): boolean {
+    return this.checkNumericCondition(condition, amount);
+  }
+
+  // 检查数值条件
+  private checkNumericCondition(condition: SettlementCondition, value: number): boolean {
+    switch (condition.operator) {
+      case 'equals':
+        return value === condition.value;
+      case 'greaterThan':
+        return value > condition.value;
+      case 'lessThan':
+        return value < condition.value;
+      case 'between':
+        return value >= condition.value[0] && value <= condition.value[1];
+      default:
+        return false;
+    }
+  }
+
+  // 获取处理状态
+  getProcessingStatus() {
+    return this.isProcessing();
+  }
+
+  // 获取提醒记录
+  getReminders(): Observable<SettlementReminder[]> {
+    return of(this.reminders());
+  }
+
+  // 启动定时任务
+  startScheduledProcessing(): void {
+    // 清理现有的定时任务
+    this.stopAllScheduledProcessing();
+    
+    // 每30分钟检查一次逾期结算
+    this.scheduledProcesses.set('overdueCheck', 
+      timer(0, 30 * 60 * 1000).subscribe(() => {
+        this.processOverdueSettlements();
+      })
+    );
+    
+    // 每天早上9点发送每日提醒
+    this.scheduledProcesses.set('dailyReminder',
+      this.scheduleDailyTask(9, 0, () => {
+        this.sendDailyReminders();
+      })
+    );
+    
+    // 每周一早上10点发送周报
+    this.scheduledProcesses.set('weeklyReport',
+      this.scheduleWeeklyTask(1, 10, 0, () => {
+        this.sendWeeklySettlementReport();
+      })
+    );
+    
+    // 每小时检查高优先级规则
+    this.scheduledProcesses.set('hourlyCheck',
+      timer(0, 60 * 60 * 1000).subscribe(() => {
+        this.processHighPrioritySettlements();
+      })
+    );
+    
+    console.log('自动化结算定时任务已启动');
+  }
+
+  // 停止所有定时任务
+  stopAllScheduledProcessing(): void {
+    this.scheduledProcesses.forEach((subscription, key) => {
+      subscription.unsubscribe();
+    });
+    this.scheduledProcesses.clear();
+  }
+
+  // 安排每日定时任务
+  private scheduleDailyTask(hour: number, minute: number, task: () => void): any {
+    const now = new Date();
+    const targetTime = new Date();
+    targetTime.setHours(hour, minute, 0, 0);
+    
+    let initialDelay = targetTime.getTime() - now.getTime();
+    if (initialDelay < 0) {
+      initialDelay += 24 * 60 * 60 * 1000; // 第二天同一时间
+    }
+    
+    return timer(initialDelay, 24 * 60 * 60 * 1000).subscribe(() => {
+      task();
+    });
+  }
+
+  // 安排每周定时任务
+  private scheduleWeeklyTask(dayOfWeek: number, hour: number, minute: number, task: () => void): any {
+    const now = new Date();
+    const targetTime = new Date();
+    
+    // 计算下一个指定星期几
+    const daysUntilTarget = (dayOfWeek - now.getDay() + 7) % 7;
+    targetTime.setDate(now.getDate() + daysUntilTarget);
+    targetTime.setHours(hour, minute, 0, 0);
+    
+    let initialDelay = targetTime.getTime() - now.getTime();
+    if (initialDelay < 0) {
+      initialDelay += 7 * 24 * 60 * 60 * 1000; // 下一周同一时间
+    }
+    
+    return timer(initialDelay, 7 * 24 * 60 * 60 * 1000).subscribe(() => {
+      task();
+    });
+  }
+
+  // 处理逾期结算
+  private processOverdueSettlements(): void {
+    console.log('执行定时逾期结算检查');
+    
+    // 获取所有待结算的记录
+    this.projectService.getSettlements().pipe(
+      map(settlements => settlements.filter(s => s.status === '待结算')),
+      switchMap(pendingSettlements => {
+        const overdueSettlements = pendingSettlements.filter(settlement => 
+          this.calculateOverdueDays(settlement) > 0
+        );
+        
+        // 对每个逾期结算应用自动化规则
+        const processingObservables = overdueSettlements.map(settlement =>
+          this.processSettlementAutomation(settlement).pipe(
+            catchError(error => {
+              console.error(`处理结算 ${settlement.id} 时出错:`, error);
+              return of(false);
+            })
+          )
+        );
+        
+        return processingObservables.length > 0 
+          ? forkJoin(processingObservables) 
+          : of([]);
+      })
+    ).subscribe(results => {
+      const successful = results.filter(result => result).length;
+      console.log(`逾期结算处理完成,成功处理 ${successful} 个结算`);
+    });
+  }
+
+  // 处理高优先级结算
+  private processHighPrioritySettlements(): void {
+    this.projectService.getSettlements().pipe(
+      map(settlements => settlements.filter(s => s.status === '待结算')),
+      map(pendingSettlements => {
+        // 筛选需要立即处理的高优先级结算(大金额或特定客户)
+        return pendingSettlements.filter(settlement => 
+          (settlement.amount || 0) > 10000 || // 大金额
+          settlement.projectName?.includes('VIP') // VIP客户
+        );
+      }),
+      switchMap(highPrioritySettlements => {
+        const processingObservables = highPrioritySettlements.map(settlement =>
+          this.processSettlementAutomation(settlement).pipe(
+            catchError(error => {
+              console.error(`处理高优先级结算 ${settlement.id} 时出错:`, error);
+              return of(false);
+            })
+          )
+        );
+        
+        return processingObservables.length > 0 
+          ? forkJoin(processingObservables) 
+          : of([]);
+      })
+    ).subscribe(results => {
+      const successful = results.filter(result => result).length;
+      if (successful > 0) {
+        console.log(`高优先级结算处理完成,成功处理 ${successful} 个结算`);
+      }
+    });
+  }
+
+  // 发送每日提醒
+  private sendDailyReminders(): void {
+    this.projectService.getSettlements().pipe(
+      map(settlements => settlements.filter(s => s.status === '待结算')),
+      map(pendingSettlements => {
+        const today = new Date();
+        return pendingSettlements.filter(settlement => {
+          const dueDate = settlement.dueDate || new Date(settlement.createdAt.getTime() + 30 * 24 * 60 * 60 * 1000);
+          const daysUntilDue = Math.ceil((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
+          
+          // 发送即将到期(3天内)和已逾期的提醒
+          return daysUntilDue <= 3 || this.calculateOverdueDays(settlement) > 0;
+        });
+      })
+    ).subscribe(settlementsToRemind => {
+      settlementsToRemind.forEach(settlement => {
+        this.sendReminder(settlement, {
+          channels: ['system', 'email'],
+          frequency: 'daily',
+          type: 'dueDateReminder'
+        });
+      });
+      
+      console.log(`每日提醒发送完成,共发送 ${settlementsToRemind.length} 个提醒`);
+    });
+  }
+
+  // 发送周报
+  private sendWeeklySettlementReport(): void {
+    this.projectService.getSettlements().subscribe(allSettlements => {
+      const pendingCount = allSettlements.filter(s => s.status === '待结算').length;
+      const overdueCount = allSettlements.filter(s => 
+        s.status === '待结算' && this.calculateOverdueDays(s) > 0
+      ).length;
+      const completedThisWeek = allSettlements.filter(s => 
+        s.status === '已结算' && 
+        s.settledAt && 
+        new Date(s.settledAt).getTime() > Date.now() - 7 * 24 * 60 * 60 * 1000
+      ).length;
+      
+      const report = {
+        totalPending: pendingCount,
+        totalOverdue: overdueCount,
+        completedThisWeek: completedThisWeek,
+        generatedAt: new Date()
+      };
+      
+      console.log('周度结算报告:', report);
+      
+      // 这里可以添加发送邮件或系统通知的逻辑
+      this.sendManagerNotification('weeklyReport', report);
+    });
+  }
+
+  // 发送经理通知
+  private sendManagerNotification(type: string, data: any): void {
+    const notification = {
+      id: `notification-${Date.now()}`,
+      type: type,
+      data: data,
+      timestamp: new Date(),
+      read: false
+    };
+    
+    console.log('发送经理通知:', notification);
+    // 实际实现中会调用通知服务
+  }
+
+  // 获取定时任务状态
+  getScheduledTasksStatus(): { [key: string]: boolean } {
+    const status: { [key: string]: boolean } = {};
+    this.scheduledProcesses.forEach((subscription, key) => {
+      status[key] = !subscription.closed;
+    });
+    return status;
+  }
+
+  // 支付凭证识别相关方法
+
+  /**
+   * 处理支付凭证上传并自动识别
+   */
+  async processPaymentVoucherUpload(file: File, settlementId: string): Promise<PaymentVoucherRecognitionResult> {
+    try {
+      const result = await this.paymentRecognitionService.recognizePaymentVoucher(file).toPromise();
+      
+      if (result && result.success) {
+        // 识别成功,更新结算状态
+        await this.updateSettlementWithPaymentInfo(settlementId, result);
+        
+        // 检查是否需要自动确认结算
+        if (this.shouldAutoConfirmAfterPayment(result)) {
+          await this.autoConfirmSettlementAfterPayment(settlementId, result);
+        }
+      }
+      
+      return result || {
+        success: false,
+        confidence: 0,
+        error: '支付凭证处理失败'
+      };
+    } catch (error) {
+      console.error('支付凭证处理失败:', error);
+      return {
+        success: false,
+        confidence: 0,
+        error: '支付凭证处理失败'
+      };
+    }
+  }
+
+  /**
+   * 批量处理支付凭证
+   */
+  async processBatchPaymentVouchers(files: File[], settlementId: string): Promise<PaymentVoucherRecognitionResult[]> {
+    const results: PaymentVoucherRecognitionResult[] = [];
+    
+    for (const file of files) {
+      try {
+        const result = await this.processPaymentVoucherUpload(file, settlementId);
+        results.push(result);
+      } catch (error) {
+        results.push({
+          success: false,
+          confidence: 0,
+          error: `文件 ${file.name} 处理失败`
+        });
+      }
+    }
+    
+    return results;
+  }
+
+  /**
+   * 使用支付信息更新结算记录
+   */
+  private async updateSettlementWithPaymentInfo(settlementId: string, paymentInfo: PaymentVoucherRecognitionResult): Promise<void> {
+    // 这里需要实现更新结算记录的逻辑
+    // 实际实现中会调用项目服务更新结算信息
+    console.log(`更新结算 ${settlementId} 的支付信息:`, paymentInfo);
+    
+    // 模拟更新操作
+    const settlements = await this.projectService.getSettlements().toPromise();
+    const settlement = settlements?.find(s => s.id === settlementId);
+    
+    if (settlement) {
+      // 更新结算记录的支付相关信息
+      console.log('结算记录已更新支付信息');
+    }
+  }
+
+  /**
+   * 检查支付后是否需要自动确认结算
+   */
+  private shouldAutoConfirmAfterPayment(paymentInfo: PaymentVoucherRecognitionResult): boolean {
+    // 根据支付金额、支付方式等条件判断是否需要自动确认
+    const hasAmount = paymentInfo.amount !== undefined && paymentInfo.amount > 0;
+    const highConfidence = paymentInfo.confidence > 0.8; // 识别置信度大于80%
+    const trustedPaymentMethod = ['alipay', 'wechat', 'bank_transfer'].includes(paymentInfo.paymentMethod || '');
+    
+    return hasAmount && highConfidence && trustedPaymentMethod;
+  }
+
+  /**
+   * 支付后自动确认结算
+   */
+  private async autoConfirmSettlementAfterPayment(settlementId: string, paymentInfo: PaymentVoucherRecognitionResult): Promise<void> {
+    console.log(`支付凭证验证通过,自动确认结算 ${settlementId}`);
+    
+    // 获取结算记录
+    const settlements = await this.projectService.getSettlements().toPromise();
+    const settlement = settlements?.find(s => s.id === settlementId);
+    
+    if (settlement) {
+      // 调用自动确认逻辑
+      this.autoConfirmSettlement(settlement, { 
+        immediate: true, 
+        paymentVerified: true,
+        paymentAmount: paymentInfo.amount,
+        paymentMethod: paymentInfo.paymentMethod
+      });
+      
+      console.log('结算已自动确认');
+    }
+  }
+
+  /**
+   * 获取支持的支付凭证类型
+   */
+  getSupportedPaymentVoucherTypes(): { extensions: string[], description: string } {
+    const supportedTypes = this.paymentRecognitionService.getSupportedFileTypes();
+    return {
+      extensions: supportedTypes,
+      description: this.paymentRecognitionService.getSupportedFileTypesDescription()
+    };
+  }
+
+  /**
+   * 验证支付凭证文件
+   */
+  validatePaymentVoucherFile(file: File): { valid: boolean, error?: string } {
+    const result = this.paymentRecognitionService.validateFile(file);
+    return {
+      valid: result.isValid,
+      error: result.error
+    };
+  }
+
+  /**
+   * 初始化小程序支付自动化流程
+   */
+  private initializeMiniprogramPaymentAutomation(): void {
+    console.log('初始化小程序支付自动化流程...');
+    
+    // 监听小程序支付完成事件
+    this.miniprogramPaymentService.onPaymentCompleted().subscribe({
+      next: (paymentResult: MiniprogramPaymentResult) => {
+        console.log('检测到小程序支付完成:', paymentResult);
+        this.handleMiniprogramPaymentCompleted(paymentResult);
+      },
+      error: (error) => {
+        console.error('小程序支付监听出错:', error);
+      }
+    });
+
+    // 启动小程序支付自动化监听
+    this.miniprogramPaymentService.startAutomationListener();
+  }
+
+  /**
+   * 处理小程序支付完成事件
+   */
+  private handleMiniprogramPaymentCompleted(paymentResult: MiniprogramPaymentResult): void {
+    if (!paymentResult.success) {
+      console.error('支付失败,跳过自动化处理');
+      return;
+    }
+
+    console.log(`开始处理小程序支付自动化流程: ${paymentResult.transactionId}`);
+
+    // 处理支付完成后的自动化流程
+    this.miniprogramPaymentService.processPaymentCompletedFlow(paymentResult).subscribe({
+      next: (success) => {
+        if (success) {
+          console.log('小程序支付自动化流程处理成功');
+          this.createAutomationLog(paymentResult.settlementId, 'miniprogram_payment_success', {
+            transactionId: paymentResult.transactionId,
+            amount: paymentResult.amount,
+            processedAt: new Date()
+          });
+
+          // 发送支付完成通知
+          this.sendPaymentCompletedNotifications(paymentResult);
+          
+        } else {
+          console.error('小程序支付自动化流程处理失败');
+          this.createAutomationLog(paymentResult.settlementId, 'miniprogram_payment_failed', {
+            transactionId: paymentResult.transactionId,
+            error: '自动化流程处理失败'
+          });
+        }
+      },
+      error: (error) => {
+        console.error('小程序支付自动化流程出错:', error);
+        this.createAutomationLog(paymentResult.settlementId, 'miniprogram_payment_error', {
+          transactionId: paymentResult.transactionId,
+          error: error.message
+        });
+      }
+    });
+  }
+
+  /**
+   * 发送支付完成通知
+   */
+  private sendPaymentCompletedNotifications(paymentResult: MiniprogramPaymentResult): void {
+    console.log('发送支付完成通知...');
+
+    // 获取客户信息(模拟)
+    const customerInfo = this.getCustomerInfo(paymentResult.settlementId);
+    
+    // 发送支付完成通知
+    this.notificationService.sendPaymentCompletedNotification({
+      recipient: customerInfo.phone,
+      paymentMethod: '小程序支付', // 使用固定值而不是paymentResult.paymentMethod
+      amount: paymentResult.amount,
+      customerName: customerInfo.name,
+      projectName: customerInfo.projectName,
+      channels: [NotificationChannel.SMS, NotificationChannel.WECHAT, NotificationChannel.IN_APP]
+    }).subscribe({
+      next: (result) => {
+        if (result.success) {
+          console.log('支付完成通知发送成功:', result);
+          
+          // 发送大图解锁通知
+          this.sendImageUnlockedNotifications(paymentResult, customerInfo);
+        } else {
+          console.error('支付完成通知发送失败:', result.error);
+        }
+      },
+      error: (error) => {
+        console.error('支付完成通知发送出错:', error);
+      }
+    });
+  }
+
+  /**
+   * 发送大图解锁通知
+   */
+  private sendImageUnlockedNotifications(paymentResult: MiniprogramPaymentResult, customerInfo: any): void {
+    console.log('发送大图解锁通知...');
+
+    this.notificationService.sendImageUnlockedNotification({
+      recipient: customerInfo.phone,
+      customerName: customerInfo.name,
+      projectName: customerInfo.projectName,
+      imageCount: customerInfo.imageCount || 8,
+      resolution: '4K高清',
+      validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString(), // 30天有效期
+      downloadLink: `https://download.yinsanse.com/project/${paymentResult.settlementId}`,
+      channels: [NotificationChannel.SMS, NotificationChannel.EMAIL, NotificationChannel.IN_APP]
+    }).subscribe({
+      next: (result) => {
+        if (result.success) {
+          console.log('大图解锁通知发送成功:', result);
+        } else {
+          console.error('大图解锁通知发送失败:', result.error);
+        }
+      },
+      error: (error) => {
+        console.error('大图解锁通知发送出错:', error);
+      }
+    });
+  }
+
+  /**
+   * 获取客户信息(模拟)
+   */
+  private getCustomerInfo(settlementId: string): {
+    name: string;
+    phone: string;
+    email: string;
+    projectName: string;
+    imageCount: number;
+  } {
+    // 模拟客户信息
+    return {
+      name: '张先生',
+      phone: '138****8888',
+      email: 'customer@example.com',
+      projectName: '现代简约三居室设计',
+      imageCount: 8
+    };
+  }
+
+  /**
+   * 创建自动化日志
+   */
+  private createAutomationLog(settlementId: string, type: string, data: any): void {
+    const log = {
+      id: `log_${Date.now()}`,
+      settlementId,
+      type,
+      data,
+      timestamp: new Date()
+    };
+
+    console.log('创建自动化日志:', log);
+    // 实际实现中会保存到数据库或日志系统
+  }
+
+  /**
+   * 手动触发小程序支付测试流程
+   */
+  triggerMiniprogramPaymentTest(settlementId: string, amount: number): Observable<boolean> {
+    console.log(`触发小程序支付测试流程: ${settlementId}, 金额: ${amount}`);
+    return this.miniprogramPaymentService.triggerTestPaymentFlow(settlementId, amount);
+  }
+
+  /**
+   * 获取小程序支付自动化状态
+   */
+  getMiniprogramPaymentAutomationStatus(): {
+    isProcessing: boolean;
+    processingQueue: string[];
+    supportedMethods: string[];
+  } {
+    return {
+      isProcessing: this.miniprogramPaymentService.getProcessingStatus(),
+      processingQueue: this.miniprogramPaymentService.getProcessingQueue(),
+      supportedMethods: this.miniprogramPaymentService.getSupportedPaymentMethods()
+    };
+  }
+
+  /**
+   * 停止小程序支付自动化监听
+   */
+  stopMiniprogramPaymentAutomation(): void {
+    console.log('停止小程序支付自动化监听');
+    this.miniprogramPaymentService.stopAutomationListener();
+  }
+}

+ 314 - 0
src/app/services/miniprogram-payment.service.ts

@@ -0,0 +1,314 @@
+import { Injectable, signal } from '@angular/core';
+import { Observable, of, delay, switchMap, catchError } from 'rxjs';
+
+export interface MiniprogramPaymentResult {
+  success: boolean;
+  transactionId: string;
+  amount: number;
+  paymentTime: Date;
+  payerInfo: {
+    openId: string;
+    nickname?: string;
+    avatar?: string;
+  };
+  settlementId: string;
+  error?: string;
+}
+
+export interface ImageDecryptionResult {
+  success: boolean;
+  decryptedImageUrl: string;
+  originalImageId: string;
+  decryptionTime: Date;
+  error?: string;
+}
+
+export interface NotificationResult {
+  success: boolean;
+  messageId: string;
+  sentTime: Date;
+  recipient: string;
+  error?: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class MiniprogramPaymentService {
+  private isProcessing = signal(false);
+  private paymentQueue = signal<string[]>([]);
+
+  constructor() {}
+
+  /**
+   * 监听小程序支付完成事件
+   */
+  onPaymentCompleted(): Observable<MiniprogramPaymentResult> {
+    // 模拟支付完成事件监听
+    return new Observable(observer => {
+      // 实际实现中会监听微信小程序支付回调
+      console.log('开始监听小程序支付完成事件...');
+      
+      // 模拟支付完成事件
+      setTimeout(() => {
+        const mockPaymentResult: MiniprogramPaymentResult = {
+          success: true,
+          transactionId: `MP${Date.now()}`,
+          amount: Math.floor(Math.random() * 50000) + 10000,
+          paymentTime: new Date(),
+          payerInfo: {
+            openId: `openid_${Date.now()}`,
+            nickname: '客户用户',
+            avatar: 'https://example.com/avatar.jpg'
+          },
+          settlementId: `settlement_${Date.now()}`
+        };
+        
+        observer.next(mockPaymentResult);
+      }, 2000);
+    });
+  }
+
+  /**
+   * 处理支付完成后的自动化流程
+   */
+  processPaymentCompletedFlow(paymentResult: MiniprogramPaymentResult): Observable<boolean> {
+    if (!paymentResult.success) {
+      return of(false);
+    }
+
+    this.isProcessing.set(true);
+    this.paymentQueue.update(queue => [...queue, paymentResult.settlementId]);
+
+    console.log(`开始处理支付完成流程: ${paymentResult.transactionId}`);
+
+    return this.decryptAndSendImages(paymentResult).pipe(
+      switchMap(decryptResult => {
+        if (decryptResult.success) {
+          return this.sendPaymentCompletedNotification(paymentResult, decryptResult);
+        }
+        return of({ success: false, error: '图片解密失败' });
+      }),
+      switchMap(notificationResult => {
+        if (notificationResult.success) {
+          return this.updateSettlementStatus(paymentResult.settlementId, 'completed');
+        }
+        return of(false);
+      }),
+      catchError(error => {
+        console.error('支付完成流程处理失败:', error);
+        return of(false);
+      }),
+      delay(1000), // 模拟处理时间
+      switchMap(result => {
+        this.isProcessing.set(false);
+        this.paymentQueue.update(queue => 
+          queue.filter(id => id !== paymentResult.settlementId)
+        );
+        return of(result);
+      })
+    );
+  }
+
+  /**
+   * 解密并发送大图给客户
+   */
+  private decryptAndSendImages(paymentResult: MiniprogramPaymentResult): Observable<ImageDecryptionResult> {
+    console.log(`开始解密图片,结算ID: ${paymentResult.settlementId}`);
+
+    return new Observable<ImageDecryptionResult>(observer => {
+      // 模拟图片解密过程
+      setTimeout(() => {
+        const decryptResult: ImageDecryptionResult = {
+          success: true,
+          decryptedImageUrl: `https://example.com/decrypted/${paymentResult.settlementId}/high-res-image.jpg`,
+          originalImageId: `img_${paymentResult.settlementId}`,
+          decryptionTime: new Date()
+        };
+
+        console.log('图片解密完成:', decryptResult);
+        observer.next(decryptResult);
+        observer.complete();
+      }, 1500);
+    }).pipe(
+      catchError(error => {
+        console.error('图片解密失败:', error);
+        return of({
+          success: false,
+          decryptedImageUrl: '',
+          originalImageId: '',
+          decryptionTime: new Date(),
+          error: error.message
+        });
+      })
+    );
+  }
+
+  /**
+   * 发送支付完成通知
+   */
+  private sendPaymentCompletedNotification(
+    paymentResult: MiniprogramPaymentResult, 
+    decryptResult: ImageDecryptionResult
+  ): Observable<NotificationResult> {
+    console.log('发送支付完成通知...');
+
+    const notificationContent = {
+      title: '尾款支付成功',
+      message: `您的尾款 ¥${paymentResult.amount} 已成功支付,大图已解锁并发送至您的微信。`,
+      imageUrl: decryptResult.decryptedImageUrl,
+      paymentInfo: {
+        amount: paymentResult.amount,
+        transactionId: paymentResult.transactionId,
+        paymentTime: paymentResult.paymentTime
+      }
+    };
+
+    return new Observable<NotificationResult>(observer => {
+      // 模拟发送通知
+      setTimeout(() => {
+        const notificationResult: NotificationResult = {
+          success: true,
+          messageId: `msg_${Date.now()}`,
+          sentTime: new Date(),
+          recipient: paymentResult.payerInfo.openId
+        };
+
+        console.log('通知发送成功:', notificationResult);
+        console.log('通知内容:', notificationContent);
+        
+        observer.next(notificationResult);
+        observer.complete();
+      }, 800);
+    }).pipe(
+      catchError(error => {
+        console.error('通知发送失败:', error);
+        return of({
+          success: false,
+          messageId: '',
+          sentTime: new Date(),
+          recipient: paymentResult.payerInfo.openId,
+          error: error.message
+        });
+      })
+    );
+  }
+
+  /**
+   * 更新结算状态
+   */
+  private updateSettlementStatus(settlementId: string, status: string): Observable<boolean> {
+    console.log(`更新结算状态: ${settlementId} -> ${status}`);
+
+    return new Observable<boolean>(observer => {
+      // 模拟更新结算状态
+      setTimeout(() => {
+        console.log('结算状态更新成功');
+        observer.next(true);
+        observer.complete();
+      }, 500);
+    }).pipe(
+      catchError(error => {
+        console.error('结算状态更新失败:', error);
+        return of(false);
+      })
+    );
+  }
+
+  /**
+   * 启动自动化监听
+   */
+  startAutomationListener(): void {
+    console.log('启动小程序支付自动化监听...');
+    
+    this.onPaymentCompleted().subscribe(paymentResult => {
+      console.log('检测到支付完成事件:', paymentResult);
+      
+      this.processPaymentCompletedFlow(paymentResult).subscribe(success => {
+        if (success) {
+          console.log('自动化流程处理成功');
+        } else {
+          console.error('自动化流程处理失败');
+        }
+      });
+    });
+  }
+
+  /**
+   * 停止自动化监听
+   */
+  stopAutomationListener(): void {
+    console.log('停止小程序支付自动化监听');
+    this.isProcessing.set(false);
+    this.paymentQueue.set([]);
+  }
+
+  /**
+   * 获取处理状态
+   */
+  getProcessingStatus(): boolean {
+    return this.isProcessing();
+  }
+
+  /**
+   * 获取处理队列
+   */
+  getProcessingQueue(): string[] {
+    return this.paymentQueue();
+  }
+
+  /**
+   * 手动触发支付完成流程(用于测试)
+   */
+  triggerTestPaymentFlow(settlementId: string, amount: number): Observable<boolean> {
+    const mockPaymentResult: MiniprogramPaymentResult = {
+      success: true,
+      transactionId: `TEST_${Date.now()}`,
+      amount: amount,
+      paymentTime: new Date(),
+      payerInfo: {
+        openId: `test_openid_${Date.now()}`,
+        nickname: '测试用户',
+        avatar: 'https://example.com/test-avatar.jpg'
+      },
+      settlementId: settlementId
+    };
+
+    return this.processPaymentCompletedFlow(mockPaymentResult);
+  }
+
+  /**
+   * 获取支持的支付方式
+   */
+  getSupportedPaymentMethods(): string[] {
+    return ['微信小程序支付', '微信H5支付', '微信扫码支付'];
+  }
+
+  /**
+   * 验证支付结果
+   */
+  validatePaymentResult(paymentResult: MiniprogramPaymentResult): { valid: boolean, errors: string[] } {
+    const errors: string[] = [];
+
+    if (!paymentResult.transactionId) {
+      errors.push('缺少交易ID');
+    }
+
+    if (!paymentResult.amount || paymentResult.amount <= 0) {
+      errors.push('支付金额无效');
+    }
+
+    if (!paymentResult.payerInfo?.openId) {
+      errors.push('缺少支付者信息');
+    }
+
+    if (!paymentResult.settlementId) {
+      errors.push('缺少结算ID');
+    }
+
+    return {
+      valid: errors.length === 0,
+      errors
+    };
+  }
+}

+ 426 - 0
src/app/services/notification.service.ts

@@ -0,0 +1,426 @@
+import { Injectable, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, of, BehaviorSubject } from 'rxjs';
+import { delay, map, catchError } from 'rxjs/operators';
+
+// 通知类型枚举
+export enum NotificationType {
+  PAYMENT_COMPLETED = 'payment_completed',
+  IMAGE_UNLOCKED = 'image_unlocked',
+  SETTLEMENT_CONFIRMED = 'settlement_confirmed',
+  REMINDER = 'reminder',
+  SYSTEM = 'system'
+}
+
+// 通知渠道枚举
+export enum NotificationChannel {
+  SMS = 'sms',
+  EMAIL = 'email',
+  WECHAT = 'wechat',
+  PUSH = 'push',
+  IN_APP = 'in_app'
+}
+
+// 通知接口
+export interface Notification {
+  id: string;
+  type: NotificationType;
+  title: string;
+  content: string;
+  recipient: string;
+  channels: NotificationChannel[];
+  templateData?: any;
+  scheduledAt?: Date;
+  sentAt?: Date;
+  status: 'pending' | 'sent' | 'failed' | 'cancelled';
+  retryCount?: number;
+  metadata?: any;
+}
+
+// 通知模板接口
+export interface NotificationTemplate {
+  id: string;
+  type: NotificationType;
+  name: string;
+  title: string;
+  content: string;
+  channels: NotificationChannel[];
+  variables: string[];
+}
+
+// 通知发送结果接口
+export interface NotificationResult {
+  success: boolean;
+  notificationId: string;
+  sentChannels: NotificationChannel[];
+  failedChannels: NotificationChannel[];
+  error?: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class NotificationService {
+  private http = inject(HttpClient);
+  private notifications$ = new BehaviorSubject<Notification[]>([]);
+  
+  // 预定义通知模板
+  private templates: NotificationTemplate[] = [
+    {
+      id: 'payment_completed',
+      type: NotificationType.PAYMENT_COMPLETED,
+      name: '支付完成通知',
+      title: '🎉 尾款已到账,大图已解锁!',
+      content: `
+        亲爱的客户,您好!
+        
+        您的尾款支付已确认到账:
+        • 支付方式:{{paymentMethod}}
+        • 支付金额:¥{{amount}}
+        • 处理时间:{{processedAt}}
+        
+        🎨 高清渲染图已为您解锁,您现在可以:
+        • 下载所有高清渲染图
+        • 查看全景漫游链接
+        • 获取设计方案文件
+        
+        感谢您选择银三色摄影工作室!
+        如有任何问题,请随时联系我们。
+      `,
+      channels: [NotificationChannel.SMS, NotificationChannel.WECHAT, NotificationChannel.IN_APP],
+      variables: ['paymentMethod', 'amount', 'processedAt', 'customerName', 'projectName']
+    },
+    {
+      id: 'image_unlocked',
+      type: NotificationType.IMAGE_UNLOCKED,
+      name: '大图解锁通知',
+      title: '📸 您的高清渲染图已解锁',
+      content: `
+        {{customerName}},您好!
+        
+        项目"{{projectName}}"的高清渲染图已为您解锁:
+        • 解锁图片数量:{{imageCount}}张
+        • 图片分辨率:{{resolution}}
+        • 下载有效期:{{validUntil}}
+        
+        立即下载:{{downloadLink}}
+        
+        银三色摄影工作室
+      `,
+      channels: [NotificationChannel.SMS, NotificationChannel.EMAIL, NotificationChannel.IN_APP],
+      variables: ['customerName', 'projectName', 'imageCount', 'resolution', 'validUntil', 'downloadLink']
+    },
+    {
+      id: 'settlement_reminder',
+      type: NotificationType.REMINDER,
+      name: '结算提醒',
+      title: '💰 尾款结算提醒',
+      content: `
+        {{customerName}},您好!
+        
+        您的项目"{{projectName}}"已完成,请及时结算尾款:
+        • 应付金额:¥{{amount}}
+        • 截止日期:{{dueDate}}
+        
+        支付完成后,我们将立即为您解锁高清渲染图。
+        
+        银三色摄影工作室
+      `,
+      channels: [NotificationChannel.SMS, NotificationChannel.WECHAT],
+      variables: ['customerName', 'projectName', 'amount', 'dueDate']
+    }
+  ];
+
+  /**
+   * 发送支付完成通知
+   */
+  sendPaymentCompletedNotification(data: {
+    recipient: string;
+    paymentMethod: string;
+    amount: number;
+    customerName: string;
+    projectName: string;
+    channels?: NotificationChannel[];
+  }): Observable<NotificationResult> {
+    console.log('发送支付完成通知:', data);
+
+    const template = this.getTemplate(NotificationType.PAYMENT_COMPLETED);
+    if (!template) {
+      return of({
+        success: false,
+        notificationId: '',
+        sentChannels: [],
+        failedChannels: [],
+        error: '未找到通知模板'
+      });
+    }
+
+    const notification: Notification = {
+      id: `notification_${Date.now()}`,
+      type: NotificationType.PAYMENT_COMPLETED,
+      title: template.title,
+      content: this.renderTemplate(template.content, {
+        paymentMethod: data.paymentMethod,
+        amount: data.amount.toString(),
+        processedAt: new Date().toLocaleString(),
+        customerName: data.customerName,
+        projectName: data.projectName
+      }),
+      recipient: data.recipient,
+      channels: data.channels || template.channels,
+      templateData: data,
+      status: 'pending'
+    };
+
+    return this.sendNotification(notification);
+  }
+
+  /**
+   * 发送大图解锁通知
+   */
+  sendImageUnlockedNotification(data: {
+    recipient: string;
+    customerName: string;
+    projectName: string;
+    imageCount: number;
+    resolution: string;
+    validUntil: string;
+    downloadLink: string;
+    channels?: NotificationChannel[];
+  }): Observable<NotificationResult> {
+    console.log('发送大图解锁通知:', data);
+
+    const template = this.getTemplate(NotificationType.IMAGE_UNLOCKED);
+    if (!template) {
+      return of({
+        success: false,
+        notificationId: '',
+        sentChannels: [],
+        failedChannels: [],
+        error: '未找到通知模板'
+      });
+    }
+
+    const notification: Notification = {
+      id: `notification_${Date.now()}`,
+      type: NotificationType.IMAGE_UNLOCKED,
+      title: template.title,
+      content: this.renderTemplate(template.content, data),
+      recipient: data.recipient,
+      channels: data.channels || template.channels,
+      templateData: data,
+      status: 'pending'
+    };
+
+    return this.sendNotification(notification);
+  }
+
+  /**
+   * 发送结算提醒通知
+   */
+  sendSettlementReminderNotification(data: {
+    recipient: string;
+    customerName: string;
+    projectName: string;
+    amount: number;
+    dueDate: string;
+    channels?: NotificationChannel[];
+  }): Observable<NotificationResult> {
+    console.log('发送结算提醒通知:', data);
+
+    const template = this.getTemplate(NotificationType.REMINDER);
+    if (!template) {
+      return of({
+        success: false,
+        notificationId: '',
+        sentChannels: [],
+        failedChannels: [],
+        error: '未找到通知模板'
+      });
+    }
+
+    const notification: Notification = {
+      id: `notification_${Date.now()}`,
+      type: NotificationType.REMINDER,
+      title: template.title,
+      content: this.renderTemplate(template.content, {
+        customerName: data.customerName,
+        projectName: data.projectName,
+        amount: data.amount.toString(),
+        dueDate: data.dueDate
+      }),
+      recipient: data.recipient,
+      channels: data.channels || template.channels,
+      templateData: data,
+      status: 'pending'
+    };
+
+    return this.sendNotification(notification);
+  }
+
+  /**
+   * 发送通知
+   */
+  private sendNotification(notification: Notification): Observable<NotificationResult> {
+    console.log('发送通知:', notification);
+
+    // 模拟发送过程
+    return of(null).pipe(
+      delay(1500), // 模拟网络延迟
+      map(() => {
+        // 模拟发送结果
+        const successRate = 0.95; // 95%成功率
+        const success = Math.random() < successRate;
+
+        if (success) {
+          notification.status = 'sent';
+          notification.sentAt = new Date();
+
+          // 添加到通知历史
+          this.addToNotificationHistory(notification);
+
+          return {
+            success: true,
+            notificationId: notification.id,
+            sentChannels: notification.channels,
+            failedChannels: [],
+          };
+        } else {
+          notification.status = 'failed';
+          notification.retryCount = (notification.retryCount || 0) + 1;
+
+          return {
+            success: false,
+            notificationId: notification.id,
+            sentChannels: [],
+            failedChannels: notification.channels,
+            error: '网络错误或服务暂时不可用'
+          };
+        }
+      }),
+      catchError(error => {
+        console.error('通知发送失败:', error);
+        notification.status = 'failed';
+        
+        return of({
+          success: false,
+          notificationId: notification.id,
+          sentChannels: [],
+          failedChannels: notification.channels,
+          error: error.message || '发送失败'
+        });
+      })
+    );
+  }
+
+  /**
+   * 获取通知模板
+   */
+  private getTemplate(type: NotificationType): NotificationTemplate | undefined {
+    return this.templates.find(t => t.type === type);
+  }
+
+  /**
+   * 渲染模板内容
+   */
+  private renderTemplate(template: string, data: any): string {
+    let rendered = template;
+    
+    Object.keys(data).forEach(key => {
+      const placeholder = `{{${key}}}`;
+      rendered = rendered.replace(new RegExp(placeholder, 'g'), data[key]);
+    });
+
+    return rendered;
+  }
+
+  /**
+   * 添加到通知历史
+   */
+  private addToNotificationHistory(notification: Notification): void {
+    const current = this.notifications$.value;
+    this.notifications$.next([notification, ...current]);
+  }
+
+  /**
+   * 获取通知历史
+   */
+  getNotificationHistory(): Observable<Notification[]> {
+    return this.notifications$.asObservable();
+  }
+
+  /**
+   * 获取通知统计
+   */
+  getNotificationStatistics(): Observable<{
+    total: number;
+    sent: number;
+    failed: number;
+    pending: number;
+    byType: { [key: string]: number };
+    byChannel: { [key: string]: number };
+  }> {
+    return this.notifications$.pipe(
+      map(notifications => {
+        const total = notifications.length;
+        const sent = notifications.filter(n => n.status === 'sent').length;
+        const failed = notifications.filter(n => n.status === 'failed').length;
+        const pending = notifications.filter(n => n.status === 'pending').length;
+
+        const byType: { [key: string]: number } = {};
+        const byChannel: { [key: string]: number } = {};
+
+        notifications.forEach(n => {
+          byType[n.type] = (byType[n.type] || 0) + 1;
+          n.channels.forEach(channel => {
+            byChannel[channel] = (byChannel[channel] || 0) + 1;
+          });
+        });
+
+        return {
+          total,
+          sent,
+          failed,
+          pending,
+          byType,
+          byChannel
+        };
+      })
+    );
+  }
+
+  /**
+   * 重试失败的通知
+   */
+  retryFailedNotification(notificationId: string): Observable<NotificationResult> {
+    const notifications = this.notifications$.value;
+    const notification = notifications.find(n => n.id === notificationId);
+
+    if (!notification || notification.status !== 'failed') {
+      return of({
+        success: false,
+        notificationId,
+        sentChannels: [],
+        failedChannels: [],
+        error: '通知不存在或状态不正确'
+      });
+    }
+
+    notification.status = 'pending';
+    return this.sendNotification(notification);
+  }
+
+  /**
+   * 获取支持的通知渠道
+   */
+  getSupportedChannels(): NotificationChannel[] {
+    return Object.values(NotificationChannel);
+  }
+
+  /**
+   * 获取通知模板列表
+   */
+  getTemplates(): NotificationTemplate[] {
+    return [...this.templates];
+  }
+}

+ 410 - 0
src/app/services/payment-voucher-recognition.service.ts

@@ -0,0 +1,410 @@
+import { Injectable, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, of } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+
+// 支付凭证识别结果接口
+export interface PaymentVoucherRecognitionResult {
+  success: boolean;
+  amount?: number;
+  paymentDate?: Date;
+  paymentMethod?: string;
+  payerName?: string;
+  payerAccount?: string;
+  receiverName?: string;
+  receiverAccount?: string;
+  transactionNumber?: string;
+  confidence: number;
+  extractedText?: string;
+  error?: string;
+}
+
+// 支付凭证文件信息接口
+export interface PaymentVoucherFile {
+  id: string;
+  fileName: string;
+  fileType: string;
+  fileSize: number;
+  fileUrl: string;
+  previewUrl?: string;
+  uploadDate: Date;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class PaymentVoucherRecognitionService {
+  private http = inject(HttpClient);
+
+  /**
+   * 识别支付凭证图片或PDF文件
+   * @param file 支付凭证文件
+   * @returns 识别结果的可观察对象
+   */
+  recognizePaymentVoucher(file: File): Observable<PaymentVoucherRecognitionResult> {
+    // 检查文件类型
+    if (!this.isSupportedFileType(file.type)) {
+      return of({
+        success: false,
+        confidence: 0,
+        error: '不支持的文件类型。请上传图片(JPG, PNG, GIF)或PDF文件。'
+      });
+    }
+
+    // 检查文件大小(限制为10MB)
+    if (file.size > 10 * 1024 * 1024) {
+      return of({
+        success: false,
+        confidence: 0,
+        error: '文件大小不能超过10MB。'
+      });
+    }
+
+    // 在实际应用中,这里会调用OCR API服务
+    // 目前先模拟识别过程
+    return this.simulateRecognition(file);
+  }
+
+  /**
+   * 批量识别支付凭证
+   * @param files 支付凭证文件数组
+   * @returns 批量识别结果的可观察对象
+   */
+  async recognizePaymentVouchers(files: File[]): Promise<PaymentVoucherRecognitionResult[]> {
+    const recognitionPromises = files.map(file => 
+      this.recognizePaymentVoucher(file).toPromise()
+    );
+
+    const results = await Promise.all(recognitionPromises);
+    return results.filter(result => result !== undefined) as PaymentVoucherRecognitionResult[];
+  }
+
+  /**
+   * 检查是否支持的文件类型
+   * @param fileType 文件MIME类型
+   */
+  private isSupportedFileType(fileType: string): boolean {
+    const supportedTypes = [
+      'image/jpeg',
+      'image/png', 
+      'image/gif',
+      'application/pdf'
+    ];
+    return supportedTypes.includes(fileType);
+  }
+
+  /**
+   * 模拟支付凭证识别过程
+   * @param file 支付凭证文件
+   */
+  private simulateRecognition(file: File): Observable<PaymentVoucherRecognitionResult> {
+    return new Observable(observer => {
+      // 模拟处理延迟
+      setTimeout(() => {
+        try {
+          // 根据文件名和类型生成模拟识别结果
+          const fileName = file.name.toLowerCase();
+          
+          // 模拟不同的识别场景
+          if (fileName.includes('alipay') || fileName.includes('支付宝')) {
+            observer.next(this.generateAlipayResult(file));
+          } else if (fileName.includes('wechat') || fileName.includes('微信')) {
+            observer.next(this.generateWechatResult(file));
+          } else if (fileName.includes('bank') || fileName.includes('银行')) {
+            observer.next(this.generateBankTransferResult(file));
+          } else {
+            observer.next(this.generateGenericResult(file));
+          }
+          
+          observer.complete();
+        } catch (error) {
+          observer.next({
+            success: false,
+            confidence: 0,
+            error: `识别过程中发生错误: ${error}`
+          });
+          observer.complete();
+        }
+      }, 2000); // 2秒延迟模拟处理时间
+    });
+  }
+
+  /**
+   * 生成支付宝支付凭证识别结果
+   */
+  private generateAlipayResult(file: File): PaymentVoucherRecognitionResult {
+    const amount = Math.round(Math.random() * 10000 + 1000); // 1000-11000之间的随机金额
+    const confidence = Math.random() * 0.3 + 0.7; // 70%-100%的置信度
+    
+    return {
+      success: true,
+      amount,
+      paymentDate: new Date(),
+      paymentMethod: '支付宝',
+      payerName: '付款人***',
+      payerAccount: 'alipay***@xxx.com',
+      receiverName: '收款人***',
+      receiverAccount: 'alipay***@xxx.com',
+      transactionNumber: `ALP${Date.now().toString().slice(-8)}`,
+      confidence,
+      extractedText: `支付宝交易凭证 金额: ${amount}元 交易时间: ${new Date().toLocaleString()}`
+    };
+  }
+
+  /**
+   * 生成微信支付凭证识别结果
+   */
+  private generateWechatResult(file: File): PaymentVoucherRecognitionResult {
+    const amount = Math.round(Math.random() * 5000 + 500); // 500-5500之间的随机金额
+    const confidence = Math.random() * 0.3 + 0.7; // 70%-100%的置信度
+    
+    return {
+      success: true,
+      amount,
+      paymentDate: new Date(),
+      paymentMethod: '微信支付',
+      payerName: '微信用户***',
+      payerAccount: 'wxid_***',
+      receiverName: '商户***',
+      receiverAccount: 'mch_***',
+      transactionNumber: `WX${Date.now().toString().slice(-8)}`,
+      confidence,
+      extractedText: `微信支付凭证 金额: ${amount}元 交易时间: ${new Date().toLocaleString()}`
+    };
+  }
+
+  /**
+   * 生成银行转账凭证识别结果
+   */
+  private generateBankTransferResult(file: File): PaymentVoucherRecognitionResult {
+    const amount = Math.round(Math.random() * 20000 + 2000); // 2000-22000之间的随机金额
+    const confidence = Math.random() * 0.2 + 0.8; // 80%-100%的置信度
+    
+    return {
+      success: true,
+      amount,
+      paymentDate: new Date(),
+      paymentMethod: '银行转账',
+      payerName: '付款人***',
+      payerAccount: '6222********1234',
+      receiverName: '收款人***',
+      receiverAccount: '6222********5678',
+      transactionNumber: `BANK${Date.now().toString().slice(-8)}`,
+      confidence,
+      extractedText: `银行转账凭证 金额: ${amount}元 交易时间: ${new Date().toLocaleString()}`
+    };
+  }
+
+  /**
+   * 生成通用支付凭证识别结果
+   */
+  private generateGenericResult(file: File): PaymentVoucherRecognitionResult {
+    const amount = Math.round(Math.random() * 15000 + 1000); // 1000-16000之间的随机金额
+    const confidence = Math.random() * 0.4 + 0.6; // 60%-100%的置信度
+    
+    const methods = ['支付宝', '微信支付', '银行转账', '现金'];
+    const paymentMethod = methods[Math.floor(Math.random() * methods.length)];
+    
+    return {
+      success: true,
+      amount,
+      paymentDate: new Date(),
+      paymentMethod,
+      payerName: '付款人***',
+      receiverName: '收款人***',
+      confidence,
+      extractedText: `支付凭证 金额: ${amount}元 支付方式: ${paymentMethod} 交易时间: ${new Date().toLocaleString()}`
+    };
+  }
+
+  /**
+   * 验证识别结果的可信度
+   * @param result 识别结果
+   * @param expectedAmount 预期金额(可选)
+   */
+  validateRecognitionResult(
+    result: PaymentVoucherRecognitionResult, 
+    expectedAmount?: number
+  ): { isValid: boolean; reasons: string[] } {
+    const reasons: string[] = [];
+    
+    if (!result.success) {
+      reasons.push('识别失败');
+      return { isValid: false, reasons };
+    }
+    
+    if (result.confidence < 0.7) {
+      reasons.push(`置信度过低: ${(result.confidence * 100).toFixed(1)}%`);
+    }
+    
+    if (!result.amount || result.amount <= 0) {
+      reasons.push('金额识别无效');
+    }
+    
+    if (expectedAmount && result.amount && Math.abs(result.amount - expectedAmount) > expectedAmount * 0.1) {
+      reasons.push(`识别金额与预期金额差异过大`);
+    }
+    
+    if (!result.paymentDate) {
+      reasons.push('支付日期识别失败');
+    }
+    
+    if (!result.paymentMethod) {
+      reasons.push('支付方式识别失败');
+    }
+    
+    return {
+      isValid: reasons.length === 0,
+      reasons
+    };
+  }
+
+  /**
+   * 格式化金额显示
+   * @param amount 金额
+   */
+  formatAmount(amount: number): string {
+    return new Intl.NumberFormat('zh-CN', {
+      style: 'currency',
+      currency: 'CNY'
+    }).format(amount);
+  }
+
+  /**
+   * 获取支持的文件类型描述
+   */
+  getSupportedFileTypesDescription(): string {
+    return '支持的文件类型: JPG, PNG, GIF图片或PDF文件,最大10MB';
+  }
+
+  /**
+   * 获取支持的文件类型列表
+   */
+  getSupportedFileTypes(): string[] {
+    return [
+      'image/jpeg',
+      'image/png', 
+      'image/gif',
+      'application/pdf'
+    ];
+  }
+
+  /**
+   * 验证文件
+   * @param file 要验证的文件
+   */
+  validateFile(file: File): { isValid: boolean; error?: string } {
+    if (!this.isSupportedFileType(file.type)) {
+      return {
+        isValid: false,
+        error: '不支持的文件类型。请上传图片(JPG, PNG, GIF)或PDF文件。'
+      };
+    }
+
+    if (file.size > 10 * 1024 * 1024) {
+      return {
+        isValid: false,
+        error: '文件大小不能超过10MB。'
+      };
+    }
+
+    return { isValid: true };
+  }
+
+  /**
+   * 处理支付凭证上传并自动识别
+   */
+  processPaymentVoucherUpload(file: File, settlementId: string): Observable<{
+    success: boolean;
+    recognitionResult?: PaymentVoucherRecognitionResult;
+    error?: string;
+  }> {
+    console.log(`开始处理支付凭证上传: ${file.name}, 结算ID: ${settlementId}`);
+
+    // 验证文件
+    const validation = this.validateFile(file);
+    if (!validation.isValid) {
+      return of({
+        success: false,
+        error: validation.error
+      });
+    }
+
+    // 执行识别并返回结果
+    return this.recognizePaymentVoucher(file).pipe(
+      map(recognitionResult => {
+        if (recognitionResult.success) {
+          // 自动更新结算状态
+          this.updateSettlementWithRecognitionResult(settlementId, recognitionResult);
+          
+          return {
+            success: true,
+            recognitionResult
+          };
+        } else {
+          return {
+            success: false,
+            error: recognitionResult.error || '识别失败'
+          };
+        }
+      }),
+      catchError(error => {
+        console.error('支付凭证处理出错:', error);
+        return of({
+          success: false,
+          error: '处理过程中出现错误'
+        });
+      })
+    );
+  }
+
+  /**
+   * 根据识别结果更新结算状态
+   */
+  private updateSettlementWithRecognitionResult(settlementId: string, result: PaymentVoucherRecognitionResult): void {
+    console.log(`更新结算状态: ${settlementId}`, result);
+    
+    // 模拟更新结算记录
+    const updateData = {
+      settlementId,
+      paymentMethod: result.paymentMethod,
+      amount: result.amount,
+      transactionNumber: result.transactionNumber,
+      recognizedAt: new Date(),
+      status: 'payment_confirmed'
+    };
+
+    console.log('结算状态更新数据:', updateData);
+    // 实际实现中会调用后端API更新数据库
+  }
+
+  /**
+   * 获取支持的支付方式列表
+   */
+  getSupportedPaymentMethods(): string[] {
+    return ['微信支付', '支付宝', '银行转账', '现金'];
+  }
+
+  /**
+   * 获取识别统计信息
+   */
+  getRecognitionStatistics(): {
+    totalProcessed: number;
+    successRate: number;
+    methodDistribution: { [key: string]: number };
+    averageProcessingTime: number;
+  } {
+    // 模拟统计数据
+    return {
+      totalProcessed: 156,
+      successRate: 0.94,
+      methodDistribution: {
+        '微信支付': 78,
+        '支付宝': 65,
+        '银行转账': 10,
+        '现金': 3
+      },
+      averageProcessingTime: 1.8 // 秒
+    };
+  }
+}

+ 10 - 0
src/app/services/project.service.ts

@@ -646,6 +646,12 @@ export class ProjectService {
     return of(this.settlements);
   }
 
+  // 新增:创建结算记录(用于自动发起尾款结算申请)
+  addSettlement(settlement: Settlement): Observable<void> {
+    this.settlements.push(settlement);
+    return of(void 0);
+  }
+
   // 获取技能标签
   getSkillTags(): Observable<SkillTag[]> {
     return of(this.skillTags);
@@ -791,6 +797,10 @@ export class ProjectService {
       preferenceTags?: string[];
       followUpStatus?: string;
     };
+    // 可选扩展字段:报价与指派信息
+    quotation?: any;
+    assignment?: any;
+    orderAmount?: number;
   }): Observable<{ success: boolean; projectId: string }> {
     // 模拟API调用
     console.log('创建项目:', projectData);

+ 435 - 0
src/app/services/quotation-approval.service.ts

@@ -0,0 +1,435 @@
+import { Injectable, signal } from '@angular/core';
+import { Observable, of, BehaviorSubject } from 'rxjs';
+import { delay, map } from 'rxjs/operators';
+
+// 报价审核状态
+export type QuotationApprovalStatus = 'pending' | 'approved' | 'rejected' | 'revision_required';
+
+// 报价审核记录
+export interface QuotationApproval {
+  id: string;
+  quotationId: string;
+  projectId: string;
+  projectName: string;
+  customerName: string;
+  submittedBy: string; // 提交人
+  submittedAt: Date;
+  totalAmount: number;
+  status: QuotationApprovalStatus;
+  approvedBy?: string; // 审批人
+  approvedAt?: Date;
+  rejectionReason?: string;
+  revisionNotes?: string;
+  scriptUsed?: string; // 使用的话术模板ID
+  communicationLog: CommunicationRecord[];
+}
+
+// 沟通记录
+export interface CommunicationRecord {
+  id: string;
+  timestamp: Date;
+  type: 'script_used' | 'customer_response' | 'internal_note' | 'approval_comment';
+  content: string;
+  scriptId?: string; // 如果是使用话术,记录话术ID
+  userId: string;
+  userName: string;
+}
+
+// 话术使用统计
+export interface ScriptUsageStats {
+  scriptId: string;
+  scriptTitle: string;
+  usageCount: number;
+  successRate: number; // 成功率(客户接受报价的比例)
+  avgResponseTime: number; // 平均响应时间(小时)
+  lastUsed: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class QuotationApprovalService {
+  private approvals = signal<QuotationApproval[]>([]);
+  private scriptUsageStats = signal<ScriptUsageStats[]>([]);
+  
+  // 实时通知流
+  private approvalUpdates$ = new BehaviorSubject<QuotationApproval | null>(null);
+
+  constructor() {
+    this.initializeMockData();
+  }
+
+  // 初始化模拟数据
+  private initializeMockData(): void {
+    const mockApprovals: QuotationApproval[] = [
+      {
+        id: 'qa001',
+        quotationId: 'q001',
+        projectId: 'p001',
+        projectName: '现代简约客厅设计',
+        customerName: '张先生',
+        submittedBy: '李设计师',
+        submittedAt: new Date('2024-01-15T10:30:00'),
+        totalAmount: 85000,
+        status: 'pending',
+        scriptUsed: 'script001',
+        communicationLog: [
+          {
+            id: 'comm001',
+            timestamp: new Date('2024-01-15T10:30:00'),
+            type: 'script_used',
+            content: '使用了"高端设计报价说明"话术模板',
+            scriptId: 'script001',
+            userId: 'designer001',
+            userName: '李设计师'
+          }
+        ]
+      },
+      {
+        id: 'qa002',
+        quotationId: 'q002',
+        projectId: 'p002',
+        projectName: '北欧风卧室设计',
+        customerName: '王女士',
+        submittedBy: '陈设计师',
+        submittedAt: new Date('2024-01-14T14:20:00'),
+        totalAmount: 65000,
+        status: 'approved',
+        approvedBy: '财务主管',
+        approvedAt: new Date('2024-01-14T16:45:00'),
+        scriptUsed: 'script002',
+        communicationLog: [
+          {
+            id: 'comm002',
+            timestamp: new Date('2024-01-14T14:20:00'),
+            type: 'script_used',
+            content: '使用了"中档设计报价说明"话术模板',
+            scriptId: 'script002',
+            userId: 'designer002',
+            userName: '陈设计师'
+          },
+          {
+            id: 'comm003',
+            timestamp: new Date('2024-01-14T16:45:00'),
+            type: 'approval_comment',
+            content: '报价合理,符合市场标准,批准通过',
+            userId: 'finance001',
+            userName: '财务主管'
+          }
+        ]
+      }
+    ];
+
+    const mockStats: ScriptUsageStats[] = [
+      {
+        scriptId: 'script001',
+        scriptTitle: '高端设计报价说明',
+        usageCount: 15,
+        successRate: 0.73,
+        avgResponseTime: 2.5,
+        lastUsed: '2024-01-15T10:30:00'
+      },
+      {
+        scriptId: 'script002',
+        scriptTitle: '中档设计报价说明',
+        usageCount: 28,
+        successRate: 0.82,
+        avgResponseTime: 1.8,
+        lastUsed: '2024-01-14T14:20:00'
+      },
+      {
+        scriptId: 'script003',
+        scriptTitle: '基础设计报价说明',
+        usageCount: 35,
+        successRate: 0.89,
+        avgResponseTime: 1.2,
+        lastUsed: '2024-01-13T09:15:00'
+      }
+    ];
+
+    this.approvals.set(mockApprovals);
+    this.scriptUsageStats.set(mockStats);
+  }
+
+  // 获取报价审核列表
+  getQuotationApprovals(): Observable<QuotationApproval[]> {
+    return of(this.approvals()).pipe(delay(300));
+  }
+
+  // 根据状态筛选报价审核记录
+  getApprovalsByStatus(status: QuotationApprovalStatus): Observable<QuotationApproval[]> {
+    return of(this.approvals().filter(approval => approval.status === status)).pipe(delay(300));
+  }
+
+  // 获取单个报价审核记录
+  getApprovalById(id: string): Observable<QuotationApproval | undefined> {
+    return of(this.approvals().find(approval => approval.id === id)).pipe(delay(200));
+  }
+
+  // 提交报价审核
+  submitQuotationForApproval(quotationData: {
+    quotationId: string;
+    projectId: string;
+    projectName: string;
+    customerName: string;
+    submittedBy: string;
+    totalAmount: number;
+    scriptUsed?: string;
+  }): Observable<QuotationApproval> {
+    const newApproval: QuotationApproval = {
+      id: `qa${Date.now()}`,
+      ...quotationData,
+      submittedAt: new Date(),
+      status: 'pending',
+      communicationLog: quotationData.scriptUsed ? [{
+        id: `comm${Date.now()}`,
+        timestamp: new Date(),
+        type: 'script_used',
+        content: `使用了话术模板进行报价说明`,
+        scriptId: quotationData.scriptUsed,
+        userId: 'current_user',
+        userName: quotationData.submittedBy
+      }] : []
+    };
+
+    const updatedApprovals = [...this.approvals(), newApproval];
+    this.approvals.set(updatedApprovals);
+    
+    // 通知更新
+    this.approvalUpdates$.next(newApproval);
+    
+    return of(newApproval).pipe(delay(500));
+  }
+
+  // 审批报价
+  approveQuotation(approvalId: string, approvedBy: string, comment?: string): Observable<QuotationApproval> {
+    const approvals = this.approvals();
+    const approvalIndex = approvals.findIndex(a => a.id === approvalId);
+    
+    if (approvalIndex === -1) {
+      throw new Error('报价审核记录不存在');
+    }
+
+    const updatedApproval = {
+      ...approvals[approvalIndex],
+      status: 'approved' as QuotationApprovalStatus,
+      approvedBy,
+      approvedAt: new Date(),
+      communicationLog: [
+        ...approvals[approvalIndex].communicationLog,
+        {
+          id: `comm${Date.now()}`,
+          timestamp: new Date(),
+          type: 'approval_comment' as const,
+          content: comment || '报价已批准',
+          userId: 'current_user',
+          userName: approvedBy
+        }
+      ]
+    };
+
+    const updatedApprovals = [...approvals];
+    updatedApprovals[approvalIndex] = updatedApproval;
+    this.approvals.set(updatedApprovals);
+    
+    // 通知更新
+    this.approvalUpdates$.next(updatedApproval);
+    
+    return of(updatedApproval).pipe(delay(300));
+  }
+
+  // 拒绝报价
+  rejectQuotation(approvalId: string, rejectedBy: string, reason: string): Observable<QuotationApproval> {
+    const approvals = this.approvals();
+    const approvalIndex = approvals.findIndex(a => a.id === approvalId);
+    
+    if (approvalIndex === -1) {
+      throw new Error('报价审核记录不存在');
+    }
+
+    const updatedApproval = {
+      ...approvals[approvalIndex],
+      status: 'rejected' as QuotationApprovalStatus,
+      approvedBy: rejectedBy,
+      approvedAt: new Date(),
+      rejectionReason: reason,
+      communicationLog: [
+        ...approvals[approvalIndex].communicationLog,
+        {
+          id: `comm${Date.now()}`,
+          timestamp: new Date(),
+          type: 'approval_comment' as const,
+          content: `报价被拒绝:${reason}`,
+          userId: 'current_user',
+          userName: rejectedBy
+        }
+      ]
+    };
+
+    const updatedApprovals = [...approvals];
+    updatedApprovals[approvalIndex] = updatedApproval;
+    this.approvals.set(updatedApprovals);
+    
+    // 通知更新
+    this.approvalUpdates$.next(updatedApproval);
+    
+    return of(updatedApproval).pipe(delay(300));
+  }
+
+  // 要求修订报价
+  requestRevision(approvalId: string, requestedBy: string, notes: string): Observable<QuotationApproval> {
+    const approvals = this.approvals();
+    const approvalIndex = approvals.findIndex(a => a.id === approvalId);
+    
+    if (approvalIndex === -1) {
+      throw new Error('报价审核记录不存在');
+    }
+
+    const updatedApproval = {
+      ...approvals[approvalIndex],
+      status: 'revision_required' as QuotationApprovalStatus,
+      revisionNotes: notes,
+      communicationLog: [
+        ...approvals[approvalIndex].communicationLog,
+        {
+          id: `comm${Date.now()}`,
+          timestamp: new Date(),
+          type: 'approval_comment' as const,
+          content: `要求修订:${notes}`,
+          userId: 'current_user',
+          userName: requestedBy
+        }
+      ]
+    };
+
+    const updatedApprovals = [...approvals];
+    updatedApprovals[approvalIndex] = updatedApproval;
+    this.approvals.set(updatedApprovals);
+    
+    // 通知更新
+    this.approvalUpdates$.next(updatedApproval);
+    
+    return of(updatedApproval).pipe(delay(300));
+  }
+
+  // 记录话术使用
+  recordScriptUsage(approvalId: string, scriptId: string, userId: string, userName: string): Observable<void> {
+    const approvals = this.approvals();
+    const approvalIndex = approvals.findIndex(a => a.id === approvalId);
+    
+    if (approvalIndex !== -1) {
+      const communicationRecord: CommunicationRecord = {
+        id: `comm${Date.now()}`,
+        timestamp: new Date(),
+        type: 'script_used',
+        content: '使用了话术模板进行沟通',
+        scriptId,
+        userId,
+        userName
+      };
+
+      const updatedApproval = {
+        ...approvals[approvalIndex],
+        communicationLog: [...approvals[approvalIndex].communicationLog, communicationRecord]
+      };
+
+      const updatedApprovals = [...approvals];
+      updatedApprovals[approvalIndex] = updatedApproval;
+      this.approvals.set(updatedApprovals);
+      
+      // 更新话术使用统计
+      this.updateScriptUsageStats(scriptId);
+    }
+    
+    return of(void 0).pipe(delay(200));
+  }
+
+  // 记录客户响应
+  recordCustomerResponse(approvalId: string, response: string, userId: string, userName: string): Observable<void> {
+    const approvals = this.approvals();
+    const approvalIndex = approvals.findIndex(a => a.id === approvalId);
+    
+    if (approvalIndex !== -1) {
+      const communicationRecord: CommunicationRecord = {
+        id: `comm${Date.now()}`,
+        timestamp: new Date(),
+        type: 'customer_response',
+        content: response,
+        userId,
+        userName
+      };
+
+      const updatedApproval = {
+        ...approvals[approvalIndex],
+        communicationLog: [...approvals[approvalIndex].communicationLog, communicationRecord]
+      };
+
+      const updatedApprovals = [...approvals];
+      updatedApprovals[approvalIndex] = updatedApproval;
+      this.approvals.set(updatedApprovals);
+    }
+    
+    return of(void 0).pipe(delay(200));
+  }
+
+  // 获取话术使用统计
+  getScriptUsageStats(): Observable<ScriptUsageStats[]> {
+    return of(this.scriptUsageStats()).pipe(delay(300));
+  }
+
+  // 获取实时更新流
+  getApprovalUpdates(): Observable<QuotationApproval | null> {
+    return this.approvalUpdates$.asObservable();
+  }
+
+  // 更新话术使用统计
+  private updateScriptUsageStats(scriptId: string): void {
+    const stats = this.scriptUsageStats();
+    const statIndex = stats.findIndex(s => s.scriptId === scriptId);
+    
+    if (statIndex !== -1) {
+      const updatedStats = [...stats];
+      updatedStats[statIndex] = {
+        ...updatedStats[statIndex],
+        usageCount: updatedStats[statIndex].usageCount + 1,
+        lastUsed: new Date().toISOString()
+      };
+      this.scriptUsageStats.set(updatedStats);
+    }
+  }
+
+  // 获取审核统计数据
+  getApprovalStats(): Observable<{
+    total: number;
+    pending: number;
+    approved: number;
+    rejected: number;
+    revisionRequired: number;
+    avgApprovalTime: number; // 平均审批时间(小时)
+  }> {
+    const approvals = this.approvals();
+    const total = approvals.length;
+    const pending = approvals.filter(a => a.status === 'pending').length;
+    const approved = approvals.filter(a => a.status === 'approved').length;
+    const rejected = approvals.filter(a => a.status === 'rejected').length;
+    const revisionRequired = approvals.filter(a => a.status === 'revision_required').length;
+    
+    // 计算平均审批时间
+    const completedApprovals = approvals.filter(a => a.approvedAt);
+    const avgApprovalTime = completedApprovals.length > 0 
+      ? completedApprovals.reduce((sum, approval) => {
+          const timeDiff = approval.approvedAt!.getTime() - approval.submittedAt.getTime();
+          return sum + (timeDiff / (1000 * 60 * 60)); // 转换为小时
+        }, 0) / completedApprovals.length
+      : 0;
+
+    return of({
+      total,
+      pending,
+      approved,
+      rejected,
+      revisionRequired,
+      avgApprovalTime
+    }).pipe(delay(200));
+  }
+}

+ 340 - 0
src/app/services/work-hour.service.ts

@@ -0,0 +1,340 @@
+import { Injectable } from '@angular/core';
+import { Observable, of, BehaviorSubject } from 'rxjs';
+import { 
+  WorkHourRecord, 
+  MonthlyWorkHourStats, 
+  WorkHourDashboardData, 
+  DifficultyCoefficient,
+  PerformanceCriteria,
+  PricingRule,
+  PricingScript,
+  AutoQuotationResult,
+  PriceAdjustment,
+  PerformanceLevel,
+  SpaceType,
+  ProjectStyle,
+  DEFAULT_DIFFICULTY_COEFFICIENTS,
+  DEFAULT_PERFORMANCE_CRITERIA,
+  DEFAULT_PRICING_RULES
+} from '../models/work-hour.model';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class WorkHourService {
+  private workHourRecords: WorkHourRecord[] = [];
+  private monthlyStats: MonthlyWorkHourStats[] = [];
+  private difficultyCoefficients = DEFAULT_DIFFICULTY_COEFFICIENTS;
+  private performanceCriteria = DEFAULT_PERFORMANCE_CRITERIA;
+  private pricingRules = DEFAULT_PRICING_RULES;
+  private pricingScripts: PricingScript[] = [];
+  
+  // 数据变更通知
+  private workHourDataSubject = new BehaviorSubject<WorkHourRecord[]>([]);
+  public workHourData$ = this.workHourDataSubject.asObservable();
+
+  constructor() {
+    this.initializeMockData();
+  }
+
+  // 初始化模拟数据
+  private initializeMockData(): void {
+    // 模拟工时记录数据
+    const mockRecords: WorkHourRecord[] = [
+      {
+        id: 'wh001',
+        projectId: 'p001',
+        projectName: '现代简约客厅设计',
+        designerId: 'd001',
+        designerName: '张设计师',
+        phase: '建模',
+        startTime: new Date('2024-01-15T09:00:00'),
+        endTime: new Date('2024-01-17T18:00:00'),
+        actualHours: 16,
+        status: '完成',
+        spaceType: '客餐厅',
+        projectStyle: '现代简约',
+        difficultyCoefficient: 1.0,
+        effectiveHours: 16,
+        notes: '按时完成建模工作'
+      },
+      {
+        id: 'wh002',
+        projectId: 'p002',
+        projectName: '新中式别墅设计',
+        designerId: 'd002',
+        designerName: '李设计师',
+        phase: '建模',
+        startTime: new Date('2024-01-10T09:00:00'),
+        endTime: new Date('2024-01-13T18:00:00'),
+        actualHours: 24,
+        status: '完成',
+        pauseReason: '客户2天未反馈',
+        pauseHours: 16,
+        spaceType: '别墅',
+        projectStyle: '新中式',
+        difficultyCoefficient: 1.8, // 1.5 * 1.2
+        effectiveHours: 43.2, // 24 * 1.8
+        notes: '复杂项目,需要多次沟通'
+      },
+      {
+        id: 'wh003',
+        projectId: 'p003',
+        projectName: '轻奢卧室设计',
+        designerId: 'd001',
+        designerName: '张设计师',
+        phase: '渲染',
+        startTime: new Date('2024-01-18T09:00:00'),
+        endTime: new Date('2024-01-19T15:00:00'),
+        actualHours: 12,
+        status: '完成',
+        spaceType: '卧室',
+        projectStyle: '轻奢',
+        difficultyCoefficient: 1.04, // 0.8 * 1.3
+        effectiveHours: 12.48,
+        notes: '渲染效果优秀'
+      }
+    ];
+
+    this.workHourRecords = mockRecords;
+    this.workHourDataSubject.next(mockRecords);
+
+    // 初始化报价话术库
+    this.pricingScripts = [
+      {
+        id: 'ps001',
+        category: '高端报价解释',
+        title: '高端报价价值说明',
+        content: '我们的高端报价包含更精细的建模工艺、4K高清渲染交付、以及专业的后期处理,确保您获得更好的视觉表现效果。相比标准版本,高端版本在细节处理和最终效果上有显著提升。',
+        applicableScenarios: ['客户质疑高端报价', '解释价格差异'],
+        tags: ['高端', '价值', '效果']
+      },
+      {
+        id: 'ps002',
+        category: '价格构成说明',
+        title: '报价构成详细说明',
+        content: '我们的报价主要包含三个部分:1)建模费用(占40%)- 根据空间复杂度计算;2)渲染费用(占35%)- 包含灯光、材质、后期处理;3)设计服务费(占25%)- 包含方案沟通、修改服务等。',
+        applicableScenarios: ['客户询问价格构成', '报价透明化说明'],
+        tags: ['构成', '透明', '详细']
+      },
+      {
+        id: 'ps003',
+        category: '修改服务说明',
+        title: '修改次数和范围说明',
+        content: '标准报价包含1次小图修改(调整色彩、材质等细节),如需大幅度修改方案或增加修改次数,会根据工作量适当调整费用。我们建议在建模阶段充分沟通需求,以减少后期修改。',
+        applicableScenarios: ['客户询问修改政策', '设定修改预期'],
+        tags: ['修改', '服务', '范围']
+      }
+    ];
+  }
+
+  // 获取工时记录
+  getWorkHourRecords(designerId?: string, projectId?: string): Observable<WorkHourRecord[]> {
+    let records = this.workHourRecords;
+    
+    if (designerId) {
+      records = records.filter(r => r.designerId === designerId);
+    }
+    
+    if (projectId) {
+      records = records.filter(r => r.projectId === projectId);
+    }
+    
+    return of(records);
+  }
+
+  // 添加工时记录
+  addWorkHourRecord(record: Omit<WorkHourRecord, 'id' | 'effectiveHours'>): Observable<WorkHourRecord> {
+    const newRecord: WorkHourRecord = {
+      ...record,
+      id: `wh${Date.now()}`,
+      effectiveHours: this.calculateEffectiveHours(record.actualHours, record.spaceType, record.projectStyle)
+    };
+    
+    this.workHourRecords.push(newRecord);
+    this.workHourDataSubject.next(this.workHourRecords);
+    
+    return of(newRecord);
+  }
+
+  // 计算有效工时
+  calculateEffectiveHours(actualHours: number, spaceType: SpaceType, projectStyle: ProjectStyle): number {
+    const spaceCoeff = this.difficultyCoefficients.find(c => c.spaceType === spaceType)?.spaceCoefficient || 1.0;
+    const styleCoeff = this.difficultyCoefficients.find(c => c.styleType === projectStyle)?.styleCoefficient || 1.0;
+    const totalCoeff = spaceCoeff * styleCoeff;
+    
+    return actualHours * totalCoeff;
+  }
+
+  // 获取月度统计数据
+  getMonthlyStats(designerId?: string, month?: string): Observable<MonthlyWorkHourStats[]> {
+    // 基于工时记录计算月度统计
+    const stats = this.calculateMonthlyStats(designerId, month);
+    return of(stats);
+  }
+
+  // 计算月度统计
+  private calculateMonthlyStats(designerId?: string, month?: string): MonthlyWorkHourStats[] {
+    const records = this.workHourRecords.filter(r => {
+      if (designerId && r.designerId !== designerId) return false;
+      if (month && !r.startTime.toISOString().startsWith(month)) return false;
+      return true;
+    });
+
+    const groupedByDesigner = records.reduce((acc, record) => {
+      const key = record.designerId;
+      if (!acc[key]) {
+        acc[key] = [];
+      }
+      acc[key].push(record);
+      return acc;
+    }, {} as Record<string, WorkHourRecord[]>);
+
+    return Object.entries(groupedByDesigner).map(([designerId, records]) => {
+      const totalActualHours = records.reduce((sum, r) => sum + r.actualHours, 0);
+      const totalEffectiveHours = records.reduce((sum, r) => sum + r.effectiveHours, 0);
+      const totalPauseHours = records.reduce((sum, r) => sum + (r.pauseHours || 0), 0);
+      const completedProjects = new Set(records.map(r => r.projectId)).size;
+      const averageCustomerSatisfaction = 4.5; // 模拟数据
+      
+      const performanceLevel = this.calculatePerformanceLevel(
+        totalEffectiveHours,
+        completedProjects,
+        averageCustomerSatisfaction
+      );
+
+      return {
+        designerId,
+        designerName: records[0].designerName,
+        month: month || new Date().toISOString().substring(0, 7),
+        totalActualHours,
+        totalEffectiveHours,
+        totalPauseHours,
+        completedProjects,
+        averageCustomerSatisfaction,
+        performanceLevel,
+        onTimeCompletionRate: 85, // 模拟数据
+        excellentWorkRate: 78, // 模拟数据
+        records
+      };
+    });
+  }
+
+  // 计算绩效等级
+  private calculatePerformanceLevel(effectiveHours: number, completedProjects: number, satisfaction: number): PerformanceLevel {
+    if (satisfaction >= 5.0 && effectiveHours > 100) return 'S';
+    if (satisfaction >= 4.0 && effectiveHours > 80) return 'A';
+    if (satisfaction >= 4.0) return 'B';
+    return 'C';
+  }
+
+  // 获取仪表板数据
+  getDashboardData(): Observable<WorkHourDashboardData> {
+    const monthlyStats = this.calculateMonthlyStats();
+    
+    const dashboardData: WorkHourDashboardData = {
+      totalDesigners: new Set(this.workHourRecords.map(r => r.designerId)).size,
+      totalProjects: new Set(this.workHourRecords.map(r => r.projectId)).size,
+      averageEffectiveHours: monthlyStats.reduce((sum, s) => sum + s.totalEffectiveHours, 0) / monthlyStats.length || 0,
+      performanceDistribution: {
+        S: monthlyStats.filter(s => s.performanceLevel === 'S').length,
+        A: monthlyStats.filter(s => s.performanceLevel === 'A').length,
+        B: monthlyStats.filter(s => s.performanceLevel === 'B').length,
+        C: monthlyStats.filter(s => s.performanceLevel === 'C').length
+      },
+      monthlyTrends: [
+        { month: '2024-01', totalHours: 320, effectiveHours: 384, efficiency: 120 },
+        { month: '2024-02', totalHours: 280, effectiveHours: 336, efficiency: 120 },
+        { month: '2024-03', totalHours: 360, effectiveHours: 432, efficiency: 120 }
+      ],
+      topPerformers: monthlyStats
+        .sort((a, b) => b.totalEffectiveHours - a.totalEffectiveHours)
+        .slice(0, 5)
+        .map(s => ({
+          designerId: s.designerId,
+          designerName: s.designerName,
+          effectiveHours: s.totalEffectiveHours,
+          performanceLevel: s.performanceLevel,
+          efficiency: Math.round((s.totalEffectiveHours / s.totalActualHours) * 100)
+        }))
+    };
+
+    return of(dashboardData);
+  }
+
+  // 获取报价规则
+  getPricingRules(): Observable<PricingRule[]> {
+    return of(this.pricingRules);
+  }
+
+  // 自动生成报价
+  generateAutoQuotation(spaceType: SpaceType, projectStyle: ProjectStyle, estimatedWorkDays: number = 3): Observable<AutoQuotationResult> {
+    const rule = this.pricingRules.find(r => r.spaceType === spaceType && r.styleType === projectStyle);
+    
+    if (!rule) {
+      throw new Error(`未找到匹配的报价规则: ${spaceType} - ${projectStyle}`);
+    }
+
+    const quotation: AutoQuotationResult = {
+      id: `aq${Date.now()}`,
+      spaceType,
+      projectStyle,
+      estimatedWorkDays,
+      basePrice: rule.basePrice,
+      styleMultiplier: rule.styleMultiplier,
+      finalPrice: rule.finalPrice * estimatedWorkDays,
+      serviceIncludes: [...rule.serviceIncludes],
+      createdAt: new Date(),
+      createdBy: 'current-user', // 应该从认证服务获取
+      status: 'draft'
+    };
+
+    return of(quotation);
+  }
+
+  // 获取报价话术库
+  getPricingScripts(category?: string): Observable<PricingScript[]> {
+    let scripts = this.pricingScripts;
+    
+    if (category) {
+      scripts = scripts.filter(s => s.category === category);
+    }
+    
+    return of(scripts);
+  }
+
+  // 添加价格调整记录
+  addPriceAdjustment(quotationId: string, adjustment: Omit<PriceAdjustment, 'id'>): Observable<PriceAdjustment> {
+    const newAdjustment: PriceAdjustment = {
+      ...adjustment,
+      id: `pa${Date.now()}`
+    };
+    
+    // 在实际应用中,这里应该更新对应的报价记录
+    console.log('价格调整记录已添加:', newAdjustment);
+    
+    return of(newAdjustment);
+  }
+
+  // 获取难度系数配置
+  getDifficultyCoefficients(): Observable<DifficultyCoefficient[]> {
+    return of(this.difficultyCoefficients);
+  }
+
+  // 更新难度系数配置
+  updateDifficultyCoefficients(coefficients: DifficultyCoefficient[]): Observable<boolean> {
+    this.difficultyCoefficients = coefficients;
+    return of(true);
+  }
+
+  // 获取绩效标准配置
+  getPerformanceCriteria(): Observable<PerformanceCriteria[]> {
+    return of(this.performanceCriteria);
+  }
+
+  // 更新绩效标准配置
+  updatePerformanceCriteria(criteria: PerformanceCriteria[]): Observable<boolean> {
+    this.performanceCriteria = criteria;
+    return of(true);
+  }
+}

+ 256 - 0
src/app/shared/components/auto-settlement-config/auto-settlement-config.html

@@ -0,0 +1,256 @@
+<div class="auto-settlement-config">
+  <!-- 头部标题和操作按钮 -->
+  <div class="config-header">
+    <h2>自动化结算规则配置</h2>
+    <button 
+      mat-raised-button 
+      color="primary" 
+      (click)="startAddNew()"
+      [disabled]="isAddingNew()">
+      <mat-icon>add</mat-icon>
+      添加新规则
+    </button>
+  </div>
+
+  <!-- 新规则表单 -->
+  @if (isAddingNew()) {
+    <mat-card class="new-rule-card">
+      <mat-card-header>
+        <mat-card-title>新建自动化规则</mat-card-title>
+        <mat-card-subtitle>配置自动处理结算的条件和动作</mat-card-subtitle>
+      </mat-card-header>
+      
+      <mat-card-content>
+        <div class="form-section">
+          <mat-form-field appearance="outline" class="full-width">
+            <mat-label>规则名称</mat-label>
+            <input matInput [(ngModel)]="newRule.name" placeholder="例如:小额自动确认">
+          </mat-form-field>
+
+          <mat-form-field appearance="outline" class="full-width">
+            <mat-label>优先级</mat-label>
+            <input matInput type="number" [(ngModel)]="newRule.priority" min="1" max="10">
+          </mat-form-field>
+
+          <mat-form-field appearance="outline" class="full-width">
+            <mat-label>规则描述</mat-label>
+            <textarea matInput [(ngModel)]="newRule.description" rows="2" placeholder="描述规则的用途和效果"></textarea>
+          </mat-form-field>
+
+          <mat-slide-toggle [(ngModel)]="newRule.enabled" color="primary">
+            启用规则
+          </mat-slide-toggle>
+        </div>
+
+        <!-- 条件配置 -->
+        <div class="conditions-section">
+          <h3>条件配置</h3>
+          @for (condition of newRule.conditions; track $index; let i = $index) {
+            <div class="condition-item">
+              <div class="condition-header">
+                <span>条件 {{ i + 1 }}</span>
+                <button mat-icon-button color="warn" (click)="removeCondition(newRule, i)">
+                  <mat-icon>delete</mat-icon>
+                </button>
+              </div>
+              
+              <div class="condition-fields">
+                <mat-form-field appearance="outline">
+                  <mat-label>条件类型</mat-label>
+                  <mat-select [(ngModel)]="condition.type">
+                    @for (type of conditionTypes; track type.value) {
+                      <mat-option [value]="type.value">{{ type.label }}</mat-option>
+                    }
+                  </mat-select>
+                </mat-form-field>
+
+                <mat-form-field appearance="outline">
+                  <mat-label>操作符</mat-label>
+                  <mat-select [(ngModel)]="condition.operator" [disabled]="!condition.type">
+                    @for (op of getOperatorOptions(condition.type); track op.value) {
+                      <mat-option [value]="op.value">{{ op.label }}</mat-option>
+                    }
+                  </mat-select>
+                </mat-form-field>
+
+                <mat-form-field appearance="outline">
+                  <mat-label>值</mat-label>
+                  <input matInput [(ngModel)]="condition.value" placeholder="输入条件值">
+                </mat-form-field>
+              </div>
+            </div>
+          }
+          
+          <button mat-button color="primary" (click)="addCondition(newRule)">
+            <mat-icon>add</mat-icon>
+            添加条件
+          </button>
+        </div>
+
+        <!-- 动作配置 -->
+        <div class="actions-section">
+          <h3>动作配置</h3>
+          @for (action of newRule.actions; track $index; let i = $index) {
+            <div class="action-item">
+              <div class="action-header">
+                <span>动作 {{ i + 1 }}</span>
+                <button mat-icon-button color="warn" (click)="removeAction(newRule, i)">
+                  <mat-icon>delete</mat-icon>
+                </button>
+              </div>
+              
+              <div class="action-fields">
+                <mat-form-field appearance="outline">
+                  <mat-label>动作类型</mat-label>
+                  <mat-select [(ngModel)]="action.type">
+                    @for (type of actionTypes; track type.value) {
+                      <mat-option [value]="type.value">{{ type.label }}</mat-option>
+                    }
+                  </mat-select>
+                </mat-form-field>
+
+                <!-- 动作参数配置 -->
+                @if (action.type === 'sendReminder') {
+                  <div class="action-params">
+                    <mat-form-field appearance="outline">
+                      <mat-label>发送渠道</mat-label>
+                      <mat-select [(ngModel)]="action.params.channels" multiple>
+                        <mat-option value="wechat">微信</mat-option>
+                        <mat-option value="sms">短信</mat-option>
+                        <mat-option value="email">邮件</mat-option>
+                        <mat-option value="system">系统通知</mat-option>
+                      </mat-select>
+                    </mat-form-field>
+
+                    <mat-form-field appearance="outline">
+                      <mat-label>发送频率</mat-label>
+                      <mat-select [(ngModel)]="action.params.frequency">
+                        <mat-option value="immediate">立即发送</mat-option>
+                        <mat-option value="daily">每天</mat-option>
+                        <mat-option value="weekly">每周</mat-option>
+                      </mat-select>
+                    </mat-form-field>
+                  </div>
+                }
+
+                @if (action.type === 'applyDiscount') {
+                  <div class="action-params">
+                    <mat-form-field appearance="outline">
+                      <mat-label>折扣类型</mat-label>
+                      <mat-select [(ngModel)]="action.params.type">
+                        <mat-option value="percentage">百分比</mat-option>
+                        <mat-option value="fixed">固定金额</mat-option>
+                      </mat-select>
+                    </mat-form-field>
+
+                    <mat-form-field appearance="outline">
+                      <mat-label>折扣值</mat-label>
+                      <input matInput type="number" [(ngModel)]="action.params.value" placeholder="例如:5">
+                    </mat-form-field>
+
+                    <mat-form-field appearance="outline">
+                      <mat-label>最大金额</mat-label>
+                      <input matInput type="number" [(ngModel)]="action.params.maxAmount" placeholder="例如:1000">
+                    </mat-form-field>
+                  </div>
+                }
+              </div>
+            </div>
+          }
+          
+          <button mat-button color="primary" (click)="addAction(newRule)">
+            <mat-icon>add</mat-icon>
+            添加动作
+          </button>
+        </div>
+      </mat-card-content>
+
+      <mat-card-actions align="end">
+        <button mat-button (click)="cancelAddNew()">取消</button>
+        <button mat-raised-button color="primary" (click)="saveRule(newRule)">保存规则</button>
+      </mat-card-actions>
+    </mat-card>
+  }
+
+  <!-- 现有规则列表 -->
+  <div class="rules-list">
+    @for (rule of rules(); track rule.id) {
+      <mat-expansion-panel 
+        [expanded]="expandedPanels().has(rule.id)"
+        (opened)="togglePanel(rule.id)"
+        (closed)="togglePanel(rule.id)">
+        
+        <mat-expansion-panel-header>
+          <mat-panel-title>
+            <div class="rule-header">
+              <mat-slide-toggle 
+                [checked]="rule.enabled" 
+                (change)="toggleRule(rule)" 
+                (click)="$event.stopPropagation()"
+                color="primary">
+              </mat-slide-toggle>
+              <span class="rule-name" [class.disabled]="!rule.enabled">{{ rule.name }}</span>
+              <span class="rule-priority">优先级: {{ rule.priority }}</span>
+            </div>
+          </mat-panel-title>
+          
+          <mat-panel-description>
+            {{ rule.description }}
+          </mat-panel-description>
+        </mat-expansion-panel-header>
+
+        <div class="rule-details">
+          <!-- 条件显示 -->
+          <div class="conditions-display">
+            <h4>条件:</h4>
+            @if (rule.conditions.length > 0) {
+              <div class="conditions-list">
+                @for (condition of rule.conditions; track $index) {
+                  <span class="condition-tag">{{ formatCondition(condition) }}</span>
+                }
+              </div>
+            } @else {
+              <p class="no-conditions">暂无条件配置</p>
+            }
+          </div>
+
+          <!-- 动作显示 -->
+          <div class="actions-display">
+            <h4>动作:</h4>
+            @if (rule.actions.length > 0) {
+              <div class="actions-list">
+                @for (action of rule.actions; track $index) {
+                  <span class="action-tag">{{ formatAction(action) }}</span>
+                }
+              </div>
+            } @else {
+              <p class="no-actions">暂无动作配置</p>
+            }
+          </div>
+
+          <!-- 操作按钮 -->
+          <div class="rule-actions">
+            <button mat-button color="warn" (click)="deleteRule(rule.id)">
+              <mat-icon>delete</mat-icon>
+              删除规则
+            </button>
+            
+            <button mat-button color="primary" (click)="togglePanel(rule.id)">
+              <mat-icon>edit</mat-icon>
+              编辑规则
+            </button>
+          </div>
+        </div>
+      </mat-expansion-panel>
+    }
+  </div>
+
+  <!-- 空状态 -->
+  @if (rules().length === 0 && !isAddingNew()) {
+    <div class="empty-state">
+      <mat-icon class="empty-icon">settings_suggest</mat-icon>
+      <h3>暂无自动化规则</h3>
+      <p>点击"添加新规则"按钮开始配置自动化结算规则</p>
+    </div>
+  }
+</div>

+ 392 - 0
src/app/shared/components/auto-settlement-config/auto-settlement-config.scss

@@ -0,0 +1,392 @@
+@use '../../styles/_ios-theme.scss' as ios;
+
+.auto-settlement-config {
+  padding: 20px;
+  max-width: 1200px;
+  margin: 0 auto;
+
+  // 头部样式
+  .config-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 24px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid ios.$ios-border;
+
+    h2 {
+      margin: 0;
+      color: ios.$ios-text-primary;
+    font-weight: ios.$ios-font-weight-semibold;
+      font-size: 24px;
+    }
+  }
+
+  // 新规则卡片
+  .new-rule-card {
+    margin-bottom: 24px;
+    border: 2px solid ios.$ios-primary;
+    background: linear-gradient(135deg, #f8f9ff 0%, #e8f4ff 100%);
+
+    .mat-card-header {
+      background: linear-gradient(135deg, ios.$ios-primary 0%, lighten(ios.$ios-primary, 10%) 100%);
+      color: white;
+      border-radius: 8px 8px 0 0;
+      padding: 16px;
+
+      .mat-card-title {
+        font-size: 18px;
+        font-weight: ios.$ios-font-weight-semibold;
+      }
+
+      .mat-card-subtitle {
+        color: rgba(255, 255, 255, 0.8);
+      }
+    }
+
+    .mat-card-content {
+      padding: 24px;
+    }
+
+    .form-section {
+      display: grid;
+      grid-template-columns: 1fr 120px;
+      gap: 16px;
+      margin-bottom: 24px;
+
+      .full-width {
+        grid-column: 1 / -1;
+      }
+    }
+
+    .conditions-section,
+    .actions-section {
+      margin-bottom: 24px;
+      padding: 16px;
+      background: #f8f9fa;
+      border-radius: 8px;
+      border: 1px solid ios.$ios-border;
+
+      h3 {
+        margin: 0 0 16px 0;
+        color: ios.$ios-text-primary;
+        font-size: 16px;
+        font-weight: ios.$ios-font-weight-semibold;
+      }
+    }
+
+    .condition-item,
+    .action-item {
+      margin-bottom: 16px;
+      padding: 16px;
+      background: white;
+      border-radius: 8px;
+      border: 1px solid ios.$ios-border;
+
+      .condition-header,
+      .action-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12px;
+        padding-bottom: 8px;
+        border-bottom: 1px solid ios.$ios-border;
+
+        span {
+          font-weight: ios.$ios-font-weight-semibold;
+          color: ios.$ios-text-primary;
+        }
+      }
+
+      .condition-fields,
+      .action-fields {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+        gap: 12px;
+
+        .mat-form-field {
+          margin-bottom: 0;
+        }
+      }
+
+      .action-params {
+        grid-column: 1 / -1;
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+        gap: 12px;
+        margin-top: 12px;
+        padding-top: 12px;
+        border-top: 1px solid ios.$ios-border;
+      }
+    }
+
+    .mat-card-actions {
+      padding: 16px 24px;
+      border-top: 1px solid ios.$ios-border;
+    }
+  }
+
+  // 规则列表
+  .rules-list {
+    .mat-expansion-panel {
+      margin-bottom: 12px;
+      border: 1px solid ios.$ios-border;
+      border-radius: 8px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+
+      &:hover {
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+      }
+
+      .mat-expansion-panel-header {
+        padding: 16px 24px;
+        height: auto;
+        min-height: 64px;
+
+        .rule-header {
+          display: flex;
+          align-items: center;
+          gap: 12px;
+          flex: 1;
+
+          .rule-name {
+            font-weight: ios.$ios-font-weight-semibold;
+            font-size: 16px;
+            color: ios.$ios-text-primary;
+
+            &.disabled {
+              color: ios.$ios-text-secondary;
+              text-decoration: line-through;
+            }
+          }
+
+          .rule-priority {
+            font-size: 12px;
+            color: ios.$ios-text-secondary;
+            background: #f0f0f0;
+            padding: 2px 8px;
+            border-radius: 12px;
+          }
+        }
+
+        .mat-panel-description {
+          color: ios.$ios-text-secondary;
+          font-size: 14px;
+          margin-right: 16px;
+        }
+      }
+
+      .rule-details {
+        padding: 16px 24px;
+
+        .conditions-display,
+        .actions-display {
+          margin-bottom: 20px;
+
+          h4 {
+            margin: 0 0 12px 0;
+            color: ios.$ios-text-primary;
+            font-size: 14px;
+            font-weight: ios.$ios-font-weight-semibold;
+          }
+
+          .conditions-list,
+          .actions-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 8px;
+          }
+
+          .condition-tag,
+          .action-tag {
+            background: lighten(ios.$ios-primary, 20%);
+            color: ios.$ios-primary;
+            padding: 6px 12px;
+            border-radius: 16px;
+            font-size: 12px;
+            font-weight: ios.$ios-font-weight-medium;
+            border: 1px solid rgba(ios.$ios-primary, 0.2);
+          }
+
+          .no-conditions,
+          .no-actions {
+            color: ios.$ios-text-secondary;
+            font-style: italic;
+            margin: 0;
+          }
+        }
+
+        .rule-actions {
+          display: flex;
+          gap: 12px;
+          justify-content: flex-end;
+          padding-top: 16px;
+          border-top: 1px solid ios.$ios-border;
+
+          button {
+            min-width: auto;
+          }
+        }
+      }
+    }
+  }
+
+  // 空状态
+  .empty-state {
+    text-align: center;
+    padding: 60px 20px;
+    color: ios.$ios-text-secondary;
+
+    .empty-icon {
+      font-size: 64px;
+      width: 64px;
+      height: 64px;
+      margin-bottom: 16px;
+      opacity: 0.5;
+    }
+
+    h3 {
+      margin: 0 0 8px 0;
+      color: ios.$ios-text-primary;
+      font-size: 18px;
+      font-weight: ios.$ios-font-weight-semibold;
+    }
+
+    p {
+      margin: 0;
+      font-size: 14px;
+      line-height: 1.4;
+    }
+  }
+
+  // 响应式设计
+  @media (max-width: 768px) {
+    padding: 16px;
+
+    .config-header {
+      flex-direction: column;
+      gap: 16px;
+      text-align: center;
+
+      h2 {
+        font-size: 20px;
+      }
+    }
+
+    .new-rule-card {
+      .form-section {
+        grid-template-columns: 1fr;
+      }
+
+      .condition-fields,
+      .action-fields {
+        grid-template-columns: 1fr;
+      }
+    }
+
+    .rules-list {
+      .mat-expansion-panel-header {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 8px;
+
+        .rule-header {
+          flex-direction: column;
+          align-items: flex-start;
+          gap: 8px;
+        }
+
+        .mat-panel-description {
+          margin-right: 0;
+          text-align: left;
+        }
+      }
+
+      .rule-details {
+        .rule-actions {
+          flex-direction: column;
+          align-items: stretch;
+
+          button {
+            width: 100%;
+          }
+        }
+      }
+    }
+  }
+
+  // 动画效果
+  .condition-item,
+  .action-item,
+  .mat-expansion-panel {
+    transition: all 0.3s ease;
+
+    &:hover {
+      transform: translateY(-1px);
+    }
+  }
+
+  // 禁用状态样式
+  .disabled {
+    opacity: 0.6;
+  }
+
+  // 表单字段样式
+  .mat-form-field {
+    margin-bottom: 16px;
+
+    &.mat-form-field-appearance-outline {
+      .mat-form-field-outline {
+        color: ios.$ios-border;
+      }
+
+      .mat-form-field-outline-thick {
+        color: ios.$ios-primary;
+      }
+
+      &.mat-focused {
+        .mat-form-field-outline-thick {
+          color: ios.$ios-primary;
+        }
+      }
+    }
+  }
+
+  // 按钮样式
+  .mat-button,
+  .mat-raised-button {
+    border-radius: 8px;
+    font-weight: ios.$ios-font-weight-medium;
+
+    &.mat-primary {
+      background: ios.$ios-primary;
+      color: white;
+
+      &:hover {
+        background: darken(ios.$ios-primary, 10%);
+      }
+    }
+
+    &.mat-warn {
+      background: #dc3545;
+      color: white;
+
+      &:hover {
+        background: darken(#dc3545, 10%);
+      }
+    }
+  }
+
+  // 滑动开关样式
+  .mat-slide-toggle {
+    &.mat-checked {
+      .mat-slide-toggle-thumb {
+        background-color: ios.$ios-primary;
+      }
+
+      .mat-slide-toggle-bar {
+        background-color: rgba(ios.$ios-primary, 0.5);
+      }
+    }
+  }
+}

+ 260 - 0
src/app/shared/components/auto-settlement-config/auto-settlement-config.ts

@@ -0,0 +1,260 @@
+import { Component, OnInit, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { MatCardModule } from '@angular/material/card';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatExpansionModule } from '@angular/material/expansion';
+import { MatSelectModule } from '@angular/material/select';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatSlideToggleModule } from '@angular/material/slide-toggle';
+import { MatDialogModule, MatDialog } from '@angular/material/dialog';
+import { AutoSettlementService, AutoSettlementRule, SettlementCondition, SettlementAction } from '../../../services/auto-settlement.service';
+
+@Component({
+  selector: 'app-auto-settlement-config',
+  standalone: true,
+  imports: [
+    CommonModule,
+    FormsModule,
+    MatCardModule,
+    MatButtonModule,
+    MatIconModule,
+    MatExpansionModule,
+    MatSelectModule,
+    MatInputModule,
+    MatCheckboxModule,
+    MatSlideToggleModule,
+    MatDialogModule
+  ],
+  templateUrl: './auto-settlement-config.html',
+  styleUrls: ['./auto-settlement-config.scss']
+})
+export class AutoSettlementConfigComponent implements OnInit {
+  rules = signal<AutoSettlementRule[]>([]);
+  expandedPanels = signal<Set<string>>(new Set());
+  isAddingNew = signal(false);
+  
+  // 条件类型选项
+  conditionTypes = [
+    { value: 'amountRange', label: '金额范围' },
+    { value: 'overdueDays', label: '逾期天数' },
+    { value: 'customerTier', label: '客户层级' },
+    { value: 'projectType', label: '项目类型' },
+    { value: 'paymentMethod', label: '支付方式' }
+  ];
+
+  // 操作符选项
+  operators = [
+    { value: 'equals', label: '等于' },
+    { value: 'greaterThan', label: '大于' },
+    { value: 'lessThan', label: '小于' },
+    { value: 'between', label: '介于' },
+    { value: 'contains', label: '包含' }
+  ];
+
+  // 动作类型选项
+  actionTypes = [
+    { value: 'sendReminder', label: '发送提醒' },
+    { value: 'applyDiscount', label: '应用折扣' },
+    { value: 'extendDueDate', label: '延长期限' },
+    { value: 'autoConfirm', label: '自动确认' },
+    { value: 'notifyManager', label: '通知经理' }
+  ];
+
+  // 新规则模板
+  newRule: AutoSettlementRule = {
+    id: '',
+    name: '',
+    enabled: true,
+    priority: 5,
+    conditions: [],
+    actions: [],
+    description: ''
+  };
+
+  constructor(private autoSettlementService: AutoSettlementService) {}
+
+  ngOnInit() {
+    this.loadRules();
+  }
+
+  // 加载规则
+  loadRules() {
+    this.autoSettlementService.getRules().subscribe(rules => {
+      this.rules.set(rules);
+    });
+  }
+
+  // 切换面板展开状态
+  togglePanel(ruleId: string) {
+    const panels = new Set(this.expandedPanels());
+    if (panels.has(ruleId)) {
+      panels.delete(ruleId);
+    } else {
+      panels.add(ruleId);
+    }
+    this.expandedPanels.set(panels);
+  }
+
+  // 添加新条件
+  addCondition(rule: AutoSettlementRule) {
+    const newCondition: SettlementCondition = {
+      type: 'amountRange',
+      operator: 'greaterThan',
+      value: 0
+    };
+    rule.conditions.push(newCondition);
+  }
+
+  // 删除条件
+  removeCondition(rule: AutoSettlementRule, index: number) {
+    rule.conditions.splice(index, 1);
+  }
+
+  // 添加新动作
+  addAction(rule: AutoSettlementRule) {
+    const newAction: SettlementAction = {
+      type: 'sendReminder',
+      params: {}
+    };
+    rule.actions.push(newAction);
+  }
+
+  // 删除动作
+  removeAction(rule: AutoSettlementRule, index: number) {
+    rule.actions.splice(index, 1);
+  }
+
+  // 保存规则
+  saveRule(rule: AutoSettlementRule) {
+    if (rule.id) {
+      this.autoSettlementService.updateRule(rule.id, rule);
+    } else {
+      rule.id = `rule-${Date.now()}`;
+      this.autoSettlementService.addRule(rule);
+    }
+    this.cancelAddNew();
+  }
+
+  // 删除规则
+  deleteRule(ruleId: string) {
+    if (confirm('确定要删除这条规则吗?')) {
+      this.autoSettlementService.deleteRule(ruleId);
+      this.loadRules();
+    }
+  }
+
+  // 启用/禁用规则
+  toggleRule(rule: AutoSettlementRule) {
+    rule.enabled = !rule.enabled;
+    this.autoSettlementService.updateRule(rule.id, { enabled: rule.enabled });
+  }
+
+  // 开始添加新规则
+  startAddNew() {
+    this.isAddingNew.set(true);
+    this.newRule = {
+      id: '',
+      name: '',
+      enabled: true,
+      priority: 5,
+      conditions: [],
+      actions: [],
+      description: ''
+    };
+  }
+
+  // 取消添加新规则
+  cancelAddNew() {
+    this.isAddingNew.set(false);
+  }
+
+  // 获取条件操作符选项
+  getOperatorOptions(conditionType: string) {
+    switch (conditionType) {
+      case 'amountRange':
+      case 'overdueDays':
+        return this.operators.filter(op => 
+          ['equals', 'greaterThan', 'lessThan', 'between'].includes(op.value)
+        );
+      case 'customerTier':
+      case 'projectType':
+      case 'paymentMethod':
+        return this.operators.filter(op => 
+          ['equals', 'contains'].includes(op.value)
+        );
+      default:
+        return this.operators;
+    }
+  }
+
+  // 获取动作参数配置
+  getActionParamsConfig(actionType: string) {
+    switch (actionType) {
+      case 'sendReminder':
+        return {
+          channels: ['wechat', 'sms', 'email', 'system'],
+          frequency: ['immediate', 'daily', 'weekly']
+        };
+      case 'applyDiscount':
+        return {
+          type: ['percentage', 'fixed'],
+          maxAmount: 1000
+        };
+      case 'extendDueDate':
+        return {
+          days: 7
+        };
+      case 'autoConfirm':
+        return {
+          immediate: true
+        };
+      case 'notifyManager':
+        return {
+          threshold: 10000
+        };
+      default:
+        return {};
+    }
+  }
+
+  // 格式化条件显示
+  formatCondition(condition: SettlementCondition): string {
+    switch (condition.type) {
+      case 'amountRange':
+        return `金额 ${this.getOperatorLabel(condition.operator)} ${condition.value}`;
+      case 'overdueDays':
+        return `逾期天数 ${this.getOperatorLabel(condition.operator)} ${condition.value}`;
+      case 'customerTier':
+        return `客户层级 ${this.getOperatorLabel(condition.operator)} ${condition.value}`;
+      default:
+        return `${condition.type} ${condition.operator} ${condition.value}`;
+    }
+  }
+
+  // 格式化动作显示
+  formatAction(action: SettlementAction): string {
+    switch (action.type) {
+      case 'sendReminder':
+        return '发送提醒';
+      case 'applyDiscount':
+        return '应用折扣';
+      case 'extendDueDate':
+        return '延长期限';
+      case 'autoConfirm':
+        return '自动确认';
+      case 'notifyManager':
+        return '通知经理';
+      default:
+        return action.type;
+    }
+  }
+
+  // 获取操作符标签
+  private getOperatorLabel(operator: string): string {
+    const op = this.operators.find(o => o.value === operator);
+    return op ? op.label : operator;
+  }
+}

+ 236 - 23
src/app/shared/components/complaint-card/complaint-card.html

@@ -1,35 +1,130 @@
 <div class="complaint-card">
-  <!-- 统计数据概览 -->
-  <div class="stats-overview">
-    <h4>投诉处理概览</h4>
-    <div class="stats-grid">
-      <div class="stat-item total">
-        <div class="stat-value">{{ stats().totalCount }}</div>
-        <div class="stat-label">总投诉数</div>
+  <!-- 企业微信监控控制区域 -->
+  <div class="wechat-monitoring-section">
+    <div class="monitoring-header">
+      <h4>企业微信投诉监控</h4>
+      <div class="monitoring-status" [class]="'status-' + realTimeTaskStatus()">
+        {{ realTimeTaskStatus() === 'idle' ? '未启动' : 
+           realTimeTaskStatus() === 'processing' ? '监控中' : '已启动' }}
       </div>
-      <div class="stat-item pending">
-        <div class="stat-value">{{ stats().pendingCount }}</div>
-        <div class="stat-label">待处理</div>
-      </div>
-      <div class="stat-item processing">
-        <div class="stat-value">{{ stats().processingCount }}</div>
-        <div class="stat-label">处理中</div>
+    </div>
+    
+    <div class="monitoring-controls">
+      <button class="btn btn-primary" 
+              (click)="startWeChatMonitoring()"
+              [disabled]="realTimeTaskStatus() === 'processing'">
+        <i class="fas fa-play"></i>
+        启动监控
+      </button>
+      
+      <button class="btn btn-secondary" 
+              (click)="setupKeywordMonitoring()">
+        <i class="fas fa-cog"></i>
+        关键词设置
+      </button>
+      
+      <div class="monitoring-stats">
+        <span class="stat-item">
+          <i class="fas fa-eye"></i>
+          监控群组: {{ monitoredGroups || 5 }}
+        </span>
+        <span class="stat-item">
+          <i class="fas fa-bell"></i>
+          今日检测: {{ todayDetections || 3 }}
+        </span>
       </div>
-      <div class="stat-item resolved">
-        <div class="stat-value">{{ stats().resolvedCount }}</div>
-        <div class="stat-label">已解决</div>
+    </div>
+  </div>
+
+  <!-- 实时代办项处理区域 -->
+  <div class="realtime-tasks-section" *ngIf="realTimeTaskStatus() !== 'idle'">
+    <div class="section-header">
+      <h4>实时代办项</h4>
+      <span class="task-count">{{ urgentComplaints().length }} 项待处理</span>
+    </div>
+    
+    <div class="realtime-task-list">
+      <div class="task-item" 
+           *ngFor="let complaint of urgentComplaints()" 
+           [class.processing]="processingTaskId() === complaint.id">
+        <div class="task-info">
+          <div class="task-title">{{ complaint.description }}</div>
+          <div class="task-meta">
+            <span class="customer">{{ complaint.customerName }}</span>
+            <span class="time">{{ complaint.submitTime | date:'HH:mm' }}</span>
+            <span class="urgency" [class]="'urgency-' + complaint.urgencyLevel">
+              {{ complaint.urgencyLevel === 'critical' ? '紧急' : '重要' }}
+            </span>
+          </div>
+        </div>
+        
+        <div class="task-actions">
+          <button class="btn btn-sm btn-primary" 
+                  (click)="processRealTimeTask(complaint.id)"
+                  [disabled]="processingTaskId() === complaint.id">
+            <i class="fas fa-play" *ngIf="processingTaskId() !== complaint.id"></i>
+            <i class="fas fa-spinner fa-spin" *ngIf="processingTaskId() === complaint.id"></i>
+            {{ processingTaskId() === complaint.id ? '处理中' : '立即处理' }}
+          </button>
+          
+          <button class="btn btn-sm btn-warning" 
+                  (click)="escalateComplaint(complaint)">
+            <i class="fas fa-arrow-up"></i>
+            升级
+          </button>
+        </div>
       </div>
-      <div class="stat-item high-priority">
-        <div class="stat-value">{{ stats().highPriorityCount }}</div>
-        <div class="stat-label">高优先级</div>
+    </div>
+  </div>
+
+  <!-- 企业微信监控控制面板 -->
+  <div class="wechat-monitoring-panel">
+    <h4>企业微信投诉监控</h4>
+    <div class="monitoring-controls">
+      <button class="control-btn start-monitoring" (click)="startWeChatMonitoring()">
+        <span class="btn-icon">📱</span>
+        开始微信监控
+      </button>
+      <button class="control-btn setup-keywords" (click)="setupKeywordMonitoring()">
+        <span class="btn-icon">🔍</span>
+        设置关键词
+      </button>
+      <div class="monitoring-status">
+        <span class="status-label">监控状态:</span>
+        <span class="status-indicator active">运行中</span>
       </div>
-      <div class="stat-item resolution-time">
-        <div class="stat-value">{{ stats().averageResolutionTime }}<span class="time-suffix">天</span></div>
-        <div class="stat-label">平均解决时间</div>
+    </div>
+    
+    <div class="keyword-display">
+      <span class="keyword-label">监控关键词:</span>
+      <div class="keyword-tags">
+        @for (keyword of monitorKeywords; track keyword) {
+          <span class="keyword-tag">{{ keyword }}</span>
+        }
       </div>
     </div>
   </div>
 
+  <!-- 实时代办项状态 -->
+  @if (realTimeTaskStatus() !== 'idle') {
+    <div class="real-time-task-status">
+      <div class="task-status-header">
+        <h5>实时代办项处理</h5>
+        <span class="task-status-badge" [class]="realTimeTaskStatus()">
+          {{ realTimeTaskStatus() === 'processing' ? '处理中...' : '已完成' }}
+        </span>
+      </div>
+      @if (realTimeTaskStatus() === 'processing') {
+        <div class="task-progress">
+          <div class="progress-bar">
+            <div class="progress-fill"></div>
+          </div>
+          <span class="progress-text">正在创建代办项:{{ processingTaskId() }}</span>
+        </div>
+      }
+    </div>
+  }
+
   <!-- 优先级统计 -->
   <div class="priority-stats">
     <h5>优先级分布</h5>
@@ -133,6 +228,33 @@
           }
         </select>
       </div>
+
+      <!-- 新增筛选项 -->
+      <div class="filter-group">
+        <label>来源筛选:</label>
+        <select 
+          class="filter-select"
+          [value]="sourceFilter()"
+          (change)="updateSourceFilter($event)">
+          <option value="all">全部来源</option>
+          @for (source of complaintSources; track source.value) {
+            <option [value]="source.value">{{ source.icon }} {{ source.label }}</option>
+          }
+        </select>
+      </div>
+
+      <div class="filter-group">
+        <label>紧急程度:</label>
+        <select 
+          class="filter-select"
+          [value]="urgencyFilter()"
+          (change)="updateUrgencyFilter($event)">
+          <option value="all">全部级别</option>
+          @for (urgency of urgencyLevels; track urgency.value) {
+            <option [value]="urgency.value">{{ urgency.label }}</option>
+          }
+        </select>
+      </div>
     </div>
   </div>
 
@@ -149,6 +271,24 @@
                 <div class="priority-badge" [class]="getPriorityClass(complaint.priority || 'low')" [style.background-color]="getPriorityInfo(complaint.priority || 'low').color">
                   {{ getPriorityInfo(complaint.priority || 'low').label }}优先级
                 </div>
+                <!-- 新增来源标识 -->
+                <div class="source-badge" [class]="complaint.source">
+                  {{ getComplaintSourceInfo(complaint).icon }} {{ getComplaintSourceInfo(complaint).label }}
+                </div>
+                <!-- 紧急程度标识 -->
+                @if (complaint.urgencyLevel && complaint.urgencyLevel !== 'normal') {
+                  <div class="urgency-badge" [style.background-color]="getUrgencyInfo(complaint).color">
+                    {{ getUrgencyInfo(complaint).label }}
+                  </div>
+                }
+                <!-- 自动标注标识 -->
+                @if (isAutoTagged(complaint)) {
+                  <span class="auto-tag-badge">🤖 自动</span>
+                }
+                <!-- 升级标识 -->
+                @if (complaint.escalationLevel && complaint.escalationLevel > 0) {
+                  <span class="escalation-badge">⬆️ {{ getEscalationDisplay(complaint.escalationLevel) }}</span>
+                }
               </div>
               <div class="header-right">
                 <span class="status-badge" [class]="getStatusClass(complaint)">
@@ -157,6 +297,9 @@
                 @if (isOverdue(complaint)) {
                   <span class="overdue-badge">超时</span>
                 }
+                @if (complaint.followUpRequired) {
+                  <span class="follow-up-badge">需跟进</span>
+                }
               </div>
             </div>
             
@@ -168,6 +311,42 @@
                   <span class="customer-name">{{ complaint.customerName }}</span>
                 </div>
               }
+
+              <!-- 微信相关信息 -->
+              @if (complaint.source === 'wechat_auto' && complaint.wechatGroupName) {
+                <div class="wechat-info">
+                  <span class="wechat-label">📱 微信群:</span>
+                  <span class="wechat-group">{{ complaint.wechatGroupName }}</span>
+                  @if (complaint.keywordMatched && complaint.keywordMatched.length > 0) {
+                    <div class="matched-keywords">
+                      <span class="keyword-label">匹配关键词:</span>
+                      @for (keyword of complaint.keywordMatched; track keyword) {
+                        <span class="matched-keyword">{{ keyword }}</span>
+                      }
+                    </div>
+                  }
+                </div>
+              }
+
+              <!-- 分配信息 -->
+              @if (complaint.assignedTo) {
+                <div class="assignment-info">
+                  <span class="assignment-label">👤 处理人:</span>
+                  <span class="assignee-name">{{ complaint.assignedTo }}</span>
+                </div>
+              }
+
+              <!-- 标签显示 -->
+              @if (complaint.tags && complaint.tags.length > 0) {
+                <div class="tags-section">
+                  <span class="tags-label">🏷️ 标签:</span>
+                  <div class="tags-list">
+                    @for (tag of complaint.tags; track tag) {
+                      <span class="complaint-tag">{{ tag }}</span>
+                    }
+                  </div>
+                </div>
+              }
               
               <div class="complaint-description">
                 <h4>投诉内容</h4>
@@ -230,6 +409,10 @@
                   <span class="btn-icon">🔧</span>
                   开始处理
                 </button>
+                <button class="action-btn task-btn" (click)="createRealTimeTask(complaint)">
+                  <span class="btn-icon">📋</span>
+                  创建代办项
+                </button>
               } @else if (complaint.status === '处理中') {
                 <button class="action-btn complete-btn" (click)="completeProcessing(complaint)">
                   <span class="btn-icon">✅</span>
@@ -247,6 +430,36 @@
                   <span class="btn-icon">👁️</span>
                   查看详情
                 </button>
+                
+                <!-- 新增操作按钮 -->
+                @if (complaint.escalationLevel !== undefined && complaint.escalationLevel < 3) {
+                  <button class="action-btn escalate-btn" (click)="escalateComplaint(complaint)">
+                    <span class="btn-icon">⬆️</span>
+                    升级处理
+                  </button>
+                }
+                
+                <button class="action-btn assign-btn" (click)="assignComplaint(complaint, '处理员A')">
+                  <span class="btn-icon">👤</span>
+                  分配处理
+                </button>
+                
+                <button class="action-btn tag-btn" (click)="addComplaintTag(complaint, '重要')">
+                  <span class="btn-icon">🏷️</span>
+                  添加标签
+                </button>
+                
+                @if (!complaint.followUpRequired) {
+                  <button class="action-btn follow-up-btn" (click)="setFollowUpRequired(complaint, true)">
+                    <span class="btn-icon">📞</span>
+                    设置跟进
+                  </button>
+                } @else {
+                  <button class="action-btn follow-up-btn active" (click)="setFollowUpRequired(complaint, false)">
+                    <span class="btn-icon">✓</span>
+                    取消跟进
+                  </button>
+                }
               }
             </div>
           </div>

+ 654 - 2
src/app/shared/components/complaint-card/complaint-card.scss

@@ -2,6 +2,243 @@
 
 :host { display: block; height: 100%; }
 
+// 企业微信监控样式
+.wechat-monitoring-section {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+  color: white;
+
+  .monitoring-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+
+    h4 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .monitoring-status {
+      padding: 4px 12px;
+      border-radius: 20px;
+      font-size: 12px;
+      font-weight: 500;
+
+      &.status-idle {
+        background: rgba(255, 255, 255, 0.2);
+      }
+
+      &.status-processing {
+        background: #ffc107;
+        color: #000;
+        animation: pulse 2s infinite;
+      }
+
+      &.status-completed {
+        background: #28a745;
+      }
+    }
+  }
+
+  .monitoring-controls {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+    flex-wrap: wrap;
+
+    .btn {
+      padding: 8px 16px;
+      border: none;
+      border-radius: 6px;
+      font-size: 14px;
+      cursor: pointer;
+      transition: all 0.3s ease;
+
+      &.btn-primary {
+        background: #007bff;
+        color: white;
+
+        &:hover:not(:disabled) {
+          background: #0056b3;
+        }
+
+        &:disabled {
+          background: rgba(255, 255, 255, 0.3);
+          cursor: not-allowed;
+        }
+      }
+
+      &.btn-secondary {
+        background: rgba(255, 255, 255, 0.2);
+        color: white;
+
+        &:hover {
+          background: rgba(255, 255, 255, 0.3);
+        }
+      }
+
+      i {
+        margin-right: 6px;
+      }
+    }
+
+    .monitoring-stats {
+      display: flex;
+      gap: 20px;
+      margin-left: auto;
+
+      .stat-item {
+        display: flex;
+        align-items: center;
+        font-size: 14px;
+
+        i {
+          margin-right: 6px;
+          opacity: 0.8;
+        }
+      }
+    }
+  }
+}
+
+// 实时代办项样式
+.realtime-tasks-section {
+  background: #fff;
+  border: 1px solid #e9ecef;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+
+    h4 {
+      margin: 0;
+      color: #333;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .task-count {
+      background: #dc3545;
+      color: white;
+      padding: 4px 12px;
+      border-radius: 20px;
+      font-size: 12px;
+      font-weight: 500;
+    }
+  }
+
+  .realtime-task-list {
+    .task-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 15px;
+      border: 1px solid #e9ecef;
+      border-radius: 8px;
+      margin-bottom: 10px;
+      transition: all 0.3s ease;
+
+      &:hover {
+        border-color: #007bff;
+        box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
+      }
+
+      &.processing {
+        border-color: #ffc107;
+        background: #fff9c4;
+      }
+
+      .task-info {
+        flex: 1;
+
+        .task-title {
+          font-weight: 500;
+          color: #333;
+          margin-bottom: 8px;
+          font-size: 14px;
+        }
+
+        .task-meta {
+          display: flex;
+          gap: 15px;
+          font-size: 12px;
+          color: #666;
+
+          .customer {
+            font-weight: 500;
+          }
+
+          .urgency {
+            padding: 2px 8px;
+            border-radius: 12px;
+            font-weight: 500;
+
+            &.urgency-urgent {
+              background: #ffc107;
+              color: #000;
+            }
+
+            &.urgency-critical {
+              background: #dc3545;
+              color: white;
+            }
+          }
+        }
+      }
+
+      .task-actions {
+        display: flex;
+        gap: 8px;
+
+        .btn {
+          padding: 6px 12px;
+          border: none;
+          border-radius: 4px;
+          font-size: 12px;
+          cursor: pointer;
+          transition: all 0.3s ease;
+
+          &.btn-primary {
+            background: #007bff;
+            color: white;
+
+            &:hover:not(:disabled) {
+              background: #0056b3;
+            }
+
+            &:disabled {
+              background: #6c757d;
+              cursor: not-allowed;
+            }
+          }
+
+          &.btn-warning {
+            background: #ffc107;
+            color: #000;
+
+            &:hover {
+              background: #e0a800;
+            }
+          }
+
+          i {
+            margin-right: 4px;
+          }
+        }
+      }
+    }
+  }
+}
+
 .complaint-card {
   padding: ios.$ios-spacing-md;
   background: ios.$ios-background;
@@ -54,10 +291,204 @@
         &.resolved .stat-value { color: ios.$ios-success; }
         &.high-priority .stat-value { color: ios.$ios-danger; }
         &.resolution-time .stat-value { color: #722ed1; }
+        
+        // 新增统计项样式
+        &.wechat-count .stat-value { color: #52c41a; }
+        &.auto-tagged .stat-value { color: #13c2c2; }
+        &.overdue .stat-value { color: #ff4d4f; }
+        &.follow-up .stat-value { color: #fa8c16; }
+      }
+    }
+  }
+
+  // 企业微信监控面板
+  .wechat-monitoring-panel {
+    margin-bottom: ios.$ios-spacing-lg;
+    padding: ios.$ios-spacing-md;
+    background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
+    border-radius: ios.$ios-radius-md;
+    border: 1px solid #91d5ff;
+
+    h4 {
+      margin: 0 0 ios.$ios-spacing-md 0;
+      font-size: ios.$ios-font-size-lg;
+      font-weight: ios.$ios-font-weight-semibold;
+      color: #1890ff;
+      display: flex;
+      align-items: center;
+      gap: ios.$ios-spacing-sm;
+
+      &::before {
+        content: '📱';
+        font-size: ios.$ios-font-size-lg;
+      }
+    }
+
+    .monitoring-controls {
+      display: flex;
+      align-items: center;
+      gap: ios.$ios-spacing-md;
+      margin-bottom: ios.$ios-spacing-md;
+      flex-wrap: wrap;
+
+      .control-btn {
+        display: flex;
+        align-items: center;
+        gap: ios.$ios-spacing-xs;
+        padding: ios.$ios-spacing-sm ios.$ios-spacing-md;
+        background: #1890ff;
+        color: white;
+        border: none;
+        border-radius: ios.$ios-radius-sm;
+        font-size: ios.$ios-font-size-sm;
+        cursor: pointer;
+        transition: all 0.3s ease;
+
+        &:hover {
+          background: #40a9ff;
+          transform: translateY(-1px);
+        }
+
+        .btn-icon {
+          font-size: ios.$ios-font-size-md;
+        }
+
+        &.setup-keywords {
+          background: #52c41a;
+
+          &:hover {
+            background: #73d13d;
+          }
+        }
+      }
+
+      .monitoring-status {
+        display: flex;
+        align-items: center;
+        gap: ios.$ios-spacing-xs;
+        margin-left: auto;
+
+        .status-label {
+          font-size: ios.$ios-font-size-sm;
+          color: ios.$ios-text-secondary;
+        }
+
+        .status-indicator {
+          padding: 2px 8px;
+          border-radius: 10px;
+          font-size: ios.$ios-font-size-xs;
+          font-weight: ios.$ios-font-weight-medium;
+
+          &.active {
+            background: #f6ffed;
+            color: #52c41a;
+            border: 1px solid #b7eb8f;
+          }
+        }
+      }
+    }
+
+    .keyword-display {
+      display: flex;
+      align-items: center;
+      gap: ios.$ios-spacing-sm;
+      flex-wrap: wrap;
+
+      .keyword-label {
+        font-size: ios.$ios-font-size-sm;
+        color: ios.$ios-text-secondary;
+        white-space: nowrap;
+      }
+
+      .keyword-tags {
+        display: flex;
+        gap: ios.$ios-spacing-xs;
+        flex-wrap: wrap;
+
+        .keyword-tag {
+          padding: 2px 8px;
+          background: #fff2e8;
+          color: #fa8c16;
+          border: 1px solid #ffd591;
+          border-radius: 10px;
+          font-size: ios.$ios-font-size-xs;
+          font-weight: ios.$ios-font-weight-medium;
+        }
       }
     }
   }
 
+  // 实时代办项状态
+  .real-time-task-status {
+    margin-bottom: ios.$ios-spacing-lg;
+    padding: ios.$ios-spacing-md;
+    background: linear-gradient(135deg, #fff7e6 0%, #fffbe6 100%);
+    border-radius: ios.$ios-radius-md;
+    border: 1px solid #ffd591;
+
+    .task-status-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: ios.$ios-spacing-sm;
+
+      h5 {
+        margin: 0;
+        font-size: ios.$ios-font-size-md;
+        font-weight: ios.$ios-font-weight-semibold;
+        color: #fa8c16;
+      }
+
+      .task-status-badge {
+        padding: 4px 12px;
+        border-radius: ios.$ios-radius-sm;
+        font-size: ios.$ios-font-size-xs;
+        font-weight: ios.$ios-font-weight-medium;
+
+        &.processing {
+          background: #fff2e8;
+          color: #fa8c16;
+          border: 1px solid #ffd591;
+        }
+
+        &.completed {
+          background: #f6ffed;
+          color: #52c41a;
+          border: 1px solid #b7eb8f;
+        }
+      }
+    }
+
+    .task-progress {
+      .progress-bar {
+        width: 100%;
+        height: 6px;
+        background: #fff7e6;
+        border-radius: 3px;
+        overflow: hidden;
+        margin-bottom: ios.$ios-spacing-xs;
+
+        .progress-fill {
+          height: 100%;
+          background: linear-gradient(90deg, #fa8c16, #ffa940);
+          border-radius: 3px;
+          animation: progress-animation 2s ease-in-out infinite;
+        }
+      }
+
+      .progress-text {
+        font-size: ios.$ios-font-size-xs;
+        color: ios.$ios-text-secondary;
+      }
+    }
+  }
+
+  @keyframes progress-animation {
+    0% { width: 0%; }
+    50% { width: 70%; }
+    100% { width: 100%; }
+  }
+
   // 优先级统计 - 小卡片样式
   .priority-stats {
     margin-bottom: ios.$ios-spacing-lg;
@@ -376,6 +807,64 @@
             font-weight: 500;
             white-space: nowrap;
           }
+
+          // 新增标识样式
+          .source-badge {
+            padding: 4px 10px;
+            border-radius: 12px;
+            font-size: 11px;
+            font-weight: 500;
+            white-space: nowrap;
+
+            &.manual {
+              background: #f0f0f0;
+              color: #666;
+              border: 1px solid #d9d9d9;
+            }
+
+            &.wechat_auto {
+              background: #e6f7ff;
+              color: #1890ff;
+              border: 1px solid #91d5ff;
+            }
+
+            &.keyword_monitor {
+              background: #fff2e8;
+              color: #fa8c16;
+              border: 1px solid #ffd591;
+            }
+          }
+
+          .urgency-badge {
+            color: white;
+            padding: 4px 10px;
+            border-radius: 12px;
+            font-size: 11px;
+            font-weight: 500;
+            white-space: nowrap;
+          }
+
+          .auto-tag-badge {
+            background: #f6ffed;
+            color: #52c41a;
+            border: 1px solid #b7eb8f;
+            padding: 2px 8px;
+            border-radius: 10px;
+            font-size: 10px;
+            font-weight: 500;
+            white-space: nowrap;
+          }
+
+          .escalation-badge {
+            background: #fff1f0;
+            color: #ff4d4f;
+            border: 1px solid #ffccc7;
+            padding: 2px 8px;
+            border-radius: 10px;
+            font-size: 10px;
+            font-weight: 500;
+            white-space: nowrap;
+          }
         }
 
         .header-right {
@@ -418,6 +907,17 @@
             font-weight: 600;
             animation: pulse 2s infinite;
           }
+
+          .follow-up-badge {
+            background: #fff2e8;
+            color: #fa8c16;
+            border: 1px solid #ffd591;
+            padding: 2px 8px;
+            border-radius: 10px;
+            font-size: 10px;
+            font-weight: 500;
+            white-space: nowrap;
+          }
         }
       }
 
@@ -449,6 +949,96 @@
           }
         }
 
+        // 新增信息区域样式
+        .wechat-info {
+          padding: 8px 12px;
+          background: #e6f7ff;
+          border-radius: 8px;
+          border-left: 3px solid #1890ff;
+
+          .wechat-label {
+            font-weight: 500;
+            color: #1890ff;
+            margin-right: 8px;
+          }
+
+          .wechat-group {
+            color: #1890ff;
+            font-weight: 600;
+          }
+
+          .matched-keywords {
+            margin-top: 6px;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            flex-wrap: wrap;
+
+            .keyword-label {
+              font-size: 12px;
+              color: #666;
+            }
+
+            .matched-keyword {
+              background: #fff2e8;
+              color: #fa8c16;
+              border: 1px solid #ffd591;
+              padding: 2px 6px;
+              border-radius: 8px;
+              font-size: 11px;
+              font-weight: 500;
+            }
+          }
+        }
+
+        .assignment-info {
+          padding: 8px 12px;
+          background: #f6ffed;
+          border-radius: 8px;
+          border-left: 3px solid #52c41a;
+
+          .assignment-label {
+            font-weight: 500;
+            color: #52c41a;
+            margin-right: 8px;
+          }
+
+          .assignee-name {
+            color: #52c41a;
+            font-weight: 600;
+          }
+        }
+
+        .tags-section {
+          padding: 8px 12px;
+          background: #fff7e6;
+          border-radius: 8px;
+          border-left: 3px solid #fa8c16;
+
+          .tags-label {
+            font-weight: 500;
+            color: #fa8c16;
+            margin-right: 8px;
+          }
+
+          .tags-list {
+            display: flex;
+            gap: 6px;
+            flex-wrap: wrap;
+            margin-top: 6px;
+
+            .complaint-tag {
+              background: #fff2e8;
+              color: #fa8c16;
+              border: 1px solid #ffd591;
+              padding: 2px 8px;
+              border-radius: 10px;
+              font-size: 11px;
+              font-weight: 500;
+            }
+          }
+        }
+
         .complaint-description {
           h4 {
             margin: 0 0 8px 0;
@@ -591,7 +1181,8 @@
         background: #f8f9fa;
         border-top: 1px solid #e9ecef;
         display: flex;
-        justify-content: space-between;
+        flex-wrap: wrap;
+        justify-content: flex-start;
         align-items: center;
         gap: 8px;
 
@@ -599,7 +1190,7 @@
           display: flex;
           align-items: center;
           gap: 6px;
-          padding: 8px 16px;
+          padding: 6px 12px;
           border: none;
           border-radius: 6px;
           font-size: 13px;
@@ -607,6 +1198,8 @@
           cursor: pointer;
           transition: all 0.2s ease;
           white-space: nowrap;
+          flex: 0 1 auto;
+          max-width: 100%;
 
           .btn-icon {
             font-size: 14px;
@@ -641,6 +1234,65 @@
               transform: translateY(-1px);
             }
           }
+
+          // 新增操作按钮样式
+          &.task-btn {
+            background: #fa8c16;
+            color: white;
+
+            &:hover {
+              background: #d46b08;
+              transform: translateY(-1px);
+            }
+          }
+
+          &.escalate-btn {
+            background: #ff4d4f;
+            color: white;
+
+            &:hover {
+              background: #cf1322;
+              transform: translateY(-1px);
+            }
+          }
+
+          &.assign-btn {
+            background: #52c41a;
+            color: white;
+
+            &:hover {
+              background: #389e0d;
+              transform: translateY(-1px);
+            }
+          }
+
+          &.tag-btn {
+            background: #722ed1;
+            color: white;
+
+            &:hover {
+              background: #531dab;
+              transform: translateY(-1px);
+            }
+          }
+
+          &.follow-up-btn {
+            background: #13c2c2;
+            color: white;
+
+            &:hover {
+              background: #08979c;
+              transform: translateY(-1px);
+            }
+
+            &.active {
+              background: #52c41a;
+
+              &:hover {
+                background: #389e0d;
+              }
+            }
+          }
         }
 
         .completed-status {

+ 353 - 11
src/app/shared/components/complaint-card/complaint-card.ts

@@ -17,6 +17,21 @@ export interface ComplaintItem {
   customerName?: string;
   priority?: string;
   images?: string[];
+  // 新增字段
+  source?: 'manual' | 'wechat_auto' | 'keyword_monitor'; // 投诉来源
+  wechatGroupName?: string; // 企业微信群名称
+  wechatMessageId?: string; // 微信消息ID
+  keywordMatched?: string[]; // 匹配的关键词
+  autoTagged?: boolean; // 是否自动标注
+  urgencyLevel?: 'normal' | 'urgent' | 'critical'; // 紧急程度
+  assignedTo?: string; // 分配给谁处理
+  estimatedResolutionTime?: Date; // 预计解决时间
+  actualResolutionTime?: Date; // 实际解决时间
+  customerSatisfaction?: number; // 客户满意度评分
+  followUpRequired?: boolean; // 是否需要后续跟进
+  relatedProjectId?: string; // 关联项目ID
+  escalationLevel?: number; // 升级级别 (0-3)
+  tags?: string[]; // 标签
 }
 
 interface ComplaintStats {
@@ -28,6 +43,15 @@ interface ComplaintStats {
   averageResolutionTime: number;
   priorityStats: { [key: string]: number };
   typeStats: { [key: string]: number };
+  // 新增统计字段
+  sourceStats: { [key: string]: number };
+  urgencyStats: { [key: string]: number };
+  escalationStats: { [key: string]: number };
+  autoTaggedCount: number;
+  wechatComplaintCount: number;
+  keywordMonitorCount: number;
+  overdueCount: number;
+  followUpRequiredCount: number;
 }
 
 @Component({
@@ -45,6 +69,43 @@ export class ComplaintCardComponent {
   priorityFilter = signal<string>('all');
   typeFilter = signal<string>('all');
   searchKeyword = signal<string>('');
+  sourceFilter = signal<string>('all');
+  urgencyFilter = signal<string>('all');
+
+  // 新增缺失的属性
+  monitoredGroups = 5; // 监控群组数量
+  todayDetections = 3; // 今日检测数量
+  
+  // 紧急投诉计算属性
+  urgentComplaints = computed(() => {
+    return this.complaints.filter(complaint => 
+      complaint.urgencyLevel === 'urgent' || 
+      complaint.urgencyLevel === 'critical' ||
+      complaint.priority === 'urgent' ||
+      complaint.priority === 'high'
+    );
+  });
+
+  // 投诉来源选项
+  complaintSources = [
+    { value: 'manual', label: '人工创建', icon: '👥' },
+    { value: 'wechat_auto', label: '微信自动', icon: '🤖' },
+    { value: 'keyword_monitor', label: '关键词监控', icon: '🔍' }
+  ];
+
+  // 紧急程度选项
+  urgencyLevels = [
+    { value: 'normal', label: '普通', color: '#52c41a' },
+    { value: 'urgent', label: '紧急', color: '#faad14' },
+    { value: 'critical', label: '严重', color: '#f5222d' }
+  ];
+
+  // 关键词监控配置
+  monitorKeywords = ['不满意', '投诉', '退款', '差评', '问题', '延期', '质量差'];
+
+  // 实时代办项状态
+  realTimeTaskStatus = signal<'idle' | 'processing' | 'completed'>('idle');
+  processingTaskId = signal<string>('');
 
   // 投诉类型
   complaintTypes = [
@@ -95,6 +156,31 @@ export class ComplaintCardComponent {
       typeStats[t.value] = complaints.filter(c => this.getComplaintType(c) === t.value).length;
     });
 
+    // 来源统计
+    const sourceStats: { [key: string]: number } = {};
+    this.complaintSources.forEach(s => {
+      sourceStats[s.value] = complaints.filter(c => c.source === s.value).length;
+    });
+
+    // 紧急程度统计
+    const urgencyStats: { [key: string]: number } = {};
+    this.urgencyLevels.forEach(u => {
+      urgencyStats[u.value] = complaints.filter(c => c.urgencyLevel === u.value).length;
+    });
+
+    // 升级级别统计
+    const escalationStats: { [key: string]: number } = {};
+    for (let i = 0; i <= 3; i++) {
+      escalationStats[i.toString()] = complaints.filter(c => c.escalationLevel === i).length;
+    }
+
+    // 其他统计
+    const autoTaggedCount = complaints.filter(c => c.autoTagged === true).length;
+    const wechatComplaintCount = complaints.filter(c => c.source === 'wechat_auto').length;
+    const keywordMonitorCount = complaints.filter(c => c.source === 'keyword_monitor').length;
+    const overdueCount = complaints.filter(c => this.isOverdue(c)).length;
+    const followUpRequiredCount = complaints.filter(c => c.followUpRequired === true).length;
+
     return {
       totalCount,
       pendingCount,
@@ -103,7 +189,15 @@ export class ComplaintCardComponent {
       highPriorityCount,
       averageResolutionTime: Math.round(averageResolutionTime * 10) / 10,
       priorityStats,
-      typeStats
+      typeStats,
+      sourceStats,
+      urgencyStats,
+      escalationStats,
+      autoTaggedCount,
+      wechatComplaintCount,
+      keywordMonitorCount,
+      overdueCount,
+      followUpRequiredCount
     };
   });
 
@@ -132,13 +226,26 @@ export class ComplaintCardComponent {
       filtered = filtered.filter(complaint => this.getComplaintType(complaint) === this.typeFilter());
     }
 
+    // 来源筛选
+    if (this.sourceFilter() !== 'all') {
+      filtered = filtered.filter(complaint => complaint.source === this.sourceFilter());
+    }
+
+    // 紧急程度筛选
+    if (this.urgencyFilter() !== 'all') {
+      filtered = filtered.filter(complaint => complaint.urgencyLevel === this.urgencyFilter());
+    }
+
     // 关键词搜索
     if (this.searchKeyword()) {
       const keyword = this.searchKeyword().toLowerCase();
       filtered = filtered.filter(complaint => 
         complaint.description?.toLowerCase().includes(keyword) ||
         (complaint as any).customerName?.toLowerCase().includes(keyword) ||
-        complaint.type?.toLowerCase().includes(keyword)
+        complaint.type?.toLowerCase().includes(keyword) ||
+        complaint.wechatGroupName?.toLowerCase().includes(keyword) ||
+        complaint.tags?.some(tag => tag.toLowerCase().includes(keyword)) ||
+        complaint.keywordMatched?.some(kw => kw.toLowerCase().includes(keyword))
       );
     }
 
@@ -282,6 +389,233 @@ export class ComplaintCardComponent {
     this.searchKeyword.set(keyword);
   }
 
+  updateSourceFilter(event: any) {
+    this.sourceFilter.set(event.target.value);
+  }
+
+  updateUrgencyFilter(event: any) {
+    this.urgencyFilter.set(event.target.value);
+  }
+
+  // ==================== 新增投诉管理功能 ====================
+
+  // 创建实时代办项
+  createRealTimeTask(complaint: ComplaintItem): void {
+    this.realTimeTaskStatus.set('processing');
+    this.processingTaskId.set(complaint.id);
+    
+    console.log('创建实时代办项:', complaint.id);
+    
+    // 模拟异步处理
+    setTimeout(() => {
+      complaint.status = '处理中';
+      complaint.assignedTo = '当前用户'; // 实际应该从用户服务获取
+      this.realTimeTaskStatus.set('completed');
+      
+      // 这里可以调用服务来创建实时代办项
+      // this.complaintService.createRealTimeTask(complaint);
+    }, 2000);
+  }
+
+  // 企业微信自动获取投诉
+  startWeChatMonitoring(): void {
+    console.log('开始企业微信投诉监控');
+    
+    // 这里可以调用企业微信API服务
+    // this.wechatService.startComplaintMonitoring(this.monitorKeywords);
+    
+    // 模拟自动获取到的投诉
+    const autoComplaint: ComplaintItem = {
+      id: 'wechat_' + Date.now(),
+      type: 'service',
+      description: '客户在微信群中表达不满意,提到"服务态度不好"',
+      status: '待处理',
+      source: 'wechat_auto',
+      wechatGroupName: '项目交流群',
+      wechatMessageId: 'msg_' + Date.now(),
+      keywordMatched: ['不满意', '服务态度'],
+      autoTagged: true,
+      urgencyLevel: 'urgent',
+      priority: 'high',
+      submitTime: new Date(),
+      submittedAt: new Date(),
+      customerName: '张先生',
+      followUpRequired: true,
+      escalationLevel: 1
+    };
+    
+    // 添加到投诉列表
+    this.complaints.push(autoComplaint);
+    
+    alert('检测到新的微信投诉,已自动创建投诉记录');
+  }
+
+  // 设置关键词监控
+  setupKeywordMonitoring(): void {
+    console.log('设置关键词监控:', this.monitorKeywords);
+    
+    // 这里可以打开关键词设置弹窗
+    const newKeywords = prompt('请输入监控关键词(用逗号分隔):', this.monitorKeywords.join(','));
+    
+    if (newKeywords) {
+      this.monitorKeywords = newKeywords.split(',').map(k => k.trim()).filter(k => k);
+      console.log('更新关键词监控:', this.monitorKeywords);
+      
+      // 这里可以调用服务更新关键词配置
+      // this.complaintService.updateMonitorKeywords(this.monitorKeywords);
+    }
+  }
+
+  // 分配投诉处理人
+  assignComplaint(complaint: ComplaintItem, assignee: string): void {
+    complaint.assignedTo = assignee;
+    complaint.status = '处理中';
+    
+    console.log('分配投诉处理人:', complaint.id, '→', assignee);
+    
+    // 这里可以调用服务更新分配信息
+    // this.complaintService.assignComplaint(complaint.id, assignee);
+  }
+
+  // 升级投诉
+  escalateComplaint(complaint: ComplaintItem): void {
+    if (!complaint.escalationLevel) {
+      complaint.escalationLevel = 0;
+    }
+    
+    if (complaint.escalationLevel < 3) {
+      complaint.escalationLevel++;
+      complaint.urgencyLevel = complaint.escalationLevel >= 2 ? 'critical' : 'urgent';
+      complaint.priority = 'urgent';
+      
+      console.log('投诉升级:', complaint.id, '升级到级别', complaint.escalationLevel);
+      
+      // 这里可以调用服务处理升级逻辑
+      // this.complaintService.escalateComplaint(complaint.id, complaint.escalationLevel);
+    }
+  }
+
+  // 模拟从企业微信获取新投诉
+  simulateNewWeChatComplaint(): void {
+    const newComplaint: ComplaintItem = {
+      id: 'wechat_' + Date.now(),
+      type: 'quality',
+      description: '客户在微信群中反映图纸质量问题,要求重新制作',
+      submitTime: new Date(),
+      submittedAt: new Date(),
+      status: '待处理',
+      customerName: '张先生',
+      priority: 'urgent',
+      source: 'wechat_auto',
+      wechatGroupName: '项目交付群-张先生',
+      wechatMessageId: 'msg_' + Date.now(),
+      keywordMatched: ['质量差', '重新制作'],
+      autoTagged: true,
+      urgencyLevel: 'urgent',
+      followUpRequired: true,
+      relatedProjectId: 'proj_001',
+      escalationLevel: 1,
+      tags: ['质量问题', '微信投诉', '紧急处理']
+    };
+    
+    // 添加到投诉列表
+    this.complaints.push(newComplaint);
+    console.log('检测到新的微信投诉:', newComplaint);
+  }
+
+  // 实时处理代办项
+  processRealTimeTask(taskId: string): void {
+    this.processingTaskId.set(taskId);
+    this.realTimeTaskStatus.set('processing');
+    
+    console.log('开始处理实时代办项:', taskId);
+    
+    // 模拟处理过程
+    setTimeout(() => {
+      this.realTimeTaskStatus.set('completed');
+      this.processingTaskId.set('');
+      console.log('实时代办项处理完成');
+    }, 3000);
+  }
+
+  // 通知升级
+  notifyEscalation(complaint: ComplaintItem): void {
+    console.log('发送升级通知:', complaint.id);
+    // 这里可以实现邮件、短信或企业微信通知
+  }
+
+  // 设置预计解决时间
+  setEstimatedResolutionTime(complaint: ComplaintItem, hours: number): void {
+    const estimatedTime = new Date();
+    estimatedTime.setHours(estimatedTime.getHours() + hours);
+    complaint.estimatedResolutionTime = estimatedTime;
+    
+    console.log('设置预计解决时间:', complaint.id, estimatedTime);
+  }
+
+  // 客户满意度评分
+  setCustomerSatisfaction(complaint: ComplaintItem, score: number): void {
+    complaint.customerSatisfaction = Math.max(1, Math.min(5, score));
+    
+    console.log('客户满意度评分:', complaint.id, score);
+  }
+
+  // 批量处理投诉
+  batchProcessComplaints(complaintIds: string[], action: string): void {
+    console.log('批量处理投诉:', complaintIds, action);
+    
+    complaintIds.forEach(id => {
+      const complaint = this.complaints.find(c => c.id === id);
+      if (complaint) {
+        switch (action) {
+          case 'assign':
+            // 批量分配
+            break;
+          case 'tag':
+            // 批量添加标签
+            break;
+          case 'escalate':
+            // 批量升级
+            this.escalateComplaint(complaint);
+            break;
+        }
+      }
+    });
+  }
+
+  // 设置后续跟进
+  setFollowUpRequired(complaint: ComplaintItem, required: boolean): void {
+    complaint.followUpRequired = required;
+    
+    console.log('设置后续跟进:', complaint.id, '→', required);
+    
+    // 这里可以调用服务更新跟进状态
+    // this.complaintService.setFollowUpRequired(complaint.id, required);
+  }
+
+  // 获取投诉来源信息
+  getComplaintSourceInfo(complaint: ComplaintItem) {
+    const source = this.complaintSources.find(s => s.value === complaint.source);
+    return source || { value: 'manual', label: '人工创建', icon: '👥' };
+  }
+
+  // 获取紧急程度信息
+  getUrgencyInfo(complaint: ComplaintItem) {
+    const urgency = this.urgencyLevels.find(u => u.value === complaint.urgencyLevel);
+    return urgency || { value: 'normal', label: '普通', color: '#52c41a' };
+  }
+
+  // 检查是否为自动标注
+  isAutoTagged(complaint: ComplaintItem): boolean {
+    return complaint.autoTagged === true;
+  }
+
+  // 获取升级级别显示
+  getEscalationDisplay(level: number): string {
+    const levels = ['普通', '一级', '二级', '三级'];
+    return levels[level] || '普通';
+  }
+
   // 确认客户评价完成
   confirmCustomerReviewComplete(complaint: ComplaintItem): void {
     // 更新投诉状态
@@ -294,15 +628,23 @@ export class ComplaintCardComponent {
     // this.complaintService.confirmCustomerReview(complaint.id);
   }
 
-  // 确认投诉处理完成
+  // 确认投诉解决完成
   confirmComplaintResolutionComplete(complaint: ComplaintItem): void {
-    // 更新投诉状态为已解决
-    complaint.status = '已解决';
-    complaint.resolvedAt = new Date();
-    
-    console.log('确认投诉处理完成:', complaint.id);
-    
-    // 这里可以调用服务来更新后端数据
-    // this.complaintService.confirmResolution(complaint.id);
+    console.log('确认投诉解决完成:', complaint.id);
+    // 这里可以添加确认投诉解决完成的逻辑
+    // 例如:更新状态、发送通知等
+    complaint.status = 'resolved';
+    complaint.actualResolutionTime = new Date();
+  }
+
+  // 添加投诉标签方法
+  addComplaintTag(complaint: ComplaintItem, tag: string): void {
+    if (!complaint.tags) {
+      complaint.tags = [];
+    }
+    if (!complaint.tags.includes(tag)) {
+      complaint.tags.push(tag);
+      console.log(`为投诉 ${complaint.id} 添加标签: ${tag}`);
+    }
   }
 }

+ 230 - 1
src/app/shared/components/customer-review-card/customer-review-card.html

@@ -1,7 +1,22 @@
 <div class="customer-review-card">
   <!-- 统计数据概览 -->
   <div class="stats-overview">
-    <h4>客户评价概览</h4>
+    <div class="stats-header">
+      <h4>客户评价概览</h4>
+      <div class="view-controls">
+        <button class="view-toggle-btn" (click)="toggleViewMode()">
+          @if (viewMode() === 'simple') {
+            切换到详细视图
+          } @else {
+            切换到简单视图
+          }
+        </button>
+        <button class="export-btn" (click)="exportReviewReport()">
+          导出报告
+        </button>
+      </div>
+    </div>
+    
     <div class="stats-grid">
       <div class="stat-item total">
         <div class="stat-value">{{ stats().totalCount }}</div>
@@ -27,6 +42,33 @@
     </div>
   </div>
 
+  <!-- 详细评价维度统计 (仅在详细视图模式下显示) -->
+  @if (viewMode() === 'detailed' && stats().dimensionAverages) {
+    <div class="dimension-stats">
+      <h5>评价维度分析</h5>
+      <div class="dimension-grid">
+        @for (dimension of reviewDimensions; track dimension.key) {
+          @if (stats().dimensionAverages[dimension.key]) {
+            <div class="dimension-item">
+              <div class="dimension-header">
+                <span class="dimension-label">{{ dimension.label }}</span>
+                <span class="dimension-score" [class]="getDimensionScoreClass(stats().dimensionAverages[dimension.key]!)">
+                  {{ (stats().dimensionAverages[dimension.key]! | number:'1.1-1') }}
+                </span>
+              </div>
+              <div class="dimension-stars">
+                @for (star of getDimensionStars(stats().dimensionAverages[dimension.key]!); track $index) {
+                  <span class="star">{{ star }}</span>
+                }
+              </div>
+              <div class="dimension-desc">{{ dimension.description }}</div>
+            </div>
+          }
+        }
+      </div>
+    </div>
+  }
+
   <!-- 分类统计 -->
   <div class="category-stats">
     <h5>问题分类统计</h5>
@@ -40,6 +82,193 @@
     </div>
   </div>
 
+  <!-- 详细评价界面 -->
+  <div class="detailed-review-section" *ngIf="viewMode() === 'detailed'">
+    <div class="detailed-header">
+      <h4>详细评价管理</h4>
+      <div class="review-actions">
+        <button class="btn btn-primary" (click)="createDetailedReview('proj_001')">
+          <i class="fas fa-plus"></i>
+          创建详细评价
+        </button>
+      </div>
+    </div>
+
+    <!-- 详细评价表单 -->
+    <div class="detailed-form" *ngIf="showDetailedForm">
+      <!-- 多维度评价表单 -->
+      <div class="dimension-form">
+        <h4>多维度评价</h4>
+        <div class="dimension-grid">
+          <div class="dimension-item" *ngFor="let dimension of reviewDimensions">
+            <label>{{ dimension.label }}</label>
+            <div class="star-rating">
+              <span 
+                class="star" 
+                *ngFor="let star of [1,2,3,4,5]; let i = index"
+                [class.active]="(currentRatings[dimension.key] || 0) > i"
+                (click)="setDimensionRating(dimension.key, i + 1)">
+                ★
+              </span>
+            </div>
+            <span class="rating-text">{{ currentRatings[dimension.key] || 0 }}/5</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 场景评价 -->
+      <div class="scene-evaluation">
+        <h4>场景评价</h4>
+        <div class="scene-grid">
+          <div class="scene-item" *ngFor="let scene of sceneTypes">
+            <div class="scene-header">
+              <span class="scene-name">{{ scene.label }}</span>
+              <div class="star-rating">
+                <span 
+                  class="star" 
+                  *ngFor="let star of [1,2,3,4,5]; let i = index"
+                  [class.active]="(currentSceneRatings[scene.value] || 0) > i"
+                  (click)="setSceneRating(scene.value, i + 1)">
+                  ★
+                </span>
+              </div>
+              <span class="rating-text">{{ currentSceneRatings[scene.value] || 0 }}/5</span>
+            </div>
+            <textarea 
+              class="scene-feedback"
+              [(ngModel)]="currentSceneFeedback[scene.value]"
+              placeholder="请输入对该场景的具体反馈...">
+            </textarea>
+          </div>
+        </div>
+      </div>
+
+      <!-- 优化建议 -->
+      <div class="optimization-suggestions">
+        <h4>下次合作,您希望我们优化哪些点?</h4>
+        <div class="suggestion-grid">
+          <label class="suggestion-item" *ngFor="let category of optimizationCategories">
+            <input 
+              type="checkbox" 
+              [(ngModel)]="selectedOptimizations[category.key]">
+            <span class="checkmark"></span>
+            <div class="suggestion-content">
+              <span class="suggestion-label">{{ category.label }}</span>
+              <span class="suggestion-desc">{{ category.description }}</span>
+            </div>
+          </label>
+        </div>
+        
+        <div class="custom-suggestion">
+          <label>其他建议:</label>
+          <textarea 
+            [(ngModel)]="customOptimizationSuggestion"
+            placeholder="请输入您的其他建议和意见...">
+          </textarea>
+        </div>
+      </div>
+
+      <!-- 操作按钮 -->
+      <div class="form-actions">
+        <button class="btn btn-primary" (click)="saveDetailedReview()">
+          保存评价
+        </button>
+        <button class="btn btn-secondary" (click)="cancelDetailedReview()">
+          取消
+        </button>
+      </div>
+    </div>
+
+    <!-- 创建详细评价按钮 -->
+    <div class="create-review-section" *ngIf="!showDetailedForm">
+      <button class="btn btn-primary" (click)="showDetailedForm = true">
+        <i class="icon-plus"></i>
+        创建详细评价
+      </button>
+    </div>
+
+    <!-- 详细评价列表 -->
+    <div class="detailed-reviews-list" *ngIf="detailedReviews && detailedReviews.length > 0">
+      <div class="list-header">
+        <h5>详细评价记录</h5>
+        <span class="review-count">{{ detailedReviews.length }} 条记录</span>
+      </div>
+
+      <div class="review-items">
+        <div class="review-item" *ngFor="let review of detailedReviews">
+          <div class="review-header">
+            <div class="customer-info">
+              <span class="customer-name">{{ review.customerName }}</span>
+              <span class="review-date">{{ review.submittedAt | date:'yyyy-MM-dd' }}</span>
+            </div>
+            <div class="overall-rating">
+              <span class="rating-label">总体评分:</span>
+              <div class="stars">
+                <span *ngFor="let i of getDimensionStars(review.dimensions.overall)">★</span>
+              </div>
+              <span class="rating-number">{{ review.dimensions.overall }}/5</span>
+            </div>
+          </div>
+
+          <div class="review-dimensions">
+            <div class="dimension-summary" *ngFor="let dim of reviewDimensions">
+              <span class="dim-label">{{ dim.label }}:</span>
+              <div class="dim-rating">
+                <div class="stars mini">
+                  <span *ngFor="let i of getDimensionStars(review.dimensions[dim.key] || 0)">★</span>
+                </div>
+                <span class="dim-score" [class]="getDimensionScoreClass(review.dimensions[dim.key] || 0)">
+                  {{ review.dimensions[dim.key] || 0 }}
+                </span>
+              </div>
+            </div>
+          </div>
+
+          <div class="scene-reviews" *ngIf="review.sceneReviews && review.sceneReviews.length > 0">
+            <h6>场景评价</h6>
+            <div class="scene-summary" *ngFor="let scene of review.sceneReviews">
+              <div class="scene-info">
+                <span class="scene-name">{{ getSceneTypeLabel(scene.sceneType) }}</span>
+                <div class="scene-rating">
+                  <div class="stars mini">
+                    <span *ngFor="let i of getDimensionStars(scene.rating)">★</span>
+                  </div>
+                  <span class="scene-score">{{ scene.rating }}/5</span>
+                </div>
+              </div>
+              <div class="scene-feedback" *ngIf="scene.feedback">
+                {{ scene.feedback }}
+              </div>
+            </div>
+          </div>
+
+          <div class="optimization-suggestions" *ngIf="review.optimizationSuggestions">
+            <h6>优化建议</h6>
+            <div class="suggestions-list">
+              <span class="suggestion-tag" *ngFor="let suggestion of review.optimizationSuggestions">
+                {{ suggestion }}
+              </span>
+            </div>
+            <div class="custom-suggestion" *ngIf="review.improvementSuggestions">
+              <strong>其他建议:</strong>{{ review.improvementSuggestions }}
+            </div>
+          </div>
+
+          <div class="review-actions">
+            <button class="btn btn-sm btn-primary" (click)="viewDetailedReview(review)">
+              <i class="fas fa-eye"></i>
+              查看详情
+            </button>
+            <button class="btn btn-sm btn-secondary" (click)="editDetailedReview(review.id)">
+              <i class="fas fa-edit"></i>
+              编辑
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
   <!-- 筛选区域 -->
   <div class="filter-section">
     <div class="filter-row">

+ 329 - 0
src/app/shared/components/customer-review-card/customer-review-card.scss

@@ -324,4 +324,333 @@
       }
     }
   }
+
+  // 详细评价表单样式
+  .detailed-form {
+    background: #f8f9fa;
+    border-radius: 8px;
+    padding: 24px;
+    margin-bottom: 20px;
+    border: 1px solid #e9ecef;
+
+    h4 {
+      color: #2c3e50;
+      margin-bottom: 16px;
+      font-size: 16px;
+      font-weight: 600;
+    }
+  }
+
+  // 多维度评价表单
+  .dimension-form {
+    margin-bottom: 24px;
+
+    .dimension-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+      gap: 16px;
+    }
+
+    .dimension-item {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 12px;
+      background: white;
+      border-radius: 6px;
+      border: 1px solid #dee2e6;
+
+      label {
+        font-weight: 500;
+        color: #495057;
+        min-width: 100px;
+      }
+
+      .star-rating {
+        display: flex;
+        gap: 2px;
+        margin: 0 12px;
+
+        .star {
+          font-size: 18px;
+          color: #ddd;
+          cursor: pointer;
+          transition: color 0.2s;
+
+          &:hover,
+          &.active {
+            color: #ffc107;
+          }
+        }
+      }
+
+      .rating-text {
+        font-size: 14px;
+        color: #6c757d;
+        min-width: 40px;
+        text-align: center;
+      }
+    }
+  }
+
+  // 场景评价
+  .scene-evaluation {
+    margin-bottom: 24px;
+
+    .scene-grid {
+      display: grid;
+      gap: 16px;
+    }
+
+    .scene-item {
+      background: white;
+      border-radius: 6px;
+      border: 1px solid #dee2e6;
+      overflow: hidden;
+
+      .scene-header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 12px 16px;
+        background: #f8f9fa;
+        border-bottom: 1px solid #dee2e6;
+
+        .scene-name {
+          font-weight: 500;
+          color: #495057;
+        }
+
+        .star-rating {
+          display: flex;
+          gap: 2px;
+          margin: 0 12px;
+
+          .star {
+            font-size: 16px;
+            color: #ddd;
+            cursor: pointer;
+            transition: color 0.2s;
+
+            &:hover,
+            &.active {
+              color: #ffc107;
+            }
+          }
+        }
+
+        .rating-text {
+          font-size: 14px;
+          color: #6c757d;
+          min-width: 40px;
+          text-align: center;
+        }
+      }
+
+      .scene-feedback {
+        width: 100%;
+        min-height: 80px;
+        padding: 12px 16px;
+        border: none;
+        resize: vertical;
+        font-size: 14px;
+        line-height: 1.5;
+
+        &:focus {
+          outline: none;
+          background: #f8f9fa;
+        }
+
+        &::placeholder {
+          color: #adb5bd;
+        }
+      }
+    }
+  }
+
+  // 优化建议
+  .optimization-suggestions {
+    margin-bottom: 24px;
+
+    .suggestion-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+      gap: 12px;
+      margin-bottom: 16px;
+    }
+
+    .suggestion-item {
+      display: flex;
+      align-items: flex-start;
+      padding: 12px;
+      background: white;
+      border-radius: 6px;
+      border: 1px solid #dee2e6;
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &:hover {
+        border-color: #007bff;
+        background: #f8f9ff;
+      }
+
+      input[type="checkbox"] {
+        margin-right: 12px;
+        margin-top: 2px;
+      }
+
+      .checkmark {
+        width: 16px;
+        height: 16px;
+        border: 2px solid #dee2e6;
+        border-radius: 3px;
+        margin-right: 12px;
+        margin-top: 2px;
+        position: relative;
+        transition: all 0.2s;
+
+        &::after {
+          content: '';
+          position: absolute;
+          left: 4px;
+          top: 1px;
+          width: 4px;
+          height: 8px;
+          border: solid white;
+          border-width: 0 2px 2px 0;
+          transform: rotate(45deg);
+          opacity: 0;
+          transition: opacity 0.2s;
+        }
+      }
+
+      input[type="checkbox"]:checked + .checkmark {
+        background: #007bff;
+        border-color: #007bff;
+
+        &::after {
+          opacity: 1;
+        }
+      }
+
+      .suggestion-content {
+        flex: 1;
+
+        .suggestion-label {
+          display: block;
+          font-weight: 500;
+          color: #495057;
+          margin-bottom: 4px;
+        }
+
+        .suggestion-desc {
+          display: block;
+          font-size: 12px;
+          color: #6c757d;
+          line-height: 1.4;
+        }
+      }
+    }
+
+    .custom-suggestion {
+      label {
+        display: block;
+        font-weight: 500;
+        color: #495057;
+        margin-bottom: 8px;
+      }
+
+      textarea {
+        width: 100%;
+        min-height: 80px;
+        padding: 12px;
+        border: 1px solid #dee2e6;
+        border-radius: 6px;
+        resize: vertical;
+        font-size: 14px;
+        line-height: 1.5;
+
+        &:focus {
+          outline: none;
+          border-color: #007bff;
+          box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+        }
+
+        &::placeholder {
+          color: #adb5bd;
+        }
+      }
+    }
+  }
+
+  // 表单操作按钮
+  .form-actions {
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+    padding-top: 16px;
+    border-top: 1px solid #dee2e6;
+
+    .btn {
+      padding: 8px 20px;
+      border-radius: 6px;
+      font-size: 14px;
+      font-weight: 500;
+      border: none;
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &.btn-primary {
+        background: #007bff;
+        color: white;
+
+        &:hover {
+          background: #0056b3;
+        }
+      }
+
+      &.btn-secondary {
+        background: #6c757d;
+        color: white;
+
+        &:hover {
+          background: #545b62;
+        }
+      }
+    }
+  }
+
+  // 创建评价按钮区域
+  .create-review-section {
+    text-align: center;
+    padding: 40px 20px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 2px dashed #dee2e6;
+    margin-bottom: 20px;
+
+    .btn {
+      padding: 12px 24px;
+      font-size: 16px;
+      font-weight: 500;
+      border-radius: 8px;
+      border: none;
+      cursor: pointer;
+      transition: all 0.2s;
+
+      &.btn-primary {
+        background: #007bff;
+        color: white;
+
+        &:hover {
+          background: #0056b3;
+          transform: translateY(-1px);
+        }
+      }
+
+      i {
+        margin-right: 8px;
+      }
+    }
+  }
 }

+ 276 - 7
src/app/shared/components/customer-review-card/customer-review-card.ts

@@ -3,6 +3,47 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { CustomerFeedback } from '../../../models/project.model';
 
+export interface DetailedReviewDimensions {
+  [key: string]: number; // 添加索引签名
+  overall: number; // 总体评价 0-5
+  serviceTimeliness: number; // 服务时效 0-5
+  suggestionResponse: number; // 建议回应 0-5
+  smallImageDelivery: number; // 小图交付 0-5
+  imageRevisionTimeliness: number; // 改图及时性 0-5
+  renderingQuality: number; // 图纸渲染 0-5
+  materialQuality: number; // 材质 0-5
+  lightingQuality: number; // 灯光 0-5
+  textureQuality: number; // 纹理 0-5
+  structureQuality: number; // 硬装结构 0-5
+  dataCompleteness: number; // 资料齐全情况 0-5
+  requirementUnderstanding: number; // 需求理解 0-5
+  communicationEffectiveness: number; // 沟通效果 0-5
+  sceneDesignQuality: number; // 场景设计质量 0-5
+}
+
+export interface SceneReview {
+  sceneType: 'bedroom' | 'dining_room' | 'living_room' | 'other';
+  sceneName: string;
+  designerId: string;
+  designerName: string;
+  rating: number; // 0-5
+  feedback: string;
+}
+
+export interface DetailedCustomerReview {
+  id: string;
+  projectId: string;
+  customerId: string;
+  customerName: string;
+  dimensions: DetailedReviewDimensions;
+  sceneReviews: SceneReview[];
+  improvementSuggestions: string; // 下次合作希望优化的点
+  optimizationSuggestions?: string[]; // 优化建议列表
+  overallFeedback: string;
+  submittedAt: Date;
+  status: 'pending' | 'completed';
+}
+
 export interface ReviewStats {
   totalCount: number;
   averageScore: number;
@@ -11,6 +52,7 @@ export interface ReviewStats {
   pendingCount: number;
   processedCount: number;
   categoryStats: { [key: string]: number };
+  dimensionAverages: Partial<DetailedReviewDimensions>;
 }
 
 @Component({
@@ -22,11 +64,39 @@ export interface ReviewStats {
 })
 export class CustomerReviewCardComponent {
   @Input() feedbacks: CustomerFeedback[] = [];
+  @Input() detailedReviews: DetailedCustomerReview[] = [];
   
   // 筛选条件
   statusFilter = signal<string>('all');
   categoryFilter = signal<string>('all');
   scoreFilter = signal<string>('all');
+  viewMode = signal<'simple' | 'detailed'>('detailed');
+  
+  // 详细评价维度配置
+  reviewDimensions = [
+    { key: 'overall', label: '总体评价', description: '对整体服务的满意度' },
+    { key: 'serviceTimeliness', label: '服务时效', description: '响应速度和交付及时性' },
+    { key: 'suggestionResponse', label: '建议回应', description: '对客户建议的响应程度' },
+    { key: 'smallImageDelivery', label: '小图交付', description: '小图质量和交付效率' },
+    { key: 'imageRevisionTimeliness', label: '改图及时性', description: '修改图纸的响应速度' },
+    { key: 'renderingQuality', label: '图纸渲染', description: '渲染效果和技术质量' },
+    { key: 'materialQuality', label: '材质', description: '材质选择和表现效果' },
+    { key: 'lightingQuality', label: '灯光', description: '灯光设计和氛围营造' },
+    { key: 'textureQuality', label: '纹理', description: '纹理细节和真实感' },
+    { key: 'structureQuality', label: '硬装结构', description: '结构设计的合理性' },
+    { key: 'dataCompleteness', label: '资料齐全情况', description: '提供资料的完整性' },
+    { key: 'requirementUnderstanding', label: '需求理解', description: '对客户需求的理解准确度' },
+    { key: 'communicationEffectiveness', label: '沟通效果', description: '沟通的有效性和清晰度' },
+    { key: 'sceneDesignQuality', label: '场景设计质量', description: '各场景设计的整体质量' }
+  ];
+  
+  // 场景类型配置
+  sceneTypes = [
+    { value: 'bedroom', label: '卧室' },
+    { value: 'dining_room', label: '餐厅' },
+    { value: 'living_room', label: '客厅' },
+    { value: 'other', label: '其他' }
+  ];
   
   // 评价分类
   categories = [
@@ -41,6 +111,7 @@ export class CustomerReviewCardComponent {
   // 计算统计数据
   stats = computed<ReviewStats>(() => {
     const feedbacks = this.feedbacks || [];
+    const detailedReviews = this.detailedReviews || [];
     
     const categoryStats: { [key: string]: number } = {};
     this.categories.forEach(cat => {
@@ -50,15 +121,27 @@ export class CustomerReviewCardComponent {
     const scores = feedbacks.filter(f => f.rating !== undefined).map(f => f.rating || 0);
     const averageScore = scores.length > 0 ? scores.reduce((sum, score) => sum + score, 0) / scores.length : 0;
     
+    // 计算详细评价维度的平均分
+    const dimensionAverages: Partial<DetailedReviewDimensions> = {};
+    if (detailedReviews.length > 0) {
+      this.reviewDimensions.forEach(dimension => {
+        const key = dimension.key as keyof DetailedReviewDimensions;
+        const values = detailedReviews.map(review => review.dimensions[key]).filter(v => v !== undefined);
+        if (values.length > 0) {
+          dimensionAverages[key] = values.reduce((sum, val) => sum + val, 0) / values.length;
+        }
+      });
+    }
+    
     return {
-      totalCount: feedbacks.length,
+      totalCount: feedbacks.length + detailedReviews.length,
       averageScore: Math.round(averageScore * 10) / 10,
-      satisfiedCount: feedbacks.filter(f => f.isSatisfied).length,
-      unsatisfiedCount: feedbacks.filter(f => !f.isSatisfied).length,
-      pendingCount: feedbacks.filter(f => f.status === '待处理').length,
-      processingCount: feedbacks.filter(f => f.status === '处理中').length,
-      processedCount: feedbacks.filter(f => f.status === '已解决').length,
-      categoryStats
+      satisfiedCount: feedbacks.filter(f => f.isSatisfied).length + detailedReviews.filter(r => r.dimensions.overall >= 4).length,
+      unsatisfiedCount: feedbacks.filter(f => !f.isSatisfied).length + detailedReviews.filter(r => r.dimensions.overall < 3).length,
+      pendingCount: feedbacks.filter(f => f.status === '待处理').length + detailedReviews.filter(r => r.status === 'pending').length,
+      processedCount: feedbacks.filter(f => f.status === '已解决').length + detailedReviews.filter(r => r.status === 'completed').length,
+      categoryStats,
+      dimensionAverages
     };
   });
   
@@ -212,4 +295,190 @@ export class CustomerReviewCardComponent {
     // 这里可以打开详情页面或模态框
     alert(`客户评价详情:\n\n客户: ${feedback.customerName}\n评分: ${feedback.rating}/5\n内容: ${feedback.content}\n回复: ${feedback.response || '暂无回复'}`);
   }
+
+  // 新增方法:获取维度评分的星级显示
+  getDimensionStars(score: number): string[] {
+    const stars = [];
+    const fullStars = Math.floor(score);
+    const hasHalfStar = score % 1 >= 0.5;
+    
+    for (let i = 0; i < 5; i++) {
+      if (i < fullStars) {
+        stars.push('★');
+      } else if (i === fullStars && hasHalfStar) {
+        stars.push('☆');
+      } else {
+        stars.push('☆');
+      }
+    }
+    return stars;
+  }
+
+  // 获取维度评分的颜色类
+  getDimensionScoreClass(score: number): string {
+    if (score >= 4.5) return 'score-excellent';
+    if (score >= 3.5) return 'score-good';
+    if (score >= 2.5) return 'score-average';
+    return 'score-poor';
+  }
+
+  // 获取场景类型标签
+  getSceneTypeLabel(sceneType: string): string {
+    const scene = this.sceneTypes.find(s => s.value === sceneType);
+    return scene ? scene.label : sceneType;
+  }
+
+  // 切换视图模式
+  toggleViewMode(): void {
+    this.viewMode.set(this.viewMode() === 'simple' ? 'detailed' : 'simple');
+  }
+
+  // 创建新的详细评价
+  createDetailedReview(projectId: string): void {
+    console.log('创建详细评价:', projectId);
+    // 这里可以打开评价表单模态框
+  }
+
+  // 查看详细评价
+  viewDetailedReview(review: DetailedCustomerReview): void {
+    console.log('查看详细评价:', review);
+    // 这里可以打开详细评价查看模态框
+  }
+
+  // 详细评价表单状态
+  showDetailedForm = false;
+  currentRatings: { [key: string]: number } = {};
+  currentSceneRatings: { [key: string]: number } = {};
+  currentSceneFeedback: { [key: string]: string } = {};
+  selectedOptimizations: { [key: string]: boolean } = {};
+  customOptimizationSuggestion = '';
+
+  // 优化建议类别
+  optimizationCategories = [
+    { key: 'service_speed', label: '服务时效', description: '提高响应速度和交付效率' },
+    { key: 'communication', label: '沟通效果', description: '加强沟通频率和质量' },
+    { key: 'design_quality', label: '设计质量', description: '提升设计水平和创意' },
+    { key: 'material_quality', label: '材质表现', description: '优化材质和纹理效果' },
+    { key: 'lighting_effect', label: '灯光效果', description: '改善灯光设置和氛围' },
+    { key: 'detail_accuracy', label: '细节准确性', description: '提高细节还原度' },
+    { key: 'revision_efficiency', label: '修改效率', description: '加快修改响应速度' },
+    { key: 'requirement_understanding', label: '需求理解', description: '更准确理解客户需求' }
+  ];
+
+  // 设置维度评分
+  setDimensionRating(dimensionKey: string, rating: number): void {
+    this.currentRatings[dimensionKey] = rating;
+  }
+
+  // 设置场景评分
+  setSceneRating(sceneKey: string, rating: number): void {
+    this.currentSceneRatings[sceneKey] = rating;
+  }
+
+  // 保存详细评价
+  saveDetailedReview(): void {
+    const newReview: DetailedCustomerReview = {
+      id: Date.now().toString(),
+      customerName: 'Customer Name',
+      projectId: 'proj_001',
+      customerId: 'customer-' + Date.now(),
+      overallFeedback: '',
+      submittedAt: new Date(),
+      status: 'completed' as 'pending' | 'completed',
+      dimensions: { ...this.currentRatings } as unknown as DetailedReviewDimensions,
+      sceneReviews: this.buildSceneReviews(),
+      optimizationSuggestions: this.getSelectedOptimizations(),
+      improvementSuggestions: this.customOptimizationSuggestion
+    };
+
+    // 添加到详细评价列表
+    if (!this.detailedReviews) {
+      this.detailedReviews = [];
+    }
+    this.detailedReviews.push(newReview);
+
+    console.log('保存详细评价:', newReview);
+    
+    // 重置表单
+    this.resetDetailedForm();
+    this.showDetailedForm = false;
+  }
+
+  // 计算总体评分
+  calculateOverallRating(): number {
+    const ratings = Object.values(this.currentRatings);
+    if (ratings.length === 0) return 0;
+    
+    const sum = ratings.reduce((acc, rating) => acc + rating, 0);
+    return Math.round(sum / ratings.length);
+  }
+
+  // 构建场景评价
+  buildSceneReviews(): SceneReview[] {
+    return this.sceneTypes
+      .filter(scene => this.currentSceneRatings[scene.value] > 0)
+      .map(scene => ({
+        sceneType: scene.value as 'bedroom' | 'dining_room' | 'living_room' | 'other',
+        sceneName: scene.label,
+        designerId: 'current-designer', // 这里应该从项目数据中获取
+        designerName: '当前设计师', // 这里应该从项目数据中获取
+        rating: this.currentSceneRatings[scene.value],
+        feedback: this.currentSceneFeedback[scene.value] || ''
+      }));
+  }
+
+  // 获取选中的优化建议
+  getSelectedOptimizations(): string[] {
+    return this.optimizationCategories
+      .filter(category => this.selectedOptimizations[category.key])
+      .map(category => category.label);
+  }
+
+  // 重置详细评价表单
+  resetDetailedForm(): void {
+    this.currentRatings = {};
+    this.currentSceneRatings = {};
+    this.currentSceneFeedback = {};
+    this.selectedOptimizations = {};
+    this.customOptimizationSuggestion = '';
+  }
+
+  // 取消详细评价
+  cancelDetailedReview(): void {
+    this.resetDetailedForm();
+    this.showDetailedForm = false;
+  }
+
+  // 编辑详细评价
+  editDetailedReview(reviewId: string): void {
+    const review = this.detailedReviews?.find(r => r.id === reviewId);
+    if (review) {
+      // 填充维度评分
+      this.currentRatings = { ...review.dimensions };
+      
+      // 填充场景评价数据
+      review.sceneReviews?.forEach(scene => {
+        this.currentSceneRatings[scene.sceneType] = scene.rating;
+        this.currentSceneFeedback[scene.sceneType] = scene.feedback || '';
+      });
+
+      // 填充优化建议
+      this.selectedOptimizations = {};
+      review.optimizationSuggestions?.forEach((suggestion: string) => {
+        const category = this.optimizationCategories.find(cat => cat.label === suggestion);
+        if (category) {
+          this.selectedOptimizations[category.key] = true;
+        }
+      });
+
+      this.customOptimizationSuggestion = review.improvementSuggestions || '';
+      this.showDetailedForm = true;
+    }
+  }
+
+  // 导出评价报告
+  exportReviewReport(): void {
+    console.log('导出评价报告');
+    // 这里可以实现导出功能
+  }
 }

+ 396 - 0
src/app/shared/components/customer-review-form/customer-review-form.html

@@ -0,0 +1,396 @@
+<div class="customer-review-form">
+  <div class="form-header">
+    <h3>客户服务评价</h3>
+    <p class="project-info">项目:{{ projectName }} | 设计师:{{ designerName }}</p>
+    <div class="overall-rating">
+      <span class="rating-label">总体评分:</span>
+      <span class="average-score">{{ getAverageScore() }}</span>
+      <span class="score-suffix">/5</span>
+      <div class="star-display">
+        @for (star of generateStars(getAverageScore()); track $index) {
+          <span class="star">{{ star }}</span>
+        }
+      </div>
+    </div>
+  </div>
+
+  <div class="form-content">
+    <!-- 总体满意度 -->
+    <div class="rating-section">
+      <h4>1. 总体满意度</h4>
+      <p class="section-description">您对本次服务的整体满意程度</p>
+      <div class="rating-control">
+        @for (i of [1,2,3,4,5]; track i) {
+          <label class="star-option">
+            <input 
+              type="radio" 
+              name="overallSatisfaction" 
+              [value]="i" 
+              [(ngModel)]="reviewData.overallSatisfaction">
+            <span class="star">{{ i <= reviewData.overallSatisfaction ? '★' : '☆' }}</span>
+            <span class="score-label">{{ i }}分</span>
+          </label>
+        }
+      </div>
+    </div>
+
+    <!-- 服务质量维度 -->
+    <div class="rating-section">
+      <h4>2. 服务质量</h4>
+      <div class="dimensions-grid">
+        <div class="dimension-item">
+          <label>服务时效</label>
+          <p class="dimension-desc">响应速度和交付及时性</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="serviceTimeliness" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.serviceTimeliness">
+                <span class="star">{{ i <= reviewData.serviceTimeliness ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>建议回应</label>
+          <p class="dimension-desc">对您建议的采纳和回应</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="suggestionResponse" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.suggestionResponse">
+                <span class="star">{{ i <= reviewData.suggestionResponse ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>小图交付</label>
+          <p class="dimension-desc">预览图的质量和效果</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="smallImageDelivery" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.smallImageDelivery">
+                <span class="star">{{ i <= reviewData.smallImageDelivery ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>改图及时性</label>
+          <p class="dimension-desc">修改请求的处理速度</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="revisionEfficiency" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.revisionEfficiency">
+                <span class="star">{{ i <= reviewData.revisionEfficiency ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 渲染质量维度 -->
+    <div class="rating-section">
+      <h4>3. 渲染质量</h4>
+      <div class="dimensions-grid">
+        <div class="dimension-item">
+          <label>材质表现</label>
+          <p class="dimension-desc">材料质感和真实度</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="materials" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.renderingQuality.materials">
+                <span class="star">{{ i <= reviewData.renderingQuality.materials ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>灯光效果</label>
+          <p class="dimension-desc">光照氛围和自然度</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="lighting" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.renderingQuality.lighting">
+                <span class="star">{{ i <= reviewData.renderingQuality.lighting ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>纹理细节</label>
+          <p class="dimension-desc">表面纹理和细节处理</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="texture" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.renderingQuality.texture">
+                <span class="star">{{ i <= reviewData.renderingQuality.texture ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>硬装结构</label>
+          <p class="dimension-desc">空间结构和比例准确性</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="structure" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.renderingQuality.structure">
+                <span class="star">{{ i <= reviewData.renderingQuality.structure ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 沟通体验维度 -->
+    <div class="rating-section">
+      <h4>4. 沟通体验</h4>
+      <div class="dimensions-grid">
+        <div class="dimension-item">
+          <label>资料齐全性</label>
+          <p class="dimension-desc">提供资料的完整程度</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="documentation" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.communication.documentation">
+                <span class="star">{{ i <= reviewData.communication.documentation ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>需求理解</label>
+          <p class="dimension-desc">对您需求的理解准确性</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="requirementUnderstanding" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.communication.requirementUnderstanding">
+                <span class="star">{{ i <= reviewData.communication.requirementUnderstanding ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>沟通充分性</label>
+          <p class="dimension-desc">沟通频率和信息透明度</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="communicationSufficiency" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.communication.communicationSufficiency">
+                <span class="star">{{ i <= reviewData.communication.communicationSufficiency ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+
+        <div class="dimension-item">
+          <label>内容相关性</label>
+          <p class="dimension-desc">交付内容与需求的匹配度</p>
+          <div class="rating-control">
+            @for (i of [1,2,3,4,5]; track i) {
+              <label class="star-option">
+                <input 
+                  type="radio" 
+                  name="contentRelevance" 
+                  [value]="i" 
+                  [(ngModel)]="reviewData.communication.contentRelevance">
+                <span class="star">{{ i <= reviewData.communication.contentRelevance ? '★' : '☆' }}</span>
+              </label>
+            }
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 场景评价 -->
+    <div class="rating-section">
+      <h4>5. 各场景评价</h4>
+      <p class="section-description">请对每个设计场景进行评分</p>
+      <div class="scenes-grid">
+        @for (scene of reviewData.scenes; track $index) {
+          <div class="scene-item">
+            <label class="scene-name">{{ scene.sceneName }}</label>
+            <div class="rating-control">
+              @for (i of [1,2,3,4,5]; track i) {
+                <label class="star-option">
+                  <input 
+                    type="radio" 
+                    [name]="'scene-' + $index" 
+                    [value]="i" 
+                    [(ngModel)]="scene.score">
+                  <span class="star">{{ i <= scene.score ? '★' : '☆' }}</span>
+                </label>
+              }
+            </div>
+            <textarea 
+              class="scene-comments" 
+              placeholder="对该场景的具体意见(选填)"
+              [(ngModel)]="scene.comments"></textarea>
+          </div>
+        }
+      </div>
+    </div>
+
+    <!-- 改进建议 -->
+    <div class="suggestion-section">
+      <h4>6. 改进建议</h4>
+      <p class="section-description">您希望我们在哪些方面进行优化?</p>
+      <textarea 
+        class="suggestion-input" 
+        placeholder="请分享您的宝贵建议,帮助我们做得更好"
+        [(ngModel)]="reviewData.improvementSuggestions"></textarea>
+    </div>
+
+    <!-- 下次合作优化点 -->
+    <div class="optimization-section">
+      <h4>7. 下次合作优化点</h4>
+      <p class="section-description">下次合作,您希望我们优化哪些点?</p>
+      <div class="optimization-options">
+        <div class="option-grid">
+          <label class="option-item">
+            <input type="checkbox" value="服务响应速度">
+            <span>服务响应速度</span>
+          </label>
+          <label class="option-item">
+            <input type="checkbox" value="设计创意水平">
+            <span>设计创意水平</span>
+          </label>
+          <label class="option-item">
+            <input type="checkbox" value="沟通协调能力">
+            <span>沟通协调能力</span>
+          </label>
+          <label class="option-item">
+            <input type="checkbox" value="项目管理流程">
+            <span>项目管理流程</span>
+          </label>
+          <label class="option-item">
+            <input type="checkbox" value="技术实现质量">
+            <span>技术实现质量</span>
+          </label>
+          <label class="option-item">
+            <input type="checkbox" value="售后服务质量">
+            <span>售后服务质量</span>
+          </label>
+        </div>
+        <textarea 
+          class="optimization-input" 
+          placeholder="其他具体的优化建议..."
+          [(ngModel)]="reviewData.nextCooperationOptimization"></textarea>
+      </div>
+    </div>
+
+    <!-- 合作意愿 -->
+    <div class="cooperation-section">
+      <h4>8. 合作意愿</h4>
+      <div class="cooperation-options">
+        <label class="cooperation-option">
+          <input 
+            type="radio" 
+            name="willCooperateAgain" 
+            [value]="true" 
+            [(ngModel)]="reviewData.willCooperateAgain">
+          <span class="option-label">愿意再次合作</span>
+        </label>
+        <label class="cooperation-option">
+          <input 
+            type="radio" 
+            name="willCooperateAgain" 
+            [value]="false" 
+            [(ngModel)]="reviewData.willCooperateAgain">
+          <span class="option-label">暂不考虑再次合作</span>
+        </label>
+      </div>
+    </div>
+
+    <!-- 投诉内容 -->
+    <div class="complaint-section">
+      <div class="complaint-toggle">
+        <label class="complaint-switch">
+          <input 
+            type="checkbox" 
+            [(ngModel)]="showComplaint">
+          <span class="switch-label">我要投诉</span>
+        </label>
+      </div>
+
+      @if (showComplaint) {
+        <div class="complaint-content">
+          <h5>投诉内容</h5>
+          <p class="complaint-notice">⚠️ 投诉内容将作为实时代办项进行处理</p>
+          <textarea 
+            class="complaint-input" 
+            placeholder="请详细描述您遇到的问题和不满意的方面"
+            [(ngModel)]="reviewData.complaintContent"></textarea>
+        </div>
+      }
+    </div>
+
+    <!-- 提交按钮 -->
+    <div class="form-actions">
+      <button 
+        class="submit-btn primary" 
+        (click)="submitReview()">
+        提交评价
+      </button>
+      <button 
+        class="submit-btn secondary" 
+        type="button">
+        暂存草稿
+      </button>
+    </div>
+  </div>
+</div>

+ 469 - 0
src/app/shared/components/customer-review-form/customer-review-form.scss

@@ -0,0 +1,469 @@
+.customer-review-form {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 24px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+
+  .form-header {
+    text-align: center;
+    margin-bottom: 32px;
+    padding-bottom: 20px;
+    border-bottom: 2px solid #f0f0f0;
+
+    h3 {
+      color: #333;
+      font-size: 24px;
+      font-weight: 600;
+      margin-bottom: 8px;
+    }
+
+    .project-info {
+      color: #666;
+      font-size: 14px;
+      margin-bottom: 16px;
+    }
+
+    .overall-rating {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 8px;
+      font-size: 16px;
+
+      .rating-label {
+        color: #333;
+        font-weight: 500;
+      }
+
+      .average-score {
+        color: #ff6b35;
+        font-size: 20px;
+        font-weight: 600;
+      }
+
+      .score-suffix {
+        color: #999;
+      }
+
+      .star-display {
+        display: flex;
+        gap: 2px;
+
+        .star {
+          color: #ffc107;
+          font-size: 18px;
+        }
+      }
+    }
+  }
+
+  .form-content {
+    .rating-section {
+      margin-bottom: 32px;
+      padding: 20px;
+      background: #fafafa;
+      border-radius: 8px;
+      border-left: 4px solid #ff6b35;
+
+      h4 {
+        color: #333;
+        font-size: 18px;
+        font-weight: 600;
+        margin-bottom: 8px;
+      }
+
+      .section-description {
+        color: #666;
+        font-size: 14px;
+        margin-bottom: 16px;
+      }
+
+      .rating-control {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+
+        .star-option {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          cursor: pointer;
+          padding: 8px;
+          border-radius: 6px;
+          transition: background-color 0.2s;
+
+          &:hover {
+            background-color: #fff3e0;
+          }
+
+          input[type="radio"] {
+            display: none;
+          }
+
+          .star {
+            font-size: 24px;
+            color: #ddd;
+            transition: color 0.2s;
+            margin-bottom: 4px;
+
+            &.filled {
+              color: #ffc107;
+            }
+          }
+
+          .score-label {
+            font-size: 12px;
+            color: #666;
+          }
+
+          input:checked + .star {
+            color: #ffc107;
+          }
+        }
+      }
+
+      .dimensions-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+        gap: 20px;
+
+        .dimension-item {
+          background: #fff;
+          padding: 16px;
+          border-radius: 8px;
+          border: 1px solid #e0e0e0;
+
+          label {
+            display: block;
+            color: #333;
+            font-weight: 500;
+            margin-bottom: 4px;
+          }
+
+          .dimension-desc {
+            color: #666;
+            font-size: 12px;
+            margin-bottom: 12px;
+          }
+
+          .rating-control {
+            justify-content: flex-start;
+
+            .star-option {
+              padding: 4px;
+
+              .star {
+                font-size: 20px;
+              }
+
+              .score-label {
+                display: none;
+              }
+            }
+          }
+        }
+      }
+
+      .scenes-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+        gap: 16px;
+
+        .scene-item {
+          background: #fff;
+          padding: 16px;
+          border-radius: 8px;
+          border: 1px solid #e0e0e0;
+
+          .scene-name {
+            display: block;
+            color: #333;
+            font-weight: 500;
+            margin-bottom: 12px;
+          }
+
+          .scene-comments {
+            width: 100%;
+            min-height: 60px;
+            padding: 8px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            font-size: 14px;
+            margin-top: 12px;
+            resize: vertical;
+
+            &:focus {
+              outline: none;
+              border-color: #ff6b35;
+            }
+          }
+        }
+      }
+    }
+
+    .suggestion-section,
+    .optimization-section {
+      margin-bottom: 24px;
+      padding: 20px;
+      background: #f8f9fa;
+      border-radius: 8px;
+
+      h4 {
+        color: #333;
+        font-size: 18px;
+        font-weight: 600;
+        margin-bottom: 8px;
+      }
+
+      .section-description {
+        color: #666;
+        font-size: 14px;
+        margin-bottom: 16px;
+      }
+
+      .suggestion-input,
+      .optimization-input {
+        width: 100%;
+        min-height: 100px;
+        padding: 12px;
+        border: 1px solid #ddd;
+        border-radius: 6px;
+        font-size: 14px;
+        resize: vertical;
+
+        &:focus {
+          outline: none;
+          border-color: #ff6b35;
+        }
+      }
+
+      .optimization-options {
+        .option-grid {
+          display: grid;
+          grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+          gap: 12px;
+          margin-bottom: 16px;
+
+          .option-item {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            padding: 8px 12px;
+            background: #fff;
+            border: 1px solid #e0e0e0;
+            border-radius: 6px;
+            cursor: pointer;
+            transition: all 0.2s;
+
+            &:hover {
+              border-color: #ff6b35;
+              background-color: #fff3e0;
+            }
+
+            input[type="checkbox"] {
+              margin: 0;
+            }
+
+            span {
+              font-size: 14px;
+              color: #333;
+            }
+          }
+        }
+      }
+    }
+
+    .cooperation-section {
+      margin-bottom: 24px;
+      padding: 20px;
+      background: #e8f5e8;
+      border-radius: 8px;
+
+      h4 {
+        color: #333;
+        font-size: 18px;
+        font-weight: 600;
+        margin-bottom: 16px;
+      }
+
+      .cooperation-options {
+        display: flex;
+        gap: 20px;
+
+        .cooperation-option {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          cursor: pointer;
+          padding: 12px 16px;
+          background: #fff;
+          border: 2px solid #e0e0e0;
+          border-radius: 8px;
+          transition: all 0.2s;
+
+          &:hover {
+            border-color: #4caf50;
+          }
+
+          input[type="radio"] {
+            margin: 0;
+          }
+
+          .option-label {
+            font-size: 14px;
+            color: #333;
+            font-weight: 500;
+          }
+
+          input:checked + .option-label {
+            color: #4caf50;
+          }
+        }
+      }
+    }
+
+    .complaint-section {
+      margin-bottom: 32px;
+      padding: 20px;
+      background: #fff3e0;
+      border-radius: 8px;
+      border-left: 4px solid #ff9800;
+
+      .complaint-toggle {
+        margin-bottom: 16px;
+
+        .complaint-switch {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          cursor: pointer;
+
+          input[type="checkbox"] {
+            margin: 0;
+          }
+
+          .switch-label {
+            font-size: 16px;
+            color: #ff9800;
+            font-weight: 500;
+          }
+        }
+      }
+
+      .complaint-content {
+        h5 {
+          color: #333;
+          font-size: 16px;
+          font-weight: 600;
+          margin-bottom: 8px;
+        }
+
+        .complaint-notice {
+          color: #ff9800;
+          font-size: 14px;
+          margin-bottom: 12px;
+          padding: 8px 12px;
+          background: #fff;
+          border-radius: 4px;
+          border-left: 3px solid #ff9800;
+        }
+
+        .complaint-input {
+          width: 100%;
+          min-height: 100px;
+          padding: 12px;
+          border: 1px solid #ddd;
+          border-radius: 6px;
+          font-size: 14px;
+          resize: vertical;
+
+          &:focus {
+            outline: none;
+            border-color: #ff9800;
+          }
+        }
+      }
+    }
+
+    .form-actions {
+      display: flex;
+      gap: 16px;
+      justify-content: center;
+      padding-top: 24px;
+      border-top: 1px solid #e0e0e0;
+
+      .submit-btn {
+        padding: 12px 32px;
+        border: none;
+        border-radius: 6px;
+        font-size: 16px;
+        font-weight: 500;
+        cursor: pointer;
+        transition: all 0.2s;
+
+        &.primary {
+          background: #ff6b35;
+          color: #fff;
+
+          &:hover {
+            background: #e55a2b;
+            transform: translateY(-1px);
+          }
+        }
+
+        &.secondary {
+          background: #f5f5f5;
+          color: #666;
+          border: 1px solid #ddd;
+
+          &:hover {
+            background: #e0e0e0;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .customer-review-form {
+    padding: 16px;
+
+    .form-content {
+      .rating-section {
+        .dimensions-grid {
+          grid-template-columns: 1fr;
+        }
+
+        .scenes-grid {
+          grid-template-columns: 1fr;
+        }
+      }
+
+      .optimization-section {
+        .optimization-options {
+          .option-grid {
+            grid-template-columns: 1fr;
+          }
+        }
+      }
+
+      .cooperation-section {
+        .cooperation-options {
+          flex-direction: column;
+        }
+      }
+
+      .form-actions {
+        flex-direction: column;
+
+        .submit-btn {
+          width: 100%;
+        }
+      }
+    }
+  }
+}

+ 227 - 0
src/app/shared/components/customer-review-form/customer-review-form.ts

@@ -0,0 +1,227 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+// 评价维度接口
+export interface ReviewDimension {
+  id: string;
+  label: string;
+  description?: string;
+  score: number;
+  subDimensions?: ReviewDimension[];
+}
+
+// 评价数据接口
+export interface CustomerReviewData {
+  // 总体评价 (0-5分)
+  overallSatisfaction: number;
+  
+  // 服务质量维度 (0-5分)
+  serviceTimeliness: number;        // 服务时效
+  suggestionResponse: number;       // 建议回应
+  smallImageDelivery: number;       // 小图交付
+  revisionEfficiency: number;       // 改图不及时
+  
+  // 图纸渲染质量 (0-5分)
+  renderingQuality: {
+    materials: number;              // 材质
+    lighting: number;               // 灯光
+    texture: number;                // 纹理
+    structure: number;              // 硬装结构
+  };
+  
+  // 沟通体验 (0-5分)
+  communication: {
+    documentation: number;          // 资料齐情况
+    requirementUnderstanding: number; // 需求误解
+    communicationSufficiency: number; // 缺少沟通
+    contentRelevance: number;       // 文不对题
+  };
+  
+  // 多个场景评价 - 对应设计师
+  scenes: SceneReview[];
+  
+  // 改进建议
+  improvementSuggestions: string;
+  
+  // 下次合作优化点
+  nextCooperationOptimization: string;
+  
+  // 合作意愿
+  willCooperateAgain: boolean;
+  
+  // 投诉内容(可选)
+  complaintContent?: string;
+  
+  // 评价时间
+  reviewDate: Date;
+}
+
+// 场景评价接口
+export interface SceneReview {
+  sceneName: string;
+  designerName: string;
+  score: number;
+  comments?: string;
+}
+
+@Component({
+  selector: 'app-customer-review-form',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './customer-review-form.html',
+  styleUrls: ['./customer-review-form.scss']
+})
+export class CustomerReviewFormComponent {
+  @Input() projectName: string = '';
+  @Input() designerName: string = '';
+  @Input() scenes: string[] = ['客厅', '餐厅', '卧室', '厨房', '卫生间', '阳台'];
+  @Output() reviewSubmitted = new EventEmitter<CustomerReviewData>();
+  
+  // 默认评价数据
+  reviewData: CustomerReviewData = {
+    overallSatisfaction: 5,
+    serviceTimeliness: 5,
+    suggestionResponse: 5,
+    smallImageDelivery: 5,
+    revisionEfficiency: 5,
+    renderingQuality: {
+      materials: 5,
+      lighting: 5,
+      texture: 5,
+      structure: 5
+    },
+    communication: {
+      documentation: 5,
+      requirementUnderstanding: 5,
+      communicationSufficiency: 5,
+      contentRelevance: 5
+    },
+    scenes: [],
+    improvementSuggestions: '',
+    nextCooperationOptimization: '',
+    willCooperateAgain: true,
+    complaintContent: '',
+    reviewDate: new Date()
+  };
+
+  // 是否显示投诉内容
+  showComplaint: boolean = false;
+
+  constructor() {
+    // 初始化场景评价
+    this.scenes.forEach(scene => {
+      this.reviewData.scenes.push({
+        sceneName: scene,
+        designerName: this.designerName,
+        score: 5,
+        comments: ''
+      });
+    });
+  }
+
+  // 提交评价
+  submitReview(): void {
+    // 验证数据
+    if (this.validateReview()) {
+      this.reviewSubmitted.emit(this.reviewData);
+      this.resetForm();
+    } else {
+      alert('请完成所有必填的评价项目');
+    }
+  }
+
+  // 验证评价数据
+  private validateReview(): boolean {
+    // 检查总体评分
+    if (this.reviewData.overallSatisfaction === 0) return false;
+    
+    // 检查所有场景评分
+    for (const scene of this.reviewData.scenes) {
+      if (scene.score === 0) return false;
+    }
+    
+    return true;
+  }
+
+  // 重置表单
+  private resetForm(): void {
+    this.reviewData = {
+      overallSatisfaction: 5,
+      serviceTimeliness: 5,
+      suggestionResponse: 5,
+      smallImageDelivery: 5,
+      revisionEfficiency: 5,
+      renderingQuality: {
+        materials: 5,
+        lighting: 5,
+        texture: 5,
+        structure: 5
+      },
+      communication: {
+        documentation: 5,
+        requirementUnderstanding: 5,
+        communicationSufficiency: 5,
+        contentRelevance: 5
+      },
+      scenes: this.scenes.map(scene => ({
+        sceneName: scene,
+        designerName: this.designerName,
+        score: 5,
+        comments: ''
+      })),
+      improvementSuggestions: '',
+      nextCooperationOptimization: '',
+      willCooperateAgain: true,
+      complaintContent: '',
+      reviewDate: new Date()
+    };
+    this.showComplaint = false;
+  }
+
+  // 获取平均分
+  getAverageScore(): number {
+    const scores = [
+      this.reviewData.overallSatisfaction,
+      this.reviewData.serviceTimeliness,
+      this.reviewData.suggestionResponse,
+      this.reviewData.smallImageDelivery,
+      this.reviewData.revisionEfficiency,
+      this.reviewData.renderingQuality.materials,
+      this.reviewData.renderingQuality.lighting,
+      this.reviewData.renderingQuality.texture,
+      this.reviewData.renderingQuality.structure,
+      this.reviewData.communication.documentation,
+      this.reviewData.communication.requirementUnderstanding,
+      this.reviewData.communication.communicationSufficiency,
+      this.reviewData.communication.contentRelevance,
+      ...this.reviewData.scenes.map(scene => scene.score)
+    ];
+    
+    const sum = scores.reduce((total, score) => total + score, 0);
+    return Math.round((sum / scores.length) * 10) / 10;
+  }
+
+  // 切换投诉显示
+  toggleComplaint(): void {
+    this.showComplaint = !this.showComplaint;
+    if (!this.showComplaint) {
+      this.reviewData.complaintContent = '';
+    }
+  }
+
+  // 生成星级显示
+  generateStars(score: number): string[] {
+    const stars = [];
+    for (let i = 1; i <= 5; i++) {
+      if (i <= score) {
+        stars.push('★');
+      } else if (i - 0.5 <= score) {
+        stars.push('☆');
+      } else {
+        stars.push('☆');
+      }
+    }
+    return stars;
+  }
+}

+ 167 - 0
src/app/shared/components/panoramic-synthesis-card/panoramic-synthesis-card.html

@@ -0,0 +1,167 @@
+<!-- 全景图合成控制区域 -->
+  <div class="synthesis-controls">
+    <div class="control-header">
+      <h4>全景图合成</h4>
+      <div class="synthesis-status" [class]="'status-' + (isProcessing() ? 'processing' : 'idle')">
+        {{ isProcessing() ? '合成中' : '就绪' }}
+      </div>
+    </div>
+    
+    <div class="quality-selector">
+      <label>合成质量:</label>
+      <div class="quality-options">
+        <button class="quality-btn" 
+                [class.active]="selectedQuality() === 'standard'"
+                (click)="setQuality('standard')">
+          标准
+        </button>
+        <button class="quality-btn" 
+                [class.active]="selectedQuality() === 'high'"
+                (click)="setQuality('high')">
+          高清
+        </button>
+        <button class="quality-btn" 
+                [class.active]="selectedQuality() === 'ultra'"
+                (click)="setQuality('ultra')">
+          超清
+        </button>
+      </div>
+    </div>
+    
+    <div class="batch-controls">
+      <label class="batch-toggle">
+        <input type="checkbox" 
+               [checked]="batchMode()" 
+               (change)="toggleBatchMode()">
+        批量处理模式
+      </label>
+      
+      <div class="space-selection" *ngIf="batchMode()">
+        <div class="space-item" 
+             *ngFor="let space of synthesis?.spaces" 
+             [class.selected]="selectedSpaces().includes(space.id)">
+          <label class="space-checkbox">
+            <input type="checkbox" 
+                   [checked]="selectedSpaces().includes(space.id)"
+                   (change)="toggleSpaceSelection(space.id)">
+            {{ space.name }}
+          </label>
+          <span class="space-type">{{ getSpaceTypeLabel(space.type) }}</span>
+        </div>
+      </div>
+    </div>
+    
+    <div class="action-buttons">
+      <button class="btn btn-primary" 
+              (click)="batchMode() ? startBatchSynthesis() : startSynthesis()"
+              [disabled]="isProcessing()">
+        <i class="fas fa-play"></i>
+        {{ batchMode() ? '批量合成' : '开始合成' }}
+      </button>
+      
+      <button class="btn btn-secondary" 
+              (click)="previewSynthesis(synthesis.id)"
+              [disabled]="!synthesis">
+        <i class="fas fa-eye"></i>
+        预览
+      </button>
+      
+      <button class="btn btn-success" 
+              (click)="downloadSynthesis(synthesis.id)"
+              [disabled]="!synthesis || synthesis.status !== 'completed'">
+        <i class="fas fa-download"></i>
+        下载
+      </button>
+    </div>
+  </div>
+
+  <!-- 合成统计信息 -->
+  <div class="synthesis-stats" *ngIf="synthesisTasks && synthesisTasks.length > 0">
+    <div class="stats-header">
+      <h4>合成统计</h4>
+    </div>
+    
+    <div class="stats-grid">
+      <div class="stat-item">
+        <div class="stat-value">{{ stats().totalTasks }}</div>
+        <div class="stat-label">总任务数</div>
+      </div>
+      <div class="stat-item">
+        <div class="stat-value">{{ stats().completedTasks }}</div>
+        <div class="stat-label">已完成</div>
+      </div>
+      <div class="stat-item">
+        <div class="stat-value">{{ stats().processingTasks }}</div>
+        <div class="stat-label">处理中</div>
+      </div>
+      <div class="stat-item">
+        <div class="stat-value">{{ stats().failedTasks }}</div>
+        <div class="stat-label">失败</div>
+      </div>
+      <div class="stat-item">
+        <div class="stat-value">{{ stats().averageRenderTime }}<span class="time-suffix">分钟</span></div>
+        <div class="stat-label">平均渲染时间</div>
+      </div>
+      <div class="stat-item">
+        <div class="stat-value">{{ (stats().totalFileSize / 1024 / 1024).toFixed(1) }}<span class="size-suffix">MB</span></div>
+        <div class="stat-label">总文件大小</div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 任务列表 -->
+  <div class="task-list" *ngIf="synthesisTasks && synthesisTasks.length > 0">
+    <div class="task-header">
+      <h4>合成任务</h4>
+      <span class="task-count">{{ synthesisTasks.length }} 个任务</span>
+    </div>
+    
+    <div class="task-items">
+      <div class="task-item" 
+           *ngFor="let task of synthesisTasks" 
+           [class]="'status-' + task.status">
+        <div class="task-info">
+          <div class="task-name">{{ task.spaceName }}</div>
+          <div class="task-meta">
+            <span class="quality">{{ task.quality === 'standard' ? '标准' : task.quality === 'high' ? '高清' : '超清' }}</span>
+            <span class="progress" *ngIf="task.status === 'processing'">{{ task.progress }}%</span>
+            <span class="render-time" *ngIf="task.renderTime">{{ task.renderTime }}分钟</span>
+          </div>
+        </div>
+        
+        <div class="task-status">
+          <span class="status-badge" [class]="'badge-' + task.status">
+            {{ task.status === 'pending' ? '等待中' : 
+               task.status === 'processing' ? '处理中' : 
+               task.status === 'completed' ? '已完成' : '失败' }}
+          </span>
+        </div>
+        
+        <div class="task-actions">
+          <button class="btn btn-sm btn-primary" 
+                  (click)="previewSynthesis(task.id)"
+                  *ngIf="task.status === 'completed'">
+            <i class="fas fa-eye"></i>
+          </button>
+          
+          <button class="btn btn-sm btn-success" 
+                  (click)="downloadSynthesis(task.id)"
+                  *ngIf="task.status === 'completed'">
+            <i class="fas fa-download"></i>
+          </button>
+          
+          <button class="btn btn-sm btn-warning" 
+                  (click)="retryFailedTask(task.id)"
+                  *ngIf="task.status === 'failed'">
+            <i class="fas fa-redo"></i>
+          </button>
+          
+          <button class="btn btn-sm btn-danger" 
+                  (click)="cancelTask(task.id)"
+                  *ngIf="task.status === 'processing'">
+            <i class="fas fa-stop"></i>
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>

+ 722 - 0
src/app/shared/components/panoramic-synthesis-card/panoramic-synthesis-card.scss

@@ -0,0 +1,722 @@
+// 全景图合成控制区域样式
+.synthesis-controls {
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+  color: white;
+
+  .control-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    h4 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .synthesis-status {
+      padding: 4px 12px;
+      border-radius: 20px;
+      font-size: 12px;
+      font-weight: 500;
+
+      &.status-idle {
+        background: rgba(255, 255, 255, 0.2);
+      }
+
+      &.status-processing {
+        background: #ffc107;
+        color: #000;
+        animation: pulse 2s infinite;
+      }
+    }
+  }
+
+  .quality-selector {
+    margin-bottom: 20px;
+
+    label {
+      display: block;
+      margin-bottom: 8px;
+      font-weight: 500;
+    }
+
+    .quality-options {
+      display: flex;
+      gap: 10px;
+
+      .quality-btn {
+        padding: 8px 16px;
+        border: 2px solid rgba(255, 255, 255, 0.3);
+        border-radius: 6px;
+        background: transparent;
+        color: white;
+        cursor: pointer;
+        transition: all 0.3s ease;
+
+        &:hover {
+          border-color: rgba(255, 255, 255, 0.6);
+        }
+
+        &.active {
+          border-color: white;
+          background: rgba(255, 255, 255, 0.2);
+        }
+      }
+    }
+  }
+
+  .batch-controls {
+    margin-bottom: 20px;
+
+    .batch-toggle {
+      display: flex;
+      align-items: center;
+      margin-bottom: 15px;
+      cursor: pointer;
+
+      input[type="checkbox"] {
+        margin-right: 8px;
+      }
+    }
+
+    .space-selection {
+      background: rgba(255, 255, 255, 0.1);
+      border-radius: 8px;
+      padding: 15px;
+      max-height: 200px;
+      overflow-y: auto;
+
+      .space-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px 0;
+        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+
+        &:last-child {
+          border-bottom: none;
+        }
+
+        &.selected {
+          background: rgba(255, 255, 255, 0.1);
+          border-radius: 4px;
+          padding: 8px;
+        }
+
+        .space-checkbox {
+          display: flex;
+          align-items: center;
+          cursor: pointer;
+
+          input[type="checkbox"] {
+            margin-right: 8px;
+          }
+        }
+
+        .space-type {
+          font-size: 12px;
+          opacity: 0.8;
+        }
+      }
+    }
+  }
+
+  .action-buttons {
+    display: flex;
+    gap: 10px;
+    flex-wrap: wrap;
+
+    .btn {
+      padding: 10px 20px;
+      border: none;
+      border-radius: 6px;
+      font-size: 14px;
+      cursor: pointer;
+      transition: all 0.3s ease;
+
+      &.btn-primary {
+        background: #007bff;
+        color: white;
+
+        &:hover:not(:disabled) {
+          background: #0056b3;
+        }
+
+        &:disabled {
+          background: rgba(255, 255, 255, 0.3);
+          cursor: not-allowed;
+        }
+      }
+
+      &.btn-secondary {
+        background: rgba(255, 255, 255, 0.2);
+        color: white;
+
+        &:hover:not(:disabled) {
+          background: rgba(255, 255, 255, 0.3);
+        }
+
+        &:disabled {
+          opacity: 0.5;
+          cursor: not-allowed;
+        }
+      }
+
+      &.btn-success {
+        background: #28a745;
+        color: white;
+
+        &:hover:not(:disabled) {
+          background: #1e7e34;
+        }
+
+        &:disabled {
+          background: rgba(255, 255, 255, 0.3);
+          cursor: not-allowed;
+        }
+      }
+
+      i {
+        margin-right: 6px;
+      }
+    }
+  }
+}
+
+// 合成统计样式
+.synthesis-stats {
+  background: #fff;
+  border: 1px solid #e9ecef;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+
+  .stats-header {
+    margin-bottom: 15px;
+
+    h4 {
+      margin: 0;
+      color: #333;
+      font-size: 18px;
+      font-weight: 600;
+    }
+  }
+
+  .stats-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+    gap: 15px;
+
+    .stat-item {
+      text-align: center;
+      padding: 15px;
+      background: #f8f9fa;
+      border-radius: 8px;
+
+      .stat-value {
+        font-size: 24px;
+        font-weight: 700;
+        color: #007bff;
+        margin-bottom: 5px;
+
+        .time-suffix,
+        .size-suffix {
+          font-size: 14px;
+          font-weight: 400;
+          color: #666;
+        }
+      }
+
+      .stat-label {
+        font-size: 12px;
+        color: #666;
+        text-transform: uppercase;
+        letter-spacing: 0.5px;
+      }
+    }
+  }
+}
+
+// 任务列表样式
+.task-list {
+  background: #fff;
+  border: 1px solid #e9ecef;
+  border-radius: 12px;
+  padding: 20px;
+
+  .task-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+
+    h4 {
+      margin: 0;
+      color: #333;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .task-count {
+      background: #007bff;
+      color: white;
+      padding: 4px 12px;
+      border-radius: 20px;
+      font-size: 12px;
+      font-weight: 500;
+    }
+  }
+
+  .task-items {
+    .task-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 15px;
+      border: 1px solid #e9ecef;
+      border-radius: 8px;
+      margin-bottom: 10px;
+      transition: all 0.3s ease;
+
+      &:hover {
+        border-color: #007bff;
+        box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
+      }
+
+      &.status-pending {
+        border-left: 4px solid #6c757d;
+      }
+
+      &.status-processing {
+        border-left: 4px solid #ffc107;
+        background: #fff9c4;
+      }
+
+      &.status-completed {
+        border-left: 4px solid #28a745;
+      }
+
+      &.status-failed {
+        border-left: 4px solid #dc3545;
+        background: #f8d7da;
+      }
+
+      .task-info {
+        flex: 1;
+
+        .task-name {
+          font-weight: 500;
+          color: #333;
+          margin-bottom: 8px;
+          font-size: 14px;
+        }
+
+        .task-meta {
+          display: flex;
+          gap: 15px;
+          font-size: 12px;
+          color: #666;
+
+          .quality {
+            background: #e9ecef;
+            padding: 2px 8px;
+            border-radius: 12px;
+            font-weight: 500;
+          }
+
+          .progress {
+            color: #ffc107;
+            font-weight: 500;
+          }
+
+          .render-time {
+            color: #28a745;
+          }
+        }
+      }
+
+      .task-status {
+        margin: 0 15px;
+
+        .status-badge {
+          padding: 4px 12px;
+          border-radius: 20px;
+          font-size: 12px;
+          font-weight: 500;
+
+          &.badge-pending {
+            background: #6c757d;
+            color: white;
+          }
+
+          &.badge-processing {
+            background: #ffc107;
+            color: #000;
+          }
+
+          &.badge-completed {
+            background: #28a745;
+            color: white;
+          }
+
+          &.badge-failed {
+            background: #dc3545;
+            color: white;
+          }
+        }
+      }
+
+      .task-actions {
+        display: flex;
+        gap: 8px;
+
+        .btn {
+          padding: 6px 12px;
+          border: none;
+          border-radius: 4px;
+          font-size: 12px;
+          cursor: pointer;
+          transition: all 0.3s ease;
+
+          &.btn-sm {
+            padding: 4px 8px;
+          }
+
+          &.btn-primary {
+            background: #007bff;
+            color: white;
+
+            &:hover {
+              background: #0056b3;
+            }
+          }
+
+          &.btn-success {
+            background: #28a745;
+            color: white;
+
+            &:hover {
+              background: #1e7e34;
+            }
+          }
+
+          &.btn-warning {
+            background: #ffc107;
+            color: #000;
+
+            &:hover {
+              background: #e0a800;
+            }
+          }
+
+          &.btn-danger {
+            background: #dc3545;
+            color: white;
+
+            &:hover {
+              background: #c82333;
+            }
+          }
+
+          i {
+            margin-right: 4px;
+          }
+        }
+      }
+    }
+  }
+}
+
+.panoramic-synthesis-card {
+  background: #fff;
+  border: 1px solid #e8e8e8;
+  border-radius: 8px;
+  padding: 20px;
+  margin-bottom: 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  transition: box-shadow 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+  }
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #f0f0f0;
+
+    .project-name {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #262626;
+    }
+
+    .status-badge {
+      padding: 4px 12px;
+      border-radius: 20px;
+      font-size: 12px;
+      font-weight: 500;
+
+      &.status-draft {
+        background: #f0f0f0;
+        color: #595959;
+      }
+
+      &.status-processing {
+        background: #e6f7ff;
+        color: #1890ff;
+      }
+
+      &.status-completed {
+        background: #f6ffed;
+        color: #52c41a;
+      }
+
+      &.status-published {
+        background: #fff7e6;
+        color: #fa8c16;
+      }
+    }
+  }
+
+  .card-content {
+    margin-bottom: 20px;
+
+    .info-section {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+      gap: 12px;
+      margin-bottom: 20px;
+
+      .info-item {
+        display: flex;
+        align-items: center;
+
+        .label {
+          font-weight: 500;
+          color: #595959;
+          margin-right: 8px;
+          font-size: 14px;
+        }
+
+        .value {
+          color: #262626;
+          font-size: 14px;
+        }
+      }
+    }
+
+    .spaces-section {
+      h4 {
+        margin: 0 0 12px 0;
+        font-size: 16px;
+        font-weight: 600;
+        color: #262626;
+      }
+
+      .space-list {
+        .space-item {
+          background: #fafafa;
+          border: 1px solid #e8e8e8;
+          border-radius: 6px;
+          padding: 12px;
+          margin-bottom: 8px;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+
+          .space-header {
+            display: flex;
+            align-items: center;
+            margin-bottom: 8px;
+
+            .space-name {
+              font-weight: 500;
+              color: #262626;
+              margin-right: 12px;
+            }
+
+            .space-type {
+              background: #f0f0f0;
+              color: #595959;
+              padding: 2px 8px;
+              border-radius: 4px;
+              font-size: 12px;
+              margin-right: 12px;
+            }
+
+            .space-status {
+              padding: 2px 8px;
+              border-radius: 4px;
+              font-size: 12px;
+              font-weight: 500;
+
+              &.status-pending {
+                background: #f0f0f0;
+                color: #595959;
+              }
+
+              &.status-processing {
+                background: #e6f7ff;
+                color: #1890ff;
+              }
+
+              &.status-completed {
+                background: #f6ffed;
+                color: #52c41a;
+              }
+
+              &.status-failed {
+                background: #fff1f0;
+                color: #f5222d;
+              }
+            }
+          }
+
+          .space-progress {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            margin-bottom: 8px;
+
+            .progress-bar {
+              flex: 1;
+              height: 8px;
+              background: #f0f0f0;
+              border-radius: 4px;
+              overflow: hidden;
+
+              .progress-fill {
+                height: 100%;
+                border-radius: 4px;
+                transition: width 0.3s ease;
+              }
+            }
+
+            .progress-text {
+              font-size: 12px;
+              color: #595959;
+              min-width: 40px;
+              text-align: right;
+            }
+          }
+
+          .space-images {
+            .image-count {
+              font-size: 12px;
+              color: #8c8c8c;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .card-actions {
+    display: flex;
+    gap: 8px;
+    justify-content: flex-end;
+
+    .btn {
+      padding: 8px 16px;
+      border: 1px solid;
+      border-radius: 6px;
+      font-size: 14px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.2s ease;
+
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+
+      &.btn-primary {
+        background: #1890ff;
+        border-color: #1890ff;
+        color: #fff;
+
+        &:hover:not(:disabled) {
+          background: #40a9ff;
+          border-color: #40a9ff;
+        }
+      }
+
+      &.btn-secondary {
+        background: #52c41a;
+        border-color: #52c41a;
+        color: #fff;
+
+        &:hover:not(:disabled) {
+          background: #73d13d;
+          border-color: #73d13d;
+        }
+      }
+
+      &.btn-outline {
+        background: transparent;
+        border-color: #d9d9d9;
+        color: #595959;
+
+        &:hover:not(:disabled) {
+          border-color: #1890ff;
+          color: #1890ff;
+        }
+      }
+
+      &.btn-danger {
+        background: #ff4d4f;
+        border-color: #ff4d4f;
+        color: #fff;
+
+        &:hover:not(:disabled) {
+          background: #ff7875;
+          border-color: #ff7875;
+        }
+      }
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .panoramic-synthesis-card {
+    padding: 16px;
+
+    .card-header {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 8px;
+
+      .status-badge {
+        align-self: flex-start;
+      }
+    }
+
+    .card-content {
+      .info-section {
+        grid-template-columns: 1fr;
+      }
+    }
+
+    .card-actions {
+      flex-wrap: wrap;
+      justify-content: center;
+
+      .btn {
+        flex: 1;
+        min-width: 80px;
+      }
+    }
+  }
+}

+ 243 - 0
src/app/shared/components/panoramic-synthesis-card/panoramic-synthesis-card.ts

@@ -0,0 +1,243 @@
+import { Component, Input, signal, computed } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { PanoramicSynthesis, PanoramicSpace, SpaceImage } from '../../../models/project.model';
+
+export interface SynthesisTask {
+  id: string;
+  projectId: string;
+  spaceId: string;
+  spaceName: string;
+  spaceType: string;
+  status: 'pending' | 'processing' | 'completed' | 'failed';
+  progress: number;
+  startTime?: Date;
+  completionTime?: Date;
+  outputUrl?: string;
+  errorMessage?: string;
+  quality?: 'standard' | 'high' | 'ultra';
+  renderTime?: number; // 渲染时间(分钟)
+  fileSize?: number; // 文件大小(字节)
+}
+
+export interface SynthesisStats {
+  totalTasks: number;
+  completedTasks: number;
+  processingTasks: number;
+  failedTasks: number;
+  averageRenderTime: number;
+  totalFileSize: number;
+}
+
+@Component({
+  selector: 'app-panoramic-synthesis-card',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './panoramic-synthesis-card.html',
+  styleUrls: ['./panoramic-synthesis-card.scss']
+})
+export class PanoramicSynthesisCardComponent {
+  @Input() synthesis!: PanoramicSynthesis;
+  @Input() showActions: boolean = true;
+  @Input() synthesisTasks: SynthesisTask[] = [];
+  
+  // 状态管理
+  isProcessing = signal<boolean>(false);
+  selectedQuality = signal<'standard' | 'high' | 'ultra'>('high');
+  batchMode = signal<boolean>(false);
+  selectedSpaces = signal<string[]>([]);
+  
+  // 计算统计数据
+  stats = computed<SynthesisStats>(() => {
+    const tasks = this.synthesisTasks || [];
+    const completedTasks = tasks.filter(t => t.status === 'completed');
+    const processingTasks = tasks.filter(t => t.status === 'processing');
+    const failedTasks = tasks.filter(t => t.status === 'failed');
+    
+    const renderTimes = completedTasks
+      .filter(t => t.startTime && t.completionTime)
+      .map(t => (t.completionTime!.getTime() - t.startTime!.getTime()) / 1000);
+    
+    const averageRenderTime = renderTimes.length > 0 
+      ? renderTimes.reduce((sum, time) => sum + time, 0) / renderTimes.length 
+      : 0;
+    
+    return {
+      totalTasks: tasks.length,
+      completedTasks: completedTasks.length,
+      processingTasks: processingTasks.length,
+      failedTasks: failedTasks.length,
+      averageRenderTime,
+      totalFileSize: 0 // 这里可以根据实际需要计算
+    };
+  });
+
+  getStatusLabel(status: string): string {
+    const statusMap: { [key: string]: string } = {
+      'draft': '草稿',
+      'processing': '处理中',
+      'completed': '已完成',
+      'published': '已发布'
+    };
+    return statusMap[status] || status;
+  }
+
+  getSpaceTypeLabel(type: string): string {
+    const typeMap: { [key: string]: string } = {
+      'living_room': '客厅',
+      'bedroom': '卧室',
+      'kitchen': '厨房',
+      'bathroom': '卫生间',
+      'dining_room': '餐厅',
+      'study': '书房',
+      'balcony': '阳台'
+    };
+    return typeMap[type] || type;
+  }
+
+  getQualityLabel(quality: string): string {
+    const qualityMap: { [key: string]: string } = {
+      'standard': '标准',
+      'high': '高清',
+      'ultra': '超清'
+    };
+    return qualityMap[quality] || quality;
+  }
+
+  getProgressPercentage(space: PanoramicSpace): number {
+    return space.progress || 0;
+  }
+
+  getProgressColor(progress: number): string {
+    if (progress >= 100) return '#52c41a';
+    if (progress >= 70) return '#1890ff';
+    if (progress >= 30) return '#faad14';
+    return '#f5222d';
+  }
+
+  onPreview(synthesis: PanoramicSynthesis): void {
+    if (synthesis.previewUrl) {
+      window.open(synthesis.previewUrl, '_blank');
+    } else {
+      console.warn('预览链接不可用');
+    }
+  }
+
+  onDownload(synthesis: PanoramicSynthesis): void {
+    if (synthesis.downloadUrl) {
+      window.open(synthesis.downloadUrl, '_blank');
+    } else {
+      console.warn('下载链接不可用');
+    }
+  }
+
+  onEdit(synthesis: PanoramicSynthesis): void {
+    console.log('编辑全景合成:', synthesis);
+    // 实际实现中会导航到编辑页面
+  }
+
+  onDelete(synthesis: PanoramicSynthesis): void {
+    if (confirm('确定要删除这个全景合成吗?')) {
+      console.log('删除全景合成:', synthesis);
+      // 实际实现中会调用删除API
+    }
+  }
+
+  formatFileSize(bytes: number | undefined): string {
+    if (!bytes) return '未知';
+    
+    const sizes = ['B', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(1024));
+    return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
+  }
+
+  // 开始合成任务
+  startSynthesis(spaceIds: string[] = []): void {
+    if (this.isProcessing()) return;
+    
+    this.isProcessing.set(true);
+    const spacesToProcess = spaceIds.length > 0 ? spaceIds : this.selectedSpaces();
+    
+    console.log('开始全景图合成:', {
+      spaces: spacesToProcess,
+      quality: this.selectedQuality(),
+      batchMode: this.batchMode()
+    });
+    
+    // 模拟合成过程
+    setTimeout(() => {
+      this.isProcessing.set(false);
+      console.log('全景图合成完成');
+    }, 3000);
+  }
+
+  // 批量合成
+  startBatchSynthesis(): void {
+    if (this.synthesis?.spaces) {
+      const allSpaceIds = this.synthesis.spaces.map(space => space.id);
+      this.startSynthesis(allSpaceIds);
+    }
+  }
+
+  // 切换空间选择
+  toggleSpaceSelection(spaceId: string): void {
+    const current = this.selectedSpaces();
+    if (current.includes(spaceId)) {
+      this.selectedSpaces.set(current.filter(id => id !== spaceId));
+    } else {
+      this.selectedSpaces.set([...current, spaceId]);
+    }
+  }
+
+  // 设置合成质量
+  setQuality(quality: 'standard' | 'high' | 'ultra'): void {
+    this.selectedQuality.set(quality);
+  }
+
+  // 切换批量模式
+  toggleBatchMode(): void {
+    this.batchMode.set(!this.batchMode());
+    if (this.batchMode()) {
+      // 批量模式下选择所有空间
+      if (this.synthesis?.spaces) {
+        this.selectedSpaces.set(this.synthesis.spaces.map(space => space.id));
+      }
+    } else {
+      this.selectedSpaces.set([]);
+    }
+  }
+
+  // 预览合成结果
+  previewSynthesis(spaceId: string): void {
+    const task = this.synthesisTasks.find(t => t.spaceId === spaceId);
+    if (task?.outputUrl) {
+      window.open(task.outputUrl, '_blank');
+    } else {
+      console.warn('预览不可用');
+    }
+  }
+
+  // 下载合成结果
+  downloadSynthesis(spaceId: string): void {
+    const task = this.synthesisTasks.find(t => t.spaceId === spaceId);
+    if (task?.outputUrl) {
+      const link = document.createElement('a');
+      link.href = task.outputUrl;
+      link.download = `panoramic_${task.spaceName}.jpg`;
+      link.click();
+    } else {
+      console.warn('下载不可用');
+    }
+  }
+
+  // 重试失败的任务
+  retryFailedTask(taskId: string): void {
+    console.log('重试合成任务:', taskId);
+    // 这里可以实现重试逻辑
+  }
+
+  // 取消处理中的任务
+  cancelTask(taskId: string): void {
+    console.log('取消合成任务:', taskId);
+    // 这里可以实现取消逻辑
+  }
+}

+ 257 - 101
src/app/shared/components/settlement-card/settlement-card.html

@@ -1,111 +1,267 @@
 <div class="settlement-card">
-  <!-- 统计数据概览 -->
-  <div class="stats-overview">
-    <h4>尾款结算概览</h4>
-    <div class="stats-grid">
-      <div class="stat-item total">
-        <div class="stat-value">{{ formatAmount(stats().totalAmount) }}</div>
-        <div class="stat-label">总金额 ({{ stats().totalCount }}项)</div>
+  <!-- 自动化控制头部 -->
+  @if (showAutomationControls) {
+    <div class="automation-header">
+      <div class="automation-title">
+        <mat-icon>auto_awesome</mat-icon>
+        <span>自动化结算处理</span>
       </div>
-      <div class="stat-item pending">
-        <div class="stat-value">{{ formatAmount(stats().pendingAmount) }}</div>
-        <div class="stat-label">待收款 ({{ stats().pendingCount }}项)</div>
+      <div class="automation-actions">
+        <button 
+          mat-raised-button 
+          color="primary"
+          [disabled]="isProcessing()"
+          (click)="processAllAutomation()"
+          matTooltip="批量处理所有待结算项">
+          @if (isProcessing()) {
+            <mat-spinner diameter="16"></mat-spinner>
+            <span>处理中...</span>
+          } @else {
+            <ng-container>
+              <mat-icon>play_arrow</mat-icon>
+              <span>批量处理</span>
+            </ng-container>
+          }
+        </button>
       </div>
-      <div class="stat-item overdue">
-        <div class="stat-value">{{ formatAmount(stats().overdueAmount) }}</div>
-        <div class="stat-label">逾期尾款 ({{ stats().overdueCount }}项)</div>
-      </div>
-      <div class="stat-item completed">
-        <div class="stat-value">{{ formatAmount(stats().completedAmount) }}</div>
-        <div class="stat-label">已收款 ({{ stats().completedCount }}项)</div>
-      </div>
-    </div>
-  </div>
-
-  <!-- 筛选和搜索 -->
-  <div class="filter-section">
-    <div class="search-bar">
-      <input 
-        type="text" 
-        placeholder="搜索项目名称或阶段..."
-        [value]="searchKeyword()"
-        (input)="updateSearchKeyword($event.target.value)"
-        class="search-input">
-    </div>
-    <div class="filter-buttons">
-      <button 
-        class="filter-btn"
-        [class.active]="statusFilter() === 'all'"
-        (click)="updateStatusFilter('all')">
-        全部
-      </button>
-      <button 
-        class="filter-btn pending"
-        [class.active]="statusFilter() === '待结算'"
-        (click)="updateStatusFilter('待结算')">
-        待结算
-      </button>
-      <button 
-        class="filter-btn overdue"
-        [class.active]="statusFilter() === 'overdue'"
-        (click)="updateStatusFilter('overdue')">
-        逾期
-      </button>
-      <button 
-        class="filter-btn completed"
-        [class.active]="statusFilter() === '已结算'"
-        (click)="updateStatusFilter('已结算')">
-        已结算
-      </button>
     </div>
-  </div>
+  }
 
-  <!-- 结算列表 -->
-  <div class="settlements-list">
-    @if (filteredSettlements() && filteredSettlements().length > 0) {
-      <div class="list-header">
-        <span class="col-project">项目信息</span>
-        <span class="col-amount">金额</span>
-        <span class="col-status">状态</span>
-        <span class="col-date">日期</span>
+  <div class="settlement-card">
+    <!-- 统计概览 -->
+    <div class="stats-overview">
+      <div class="stats-title">
+        <mat-icon>account_balance_wallet</mat-icon>
+        <span>尾款结算统计</span>
       </div>
-      <div class="list-body">
-        @for (settlement of filteredSettlements(); track settlement.id) {
-          <div class="settlement-item" [class]="getStatusClass(settlement)">
-            <div class="col-project">
-              <div class="project-name">{{ settlement.projectName || '结算项' }}</div>
-              <div class="stage-name">{{ settlement.stage || '阶段' }}</div>
-            </div>
-            <div class="col-amount">
-              <div class="amount">{{ formatAmount(settlement.amount || 0) }}</div>
-              <div class="percentage">{{ settlement.percentage }}%</div>
-            </div>
-            <div class="col-status">
-              <span class="status-badge" [class]="getStatusClass(settlement)">
-                {{ settlement.status }}
-              </span>
-              @if (isOverdue(settlement)) {
-                <div class="overdue-days">逾期 {{ getDaysOverdue(settlement) }} 天</div>
-              }
-            </div>
-            <div class="col-date">
-              @if (settlement.settledAt) {
-                <div class="settled-date">{{ settlement.settledAt | date:'yyyy-MM-dd' }}</div>
-                <div class="date-label">结算日期</div>
-              } @else {
-                <div class="due-date">{{ settlement.createdAt | date:'yyyy-MM-dd' }}</div>
-                <div class="date-label">创建日期</div>
-              }
-            </div>
-          </div>
-        }
+      <div class="stats-grid">
+        <div class="stat-item total">
+          <div class="stat-value">{{ formatAmount(stats().totalAmount) }}</div>
+          <div class="stat-label">总金额 ({{ stats().totalCount }}项)</div>
+        </div>
+        <div class="stat-item pending">
+          <div class="stat-value">{{ formatAmount(stats().pendingAmount) }}</div>
+          <div class="stat-label">待结算 ({{ stats().pendingCount }}项)</div>
+        </div>
+        <div class="stat-item overdue">
+          <div class="stat-value">{{ formatAmount(stats().overdueAmount) }}</div>
+          <div class="stat-label">逾期 ({{ stats().overdueCount }}项)</div>
+        </div>
+        <div class="stat-item completed">
+          <div class="stat-value">{{ formatAmount(stats().completedAmount) }}</div>
+          <div class="stat-label">已结算 ({{ stats().completedCount }}项)</div>
+        </div>
+      </div>
+    </div>
+  
+    <!-- 筛选区域 -->
+    <div class="filter-section">
+      <div class="search-box">
+        <mat-icon>search</mat-icon>
+        <input 
+          type="text" 
+          placeholder="搜索项目名称或阶段..."
+          [value]="searchKeyword()"
+          (input)="updateSearchKeyword($event.target.value)">
       </div>
-    } @else {
-      <div class="empty-state">
-        <div class="empty-icon">💰</div>
-        <div class="empty-title">暂无结算信息</div>
-        <div class="empty-desc">当前筛选条件下没有找到相关的结算记录</div>
+      <div class="filter-buttons">
+        <button 
+          class="filter-btn"
+          [class.active]="statusFilter() === 'all'"
+          (click)="updateStatusFilter('all')">
+          全部
+        </button>
+        <button 
+          class="filter-btn"
+          [class.active]="statusFilter() === '待结算'"
+          (click)="updateStatusFilter('待结算')">
+          待结算
+        </button>
+        <button 
+          class="filter-btn"
+          [class.active]="statusFilter() === '已结算'"
+          (click)="updateStatusFilter('已结算')">
+          已结算
+        </button>
+        <button 
+          class="filter-btn overdue"
+          [class.active]="statusFilter() === 'overdue'"
+          (click)="updateStatusFilter('overdue')">
+          逾期
+        </button>
       </div>
-    }
+    </div>
+  
+    <!-- 结算列表 -->
+    <div class="settlement-list">
+      @if (filteredSettlements().length > 0) {
+        <div class="list-header">
+          <span class="col-project">项目信息</span>
+          <span class="col-amount">金额</span>
+          <span class="col-status">状态</span>
+          <span class="col-actions">操作</span>
+          <span class="col-date">日期</span>
+        </div>
+        <div class="list-body">
+          @for (settlement of filteredSettlements(); track settlement.id) {
+            <div class="settlement-item" [class]="getStatusClass(settlement)">
+              <div class="col-project">
+                <div class="project-name">{{ settlement.projectName || '结算项' }}</div>
+                <div class="stage-name">{{ settlement.stage || '阶段' }}</div>
+              </div>
+              <div class="col-amount">
+                <div class="amount">{{ formatAmount(settlement.amount || 0) }}</div>
+                <div class="percentage">{{ settlement.percentage }}%</div>
+              </div>
+              <div class="col-status">
+                <span class="status-badge" [class]="getStatusClass(settlement)">
+                  {{ settlement.status }}
+                </span>
+                @if (isOverdue(settlement)) {
+                  <div class="overdue-days">逾期 {{ getDaysOverdue(settlement) }} 天</div>
+                }
+                
+                <!-- 自动化处理按钮 -->
+                @if (showAutomationControls && settlement.status === '待结算' && !isOverdue(settlement)) {
+                  <button 
+                    class="auto-process-btn"
+                    mat-icon-button
+                    color="primary"
+                    [disabled]="isProcessing() && processingSettlementId() !== settlement.id"
+                    (click)="processSettlementAutomation(settlement); $event.stopPropagation()"
+                    matTooltip="自动处理此结算">
+                    @if (processingSettlementId() === settlement.id) {
+                      <mat-spinner diameter="16"></mat-spinner>
+                    } @else {
+                      <mat-icon>auto_awesome</mat-icon>
+                    }
+                  </button>
+                }
+              </div>
+              
+              <!-- 新增操作列 -->
+              <div class="col-actions">
+                <div class="action-buttons">
+                  <!-- 项目验收确认按钮 -->
+                  @if (settlement.status === '待结算') {
+                    <button 
+                      mat-stroked-button
+                      [color]="getAcceptanceStatus(settlement.projectId) === 'confirmed' ? 'primary' : 'accent'"
+                      [disabled]="getAcceptanceStatus(settlement.projectId) === 'confirming'"
+                      (click)="confirmProjectAcceptance(settlement)"
+                      matTooltip="技术确认项目验收"
+                      class="acceptance-btn">
+                      @if (getAcceptanceStatus(settlement.projectId) === 'confirming') {
+                        <mat-spinner diameter="16"></mat-spinner>
+                        <span>确认中</span>
+                      } @else if (getAcceptanceStatus(settlement.projectId) === 'confirmed') {
+                        <ng-container>
+                          <mat-icon>check_circle</mat-icon>
+                          <span>已验收</span>
+                        </ng-container>
+                      } @else {
+                        <ng-container>
+                          <mat-icon>task_alt</mat-icon>
+                          <span>确认验收</span>
+                        </ng-container>
+                      }
+                    </button>
+                  }
+                  
+                  <!-- 客服跟进提醒按钮 -->
+                  @if (settlement.status === '待结算' && getAcceptanceStatus(settlement.projectId) === 'confirmed') {
+                    <button 
+                      mat-stroked-button
+                      color="warn"
+                      [disabled]="getReminderStatus(settlement.projectId) === 'creating' || getReminderStatus(settlement.projectId) === 'created'"
+                      (click)="createCustomerServiceReminder(settlement, 'payment_follow_up')"
+                      matTooltip="创建客服跟进尾款提醒"
+                      class="reminder-btn">
+                      @if (getReminderStatus(settlement.projectId) === 'creating') {
+                        <mat-spinner diameter="16"></mat-spinner>
+                        <span>创建中</span>
+                      } @else if (getReminderStatus(settlement.projectId) === 'created') {
+                        <ng-container>
+                          <mat-icon>notifications_active</mat-icon>
+                          <span>已提醒</span>
+                        </ng-container>
+                      } @else {
+                        <ng-container>
+                          <mat-icon>notification_add</mat-icon>
+                          <span>跟进尾款</span>
+                        </ng-container>
+                      }
+                    </button>
+                  }
+                  
+                  <!-- 一键发图到企业微信群按钮 -->
+                  @if (canSendImages(settlement)) {
+                    <button 
+                      mat-raised-button
+                      color="primary"
+                      [disabled]="isSendingImagesForProject(settlement.projectId)"
+                      (click)="sendImagesToWeChatGroup(settlement)"
+                      matTooltip="收款后一键发送大图到企业微信群"
+                      class="wechat-send-btn">
+                      @if (isSendingImagesForProject(settlement.projectId)) {
+                        <mat-spinner diameter="16"></mat-spinner>
+                        <span>发送中</span>
+                      } @else {
+                        <ng-container>
+                          <mat-icon>send</mat-icon>
+                          <span>发图到群</span>
+                        </ng-container>
+                      }
+                    </button>
+                  }
+                  
+                  <!-- 原有的处理结算按钮 -->
+                  <button 
+                    mat-raised-button
+                    color="primary"
+                    [disabled]="settlement.status === '已结算'"
+                    (click)="processSettlement(settlement.id)"
+                    class="process-btn">
+                    @if (settlement.status === '待结算') {
+                      处理结算
+                    } @else if (settlement.status === '逾期') {
+                      逾期处理
+                    } @else {
+                      已完成
+                    }
+                  </button>
+                  
+                  @if (isOverdue(settlement)) {
+                    <button 
+                      mat-stroked-button
+                      color="warn"
+                      (click)="sendReminder(settlement.id)"
+                      class="reminder-btn">
+                      发送催款
+                    </button>
+                  }
+                </div>
+              </div>
+              
+              <div class="col-date">
+                @if (settlement.settledAt) {
+                  <div class="settled-date">{{ settlement.settledAt | date:'yyyy-MM-dd' }}</div>
+                  <div class="date-label">结算日期</div>
+                } @else {
+                  <div class="due-date">{{ settlement.createdAt | date:'yyyy-MM-dd' }}</div>
+                  <div class="date-label">创建日期</div>
+                }
+              </div>
+            </div>
+          }
+        </div>
+      } @else {
+        <div class="empty-state">
+          <div class="empty-icon">💰</div>
+          <div class="empty-title">暂无结算信息</div>
+          <div class="empty-desc">当前筛选条件下没有找到相关的结算记录</div>
+        </div>
+      }
+    </div>
   </div>
 </div>

+ 266 - 101
src/app/shared/components/settlement-card/settlement-card.scss

@@ -12,15 +12,25 @@
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
   border: 1px solid $ios-border;
 
-  // 统计数据概览
+  // 统计概览
   .stats-overview {
     margin-bottom: 24px;
 
-    h4 {
-      margin: 0 0 16px 0;
+    .stats-title {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin-bottom: 16px;
       font-size: 18px;
       font-weight: $ios-font-weight-semibold;
       color: $ios-text-primary;
+
+      mat-icon {
+        color: $ios-primary;
+        font-size: 20px;
+        width: 20px;
+        height: 20px;
+      }
     }
 
     .stats-grid {
@@ -44,20 +54,23 @@
         &.pending {
           background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
           color: #8b4513;
+          border: none;
         }
 
         &.overdue {
           background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
-          color: #dc3545;
+          color: #d63384;
+          border: none;
         }
 
         &.completed {
           background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
-          color: #28a745;
+          color: #198754;
+          border: none;
         }
 
         .stat-value {
-          font-size: 24px;
+          font-size: 20px;
           font-weight: $ios-font-weight-bold;
           margin-bottom: 4px;
         }
@@ -70,31 +83,39 @@
     }
   }
 
-  // 筛选和搜索区域
+  // 筛选区域
   .filter-section {
-    margin-bottom: 20px;
     display: flex;
-    flex-direction: column;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
     gap: 16px;
 
-    .search-bar {
-      width: 100%;
+    .search-box {
+      display: flex;
+      align-items: center;
+      background: #f8f9fa;
+      border-radius: 20px;
+      padding: 8px 16px;
+      flex: 1;
+      max-width: 300px;
+      border: 1px solid $ios-border;
+
+      mat-icon {
+        color: $ios-text-secondary;
+        margin-right: 8px;
+        font-size: 18px;
+        width: 18px;
+        height: 18px;
+      }
 
-      .search-input {
-        width: 100%;
-        padding: 10px 16px;
-        border: 1px solid $ios-border;
-        border-radius: 8px;
+      input {
+        border: none;
+        background: transparent;
+        outline: none;
+        flex: 1;
         font-size: 14px;
-        background: white;
-        transition: border-color 0.2s ease;
-        box-sizing: border-box;
-
-        &:focus {
-          outline: none;
-          border-color: $ios-primary;
-          box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
-        }
+        color: $ios-text-primary;
 
         &::placeholder {
           color: $ios-text-secondary;
@@ -105,23 +126,20 @@
     .filter-buttons {
       display: flex;
       gap: 8px;
-      flex-wrap: wrap;
-      width: 100%;
-      justify-content: flex-start;
 
       .filter-btn {
-        padding: 8px 16px;
+        padding: 6px 16px;
         border: 1px solid $ios-border;
-        border-radius: 20px;
         background: white;
-        color: $ios-text-secondary;
-        font-size: 14px;
+        border-radius: 16px;
+        font-size: 13px;
         cursor: pointer;
         transition: all 0.2s ease;
-        white-space: nowrap; // 防止按钮文字换行
+        color: $ios-text-secondary;
 
         &:hover {
           background: #f8f9fa;
+          border-color: $ios-primary;
         }
 
         &.active {
@@ -130,45 +148,46 @@
           border-color: $ios-primary;
         }
 
-        &.pending.active {
-          background: #ff9500;
-          border-color: #ff9500;
-        }
-
         &.overdue.active {
           background: #dc3545;
           border-color: #dc3545;
         }
-
-        &.completed.active {
-          background: #28a745;
-          border-color: #28a745;
-        }
       }
     }
   }
 
   // 结算列表
-  .settlements-list {
+  .settlement-list {
     .list-header {
       display: grid;
-      grid-template-columns: 2fr 1fr 1fr 1fr;
+      grid-template-columns: 2fr 1fr 1fr 2fr 1fr;
       gap: 16px;
       padding: 12px 16px;
       background: #f8f9fa;
       border-radius: 8px;
       font-weight: $ios-font-weight-semibold;
+      font-size: 13px;
       color: $ios-text-secondary;
-      font-size: 12px;
-      text-transform: uppercase;
-      letter-spacing: 0.5px;
       margin-bottom: 8px;
+
+      span {
+        text-align: left;
+
+        &.col-amount,
+        &.col-date {
+          text-align: center;
+        }
+
+        &.col-actions {
+          text-align: center;
+        }
+      }
     }
 
     .list-body {
       .settlement-item {
         display: grid;
-        grid-template-columns: 2fr 1fr 1fr 1fr;
+        grid-template-columns: 2fr 1fr 1fr minmax(200px, 2fr) 1fr;
         gap: 16px;
         padding: 16px;
         border: 1px solid $ios-border;
@@ -179,16 +198,11 @@
 
         &:hover {
           box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-          transform: translateY(-1px);
-        }
-
-        &.overdue {
-          border-left: 4px solid #dc3545;
-          background: #fff5f5;
+          border-color: $ios-primary;
         }
 
         &.pending {
-          border-left: 4px solid #ff9500;
+          border-left: 4px solid #ffc107;
         }
 
         &.completed {
@@ -196,9 +210,14 @@
           background: #f8fff9;
         }
 
+        &.overdue {
+          border-left: 4px solid #dc3545;
+          background: #fff8f8;
+        }
+
         .col-project {
           .project-name {
-            font-weight: $ios-font-weight-semibold;
+            font-weight: $ios-font-weight-medium;
             color: $ios-text-primary;
             margin-bottom: 4px;
           }
@@ -210,40 +229,44 @@
         }
 
         .col-amount {
+          text-align: center;
+
           .amount {
-            font-weight: $ios-font-weight-bold;
-            color: $ios-primary;
-            font-size: 16px;
+            font-weight: $ios-font-weight-semibold;
+            color: $ios-text-primary;
+            margin-bottom: 4px;
           }
 
           .percentage {
             font-size: 12px;
             color: $ios-text-secondary;
-            margin-top: 2px;
           }
         }
 
         .col-status {
+          text-align: center;
+          position: relative;
+
           .status-badge {
             display: inline-block;
-            padding: 4px 8px;
+            padding: 4px 12px;
             border-radius: 12px;
             font-size: 12px;
             font-weight: $ios-font-weight-medium;
 
             &.pending {
-              background: rgba(255, 149, 0, 0.1);
-              color: #ff9500;
+              background: #fff3cd;
+              color: #856404;
             }
 
-            &.overdue {
-              background: rgba(220, 53, 69, 0.1);
-              color: #dc3545;
+            &.completed {
+              background: #d1edff;
+              color: #0c5460;
             }
 
-            &.completed {
-              background: rgba(40, 167, 69, 0.1);
-              color: #28a745;
+            &.overdue {
+              background: #f8d7da;
+              color: #721c24;
             }
           }
 
@@ -251,51 +274,182 @@
             font-size: 11px;
             color: #dc3545;
             margin-top: 4px;
-            font-weight: $ios-font-weight-medium;
+          }
+
+          .auto-process-btn {
+            position: absolute;
+            top: -8px;
+            right: -8px;
+            width: 24px;
+            height: 24px;
+
+            mat-icon {
+              font-size: 16px;
+              width: 16px;
+              height: 16px;
+            }
+          }
+        }
+
+        // 新增操作列样式
+        .col-actions {
+          .action-buttons {
+            display: flex;
+            flex-direction: column;
+            gap: 8px;
+            align-items: center;
+            width: 100%;
+
+            button {
+              min-width: 100px;
+              width: 100%;
+              height: 32px;
+              font-size: 12px;
+              border-radius: 16px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              gap: 4px;
+
+              mat-icon {
+                font-size: 16px;
+                width: 16px;
+                height: 16px;
+              }
+
+              mat-spinner {
+                margin-right: 4px;
+              }
+
+              &.acceptance-btn {
+                &[color="primary"] {
+                  background: #e3f2fd;
+                  color: #1976d2;
+                  border-color: #1976d2;
+                }
+              }
+
+              &.reminder-btn {
+                &[color="warn"] {
+                  background: #fff3e0;
+                  color: #f57c00;
+                  border-color: #f57c00;
+                }
+              }
+
+              &.wechat-send-btn {
+                background: linear-gradient(135deg, #07c160 0%, #00d4aa 100%);
+                color: white;
+                border: none;
+                font-weight: 500;
+
+                &:hover:not(:disabled) {
+                  background: linear-gradient(135deg, #06ad56 0%, #00c49a 100%);
+                }
+
+                &:disabled {
+                  opacity: 0.7;
+                }
+              }
+            }
           }
         }
 
         .col-date {
-          text-align: right;
+          text-align: center;
 
           .settled-date,
           .due-date {
             font-weight: $ios-font-weight-medium;
             color: $ios-text-primary;
+            margin-bottom: 4px;
           }
 
           .date-label {
-            font-size: 11px;
+            font-size: 12px;
             color: $ios-text-secondary;
-            margin-top: 2px;
           }
         }
       }
     }
-  }
 
-  // 空状态
-  .empty-state {
-    text-align: center;
-    padding: 40px 20px;
-    color: $ios-text-secondary;
+    .empty-state {
+      text-align: center;
+      padding: 60px 20px;
+      color: $ios-text-secondary;
 
-    .empty-icon {
-      font-size: 48px;
-      margin-bottom: 16px;
-      opacity: 0.5;
+      .empty-icon {
+        font-size: 48px;
+        margin-bottom: 16px;
+      }
+
+      .empty-title {
+        font-size: 18px;
+        font-weight: $ios-font-weight-medium;
+        color: $ios-text-primary;
+        margin-bottom: 8px;
+      }
+
+      .empty-desc {
+        font-size: 14px;
+      }
     }
+  }
+}
 
-    .empty-title {
-      font-size: 16px;
-      font-weight: $ios-font-weight-semibold;
-      color: $ios-text-primary;
-      margin-bottom: 8px;
+// 自动化控制头部样式
+.automation-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px;
+  margin-bottom: 20px;
+  color: white;
+  
+  .automation-title {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-weight: 600;
+    font-size: 16px;
+    
+    mat-icon {
+      font-size: 20px;
+      width: 20px;
+      height: 20px;
     }
+  }
+  
+  .automation-actions {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    
+    button {
+      border-radius: 20px;
+      font-weight: 500;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      
+      background: rgba(255, 255, 255, 0.2);
+      backdrop-filter: blur(10px);
+      border: 1px solid rgba(255, 255, 255, 0.3);
+      color: white;
+      
+      &:hover:not(:disabled) {
+        background: rgba(255, 255, 255, 0.3);
+      }
+      
+      &:disabled {
+        opacity: 0.6;
+      }
 
-    .empty-desc {
-      font-size: 14px;
-      line-height: 1.4;
+      mat-spinner {
+        margin-right: 4px;
+      }
     }
   }
 }
@@ -308,27 +462,38 @@
     }
 
     .filter-section {
+      flex-direction: column;
+      align-items: stretch;
+      gap: 12px;
+
+      .search-box {
+        max-width: none;
+      }
+
       .filter-buttons {
         justify-content: center;
+        flex-wrap: wrap;
       }
     }
 
-    .settlements-list {
+    .settlement-list {
       .list-header {
         display: none;
       }
 
       .list-body .settlement-item {
         grid-template-columns: 1fr;
-        gap: 8px;
+        gap: 12px;
 
-        .col-project,
-        .col-amount,
-        .col-status,
-        .col-date {
-          display: flex;
-          justify-content: space-between;
-          align-items: center;
+        .col-actions .action-buttons {
+          flex-direction: row;
+          justify-content: center;
+          flex-wrap: wrap;
+
+          button {
+            width: auto;
+            min-width: 100px;
+          }
         }
       }
     }

+ 354 - 4
src/app/shared/components/settlement-card/settlement-card.ts

@@ -1,7 +1,15 @@
-import { Component, Input, computed, signal } from '@angular/core';
-import { CommonModule } from '@angular/common';
+import { Component, Input, computed, signal, OnInit } from '@angular/core';
+import { CommonModule, DatePipe } from '@angular/common';
 import { FormsModule } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatDialogModule, MatDialog } from '@angular/material/dialog';
+import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
 import { Settlement } from '../../../models/project.model';
+import { AutoSettlementService } from '../../../services/auto-settlement.service';
+import { ProjectService } from '../../../services/project.service';
 
 export interface SettlementStats {
   totalAmount: number;
@@ -14,20 +22,322 @@ export interface SettlementStats {
   completedCount: number;
 }
 
+// 项目验收确认接口
+export interface ProjectAcceptance {
+  id: string;
+  projectId: string;
+  projectName: string;
+  technicianId: string;
+  technicianName: string;
+  acceptanceStatus: 'pending' | 'confirmed' | 'rejected';
+  acceptanceDate?: Date;
+  acceptanceNotes?: string;
+  customerNotified: boolean;
+}
+
+// 客服跟进提醒接口
+export interface CustomerServiceReminder {
+  id: string;
+  projectId: string;
+  projectName: string;
+  customerName: string;
+  reminderType: 'payment_follow_up' | 'acceptance_notification' | 'delivery_confirmation';
+  reminderStatus: 'pending' | 'in_progress' | 'completed';
+  assignedTo: string;
+  dueDate: Date;
+  notes?: string;
+  createdAt: Date;
+}
+
+// 企业微信群发图接口
+export interface WeChatImageDelivery {
+  id: string;
+  projectId: string;
+  groupId: string;
+  groupName: string;
+  imageUrls: string[];
+  deliveryStatus: 'pending' | 'sending' | 'completed' | 'failed';
+  deliveryDate?: Date;
+  errorMessage?: string;
+}
+
 @Component({
   selector: 'app-settlement-card',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [
+    CommonModule, 
+    DatePipe,
+    FormsModule,
+    MatButtonModule,
+    MatIconModule,
+    MatProgressSpinnerModule,
+    MatTooltipModule,
+    MatDialogModule,
+    MatSnackBarModule
+  ],
   templateUrl: './settlement-card.html',
   styleUrls: ['./settlement-card.scss']
 })
-export class SettlementCardComponent {
+export class SettlementCardComponent implements OnInit {
   @Input() settlements: Settlement[] = [];
+  @Input() showAutomationControls = false;
+  @Input() projectAcceptances: ProjectAcceptance[] = [];
+  @Input() customerServiceReminders: CustomerServiceReminder[] = [];
   
   // 筛选条件
   statusFilter = signal<string>('all');
   searchKeyword = signal<string>('');
   
+  // 自动化处理状态
+  isProcessing = signal(false);
+  processingSettlementId = signal<string | null>(null);
+  
+  // 新增功能状态
+  acceptanceStatus = signal<{[key: string]: 'pending' | 'confirming' | 'confirmed'}>({});
+  reminderStatus = signal<{[key: string]: 'pending' | 'creating' | 'created'}>({});
+  isSendingImages = signal(false);
+  sendingProjectId = signal<string | null>(null);
+
+  constructor(
+    private autoSettlementService: AutoSettlementService,
+    private dialog: MatDialog,
+    private snackBar: MatSnackBar,
+    private projectService: ProjectService
+  ) { }
+
+  ngOnInit() {
+    // 启动定时自动化处理
+    this.autoSettlementService.startScheduledProcessing();
+    
+    // 初始化项目验收状态
+    this.initializeAcceptanceStatus();
+    
+    // 初始化客服提醒状态
+    this.initializeReminderStatus();
+  }
+
+  // 初始化项目验收状态
+  private initializeAcceptanceStatus(): void {
+    const status: {[key: string]: 'pending' | 'confirming' | 'confirmed'} = {};
+    this.settlements.forEach(settlement => {
+      const acceptance = this.projectAcceptances.find(a => a.projectId === settlement.projectId);
+      if (acceptance) {
+        status[settlement.projectId] = acceptance.acceptanceStatus === 'confirmed' ? 'confirmed' : 'pending';
+      } else {
+        status[settlement.projectId] = 'pending';
+      }
+    });
+    this.acceptanceStatus.set(status);
+  }
+
+  // 初始化客服提醒状态
+  private initializeReminderStatus(): void {
+    const status: {[key: string]: 'pending' | 'creating' | 'created'} = {};
+    this.settlements.forEach(settlement => {
+      const reminder = this.customerServiceReminders.find(r => 
+        r.projectId === settlement.projectId && r.reminderType === 'payment_follow_up'
+      );
+      if (reminder) {
+        status[settlement.projectId] = reminder.reminderStatus === 'completed' ? 'created' : 'pending';
+      } else {
+        status[settlement.projectId] = 'pending';
+      }
+    });
+    this.reminderStatus.set(status);
+  }
+
+  // 技术确认项目验收
+  confirmProjectAcceptance(settlement: Settlement): void {
+    const currentStatus = this.acceptanceStatus();
+    currentStatus[settlement.projectId] = 'confirming';
+    this.acceptanceStatus.set({...currentStatus});
+
+    // 模拟API调用
+    setTimeout(() => {
+      const updatedStatus = this.acceptanceStatus();
+      updatedStatus[settlement.projectId] = 'confirmed';
+      this.acceptanceStatus.set({...updatedStatus});
+      
+      this.snackBar.open(`项目 ${settlement.projectName} 验收确认成功`, '关闭', {
+        duration: 3000,
+        horizontalPosition: 'center',
+        verticalPosition: 'top'
+      });
+
+      // 自动创建客服跟进提醒
+      this.createCustomerServiceReminder(settlement, 'acceptance_notification');
+
+      // 验收完成后,自动发起尾款结算申请
+      this.initiateFinalSettlement(settlement);
+    }, 1500);
+  }
+
+  // 创建客服跟进提醒
+  createCustomerServiceReminder(settlement: Settlement, type: 'payment_follow_up' | 'acceptance_notification' | 'delivery_confirmation'): void {
+    const currentStatus = this.reminderStatus();
+    currentStatus[settlement.projectId] = 'creating';
+    this.reminderStatus.set({...currentStatus});
+
+    // 模拟API调用创建提醒
+    setTimeout(() => {
+      const updatedStatus = this.reminderStatus();
+      updatedStatus[settlement.projectId] = 'created';
+      this.reminderStatus.set({...updatedStatus});
+      
+      let message = '';
+      switch(type) {
+        case 'payment_follow_up':
+          message = `已创建尾款跟进提醒`;
+          break;
+        case 'acceptance_notification':
+          message = `已创建验收通知提醒`;
+          break;
+        case 'delivery_confirmation':
+          message = `已创建交付确认提醒`;
+          break;
+      }
+      
+      this.snackBar.open(`${settlement.projectName} ${message}`, '关闭', {
+        duration: 3000,
+        horizontalPosition: 'center',
+        verticalPosition: 'top'
+      });
+    }, 1000);
+  }
+
+  // 一键发图到企业微信群
+  sendImagesToWeChatGroup(settlement: Settlement): void {
+    this.isSendingImages.set(true);
+    this.sendingProjectId.set(settlement.projectId);
+
+    // 模拟获取项目大图和发送到企业微信群
+    setTimeout(() => {
+      this.isSendingImages.set(false);
+      this.sendingProjectId.set(null);
+      
+      this.snackBar.open(`项目 ${settlement.projectName} 大图已发送到企业微信群`, '关闭', {
+        duration: 4000,
+        horizontalPosition: 'center',
+        verticalPosition: 'top'
+      });
+
+      // 自动创建交付确认提醒
+      this.createCustomerServiceReminder(settlement, 'delivery_confirmation');
+    }, 3000);
+  }
+
+  // 获取项目验收状态
+  getAcceptanceStatus(projectId: string): 'pending' | 'confirming' | 'confirmed' {
+    return this.acceptanceStatus()[projectId] || 'pending';
+  }
+
+  // 获取客服提醒状态
+  getReminderStatus(projectId: string): 'pending' | 'creating' | 'created' {
+    return this.reminderStatus()[projectId] || 'pending';
+  }
+
+  // 检查是否可以发送图片
+  canSendImages(settlement: Settlement): boolean {
+    return settlement.status === '已结算' && this.getAcceptanceStatus(settlement.projectId) === 'confirmed';
+  }
+
+  // 检查是否正在发送图片
+  isSendingImagesForProject(projectId: string): boolean {
+    return this.isSendingImages() && this.sendingProjectId() === projectId;
+  }
+
+  // 处理单个结算
+  processSettlement(settlementId: string): void {
+    const settlement = this.settlements.find(s => s.id === settlementId);
+    if (settlement) {
+      this.processSettlementAutomation(settlement);
+    }
+  }
+
+  // 发送提醒
+  sendReminder(settlementId: string): void {
+    const settlement = this.settlements.find(s => s.id === settlementId);
+    if (settlement) {
+      this.createCustomerServiceReminder(settlement, 'payment_follow_up');
+    }
+  }
+
+  // 处理单个结算自动化
+  processSettlementAutomation(settlement: Settlement): void {
+    this.processingSettlementId.set(settlement.id);
+    this.isProcessing.set(true);
+    
+    this.autoSettlementService.processSettlementAutomation(settlement).subscribe({
+      next: (processed) => {
+        if (processed) {
+          console.log(`结算 ${settlement.id} 已自动处理`);
+        }
+        this.processingSettlementId.set(null);
+        this.isProcessing.set(false);
+      },
+      error: (error) => {
+        console.error('自动化处理失败:', error);
+        this.processingSettlementId.set(null);
+        this.isProcessing.set(false);
+      }
+    });
+  }
+
+  // 项目验收确认相关方法
+  isProjectAccepted(projectId: string): boolean {
+    return this.projectAcceptances.some(acceptance => 
+      acceptance.projectId === projectId && acceptance.acceptanceStatus === 'confirmed'
+    );
+  }
+
+  // 客服跟进提醒相关方法
+  hasCustomerServiceReminder(projectId: string): boolean {
+    return this.customerServiceReminders.some(reminder => 
+      reminder.projectId === projectId && reminder.reminderStatus === 'completed'
+    );
+  }
+
+  // 企业微信发图相关方法
+  canSendToWeChat(projectId: string): boolean {
+    return this.isProjectAccepted(projectId) && !this.isSendingImagesForProject(projectId);
+  }
+
+  // 批量处理自动化
+  processAllAutomation(): void {
+    const pendingSettlements = this.settlements.filter(
+      s => s.status === '待结算' && !this.isOverdue(s)
+    );
+    
+    this.isProcessing.set(true);
+    
+    // 模拟批量处理
+    let processedCount = 0;
+    const processNext = () => {
+      if (processedCount >= pendingSettlements.length) {
+        this.isProcessing.set(false);
+        return;
+      }
+      
+      const settlement = pendingSettlements[processedCount];
+      this.processingSettlementId.set(settlement.id);
+      
+      this.autoSettlementService.processSettlementAutomation(settlement).subscribe({
+        next: () => {
+          processedCount++;
+          this.processingSettlementId.set(null);
+          setTimeout(processNext, 500); // 添加延迟以避免同时处理过多
+        },
+        error: () => {
+          processedCount++;
+          this.processingSettlementId.set(null);
+          setTimeout(processNext, 500);
+        }
+      });
+    };
+    
+    processNext();
+  }
+  
   // 计算统计数据
   stats = computed<SettlementStats>(() => {
     const settlements = this.settlements || [];
@@ -115,4 +425,44 @@ export class SettlementCardComponent {
     const diffTime = today.getTime() - thirtyDaysAgo.getTime();
     return Math.max(0, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
   }
+  
+  // 验收完成后自动发起尾款结算申请
+  private initiateFinalSettlement(contextSettlement: Settlement): void {
+    // 查找当前项目的“尾款结算”记录
+    this.projectService.getSettlements().subscribe(settlements => {
+      const finalSettlement = settlements.find(s => s.projectId === contextSettlement.projectId && s.stage === '尾款结算');
+  
+      if (finalSettlement) {
+        // 标记为已发起(更新时间)并触发自动化处理
+        finalSettlement.createdAt = new Date();
+        this.snackBar.open(`已自动发起尾款结算申请:${finalSettlement.projectName}`, '关闭', {
+          duration: 3000,
+          horizontalPosition: 'center',
+          verticalPosition: 'top'
+        });
+        // 触发自动化处理(提醒/折扣/自动确认等)
+        this.processSettlementAutomation(finalSettlement);
+      } else {
+        // 若不存在尾款结算记录,则创建一条默认记录并发起
+        const newSettlement: Settlement = {
+          id: `s${Date.now()}`,
+          projectId: contextSettlement.projectId,
+          projectName: contextSettlement.projectName,
+          stage: '尾款结算',
+          amount: 0,
+          percentage: 100,
+          status: '待结算',
+          createdAt: new Date()
+        };
+        this.projectService.addSettlement(newSettlement).subscribe(() => {
+          this.snackBar.open(`已创建并自动发起尾款结算申请:${newSettlement.projectName}`, '关闭', {
+            duration: 3000,
+            horizontalPosition: 'center',
+            verticalPosition: 'top'
+          });
+          this.processSettlementAutomation(newSettlement);
+        });
+      }
+    });
+  }
 }

+ 24 - 0
src/app/shared/components/star-rating/star-rating.component.html

@@ -0,0 +1,24 @@
+<div class="star-rating" [class.readonly]="readonly" [class.size-small]="size === 'small'" [class.size-medium]="size === 'medium'" [class.size-large]="size === 'large'">
+  <div class="stars-container">
+    <span 
+      *ngFor="let star of stars; let i = index"
+      class="star"
+      [class.filled]="star.filled"
+      [class.hovered]="star.hovered"
+      (click)="setRating(star.index)"
+      (mouseenter)="setHoverRating(star.index)"
+      (mouseleave)="clearHover()"
+      [title]="star.index + '星'"
+    >
+      ★
+    </span>
+  </div>
+  
+  <div class="rating-text" *ngIf="showText && rating > 0">
+    {{ rating }}星{{ getRatingText() ? ' - ' + getRatingText() : '' }}
+  </div>
+  
+  <div class="rating-text" *ngIf="showText && rating === 0 && !readonly">
+    请评分
+  </div>
+</div>

+ 133 - 0
src/app/shared/components/star-rating/star-rating.component.scss

@@ -0,0 +1,133 @@
+.star-rating {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+
+  &.readonly {
+    .star {
+      cursor: default;
+      
+      &:hover {
+        transform: none;
+      }
+    }
+  }
+
+  // 尺寸变体
+  &.size-small {
+    .star {
+      font-size: 18px;
+      width: 24px;
+      height: 24px;
+    }
+    
+    .rating-text {
+      font-size: 12px;
+    }
+  }
+
+  &.size-medium {
+    .star {
+      font-size: 24px;
+      width: 32px;
+      height: 32px;
+    }
+    
+    .rating-text {
+      font-size: 14px;
+    }
+  }
+
+  &.size-large {
+    .star {
+      font-size: 32px;
+      width: 40px;
+      height: 40px;
+    }
+    
+    .rating-text {
+      font-size: 16px;
+    }
+  }
+
+  .stars-container {
+    display: flex;
+    gap: 4px;
+  }
+
+  .star {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    color: #e8e8e8;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    user-select: none;
+    border-radius: 3px;
+    
+    &:hover {
+      transform: scale(1.1);
+      color: #ffc53d !important;
+    }
+
+    &.filled {
+      color: #ffc53d;
+    }
+
+    &.hovered {
+      color: #ffc53d;
+      opacity: 0.7;
+    }
+  }
+
+  .rating-text {
+    color: #595959;
+    font-weight: 500;
+    text-align: center;
+    min-height: 20px;
+  }
+}
+
+// 暗色主题支持
+@media (prefers-color-scheme: dark) {
+  .star-rating {
+    .star {
+      color: #434343;
+      
+      &.filled {
+        color: #ffc53d;
+      }
+      
+      &.hovered {
+        color: #ffc53d;
+        opacity: 0.7;
+      }
+    }
+    
+    .rating-text {
+      color: #bfbfbf;
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 480px) {
+  .star-rating {
+    &.size-medium {
+      .star {
+        font-size: 20px;
+        width: 28px;
+        height: 28px;
+      }
+    }
+    
+    &.size-large {
+      .star {
+        font-size: 24px;
+        width: 32px;
+        height: 32px;
+      }
+    }
+  }
+}

+ 104 - 0
src/app/shared/components/star-rating/star-rating.component.ts

@@ -0,0 +1,104 @@
+import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+
+@Component({
+  selector: 'app-star-rating',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './star-rating.component.html',
+  styleUrls: ['./star-rating.component.scss'],
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => StarRatingComponent),
+      multi: true
+    }
+  ]
+})
+export class StarRatingComponent implements ControlValueAccessor {
+  @Input() rating: number = 0;
+  @Input() maxStars: number = 5;
+  @Input() size: 'small' | 'medium' | 'large' = 'medium';
+  @Input() readonly: boolean = false;
+  @Input() showText: boolean = true;
+  
+  @Output() ratingChange = new EventEmitter<number>();
+
+  stars: { index: number; filled: boolean; hovered: boolean; }[] = [];
+  hoverRating: number = 0;
+  
+  private onChange: (value: number) => void = () => {};
+  private onTouched: () => void = () => {};
+
+  ngOnInit() {
+    this.updateStars();
+  }
+
+  ngOnChanges() {
+    this.updateStars();
+  }
+
+  updateStars() {
+    this.stars = Array.from({ length: this.maxStars }, (_, index) => ({
+      index: index + 1,
+      filled: index < this.rating,
+      hovered: index < this.hoverRating
+    }));
+  }
+
+  setRating(rating: number) {
+    if (this.readonly) return;
+    
+    this.rating = rating;
+    this.hoverRating = 0;
+    this.updateStars();
+    this.ratingChange.emit(rating);
+    this.onChange(rating);
+    this.onTouched();
+  }
+
+  setHoverRating(rating: number) {
+    if (this.readonly) return;
+    
+    this.hoverRating = rating;
+    this.updateStars();
+  }
+
+  clearHover() {
+    if (this.readonly) return;
+    
+    this.hoverRating = 0;
+    this.updateStars();
+  }
+
+  getRatingText(): string {
+    const texts = [
+      '非常差',
+      '差',
+      '一般',
+      '好',
+      '非常好'
+    ];
+    
+    return texts[Math.floor(this.rating) - 1] || '';
+  }
+
+  // ControlValueAccessor implementation
+  writeValue(value: number): void {
+    this.rating = value || 0;
+    this.updateStars();
+  }
+
+  registerOnChange(fn: (value: number) => void): void {
+    this.onChange = fn;
+  }
+
+  registerOnTouched(fn: () => void): void {
+    this.onTouched = fn;
+  }
+
+  setDisabledState?(isDisabled: boolean): void {
+    this.readonly = isDisabled;
+  }
+}

+ 1 - 2
src/index.html

@@ -6,8 +6,7 @@
     <base href="/">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <link rel="icon" type="image/x-icon" href="favicon.ico">
-    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
-    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+    <!-- 使用系统字体替代Google Fonts以避免网络错误 -->
     <!-- Local import map to resolve bare specifier 'echarts' to local ESM file -->
     <script type="importmap">
       {

+ 8 - 3
src/styles.scss

@@ -38,14 +38,19 @@ body {
 
   // Reset the user agent margin.
   margin: 0;
+  
+  // 使用系统字体栈替代Google Fonts
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 }
-/* 从本地npm包引入Roboto字体,避免网络请求 */
-@import 'roboto-fontface/css/roboto/roboto-fontface.css';
+/* 使用系统字体栈替代Google Fonts,避免网络请求错误 */
 
 /* You can add global styles to this file, and also import other style files */
 
 html, body { height: 100%; }
-body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+body { 
+  margin: 0; 
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
 
 
 /* HR Dialog Global Styles - 重新设计解决背景色重合问题 */

+ 3 - 0
temp.txt

@@ -0,0 +1,3 @@
+"}"
+"} @endfor"
+"}"

Некоторые файлы не были показаны из-за большого количества измененных файлов