项目-订单分配.md 35 KB

项目管理 - 订单分配阶段 PRD

1. 功能概述

1.1 阶段定位

订单分配阶段是项目管理流程的第一个环节,主要负责将客户咨询转化为正式项目订单,并完成设计师团队的初步分配。该阶段是连接客服端和设计师端的关键桥梁。

1.2 核心目标

  • 完成客户信息的结构化录入和同步
  • 确定项目报价和付款条件
  • 匹配并分配合适的设计师资源
  • 建立项目基础档案,为后续环节提供数据支撑

1.3 涉及角色

  • 客服人员:负责创建订单、录入客户信息、初步需求沟通
  • 设计师:接收订单分配、查看项目基础信息
  • 组长:查看团队订单分配情况、协调设计师资源

2. 核心功能模块

2.1 客户信息管理

2.1.1 信息展示卡片

位置:订单分配阶段左侧面板

展示内容

interface CustomerInfoDisplay {
  // 基础信息
  name: string;           // 客户姓名
  phone: string;          // 联系电话
  wechat?: string;        // 微信号
  customerType: string;   // 客户类型:新客户/老客户/VIP客户
  source: string;         // 来源:小程序/官网咨询/推荐介绍
  remark?: string;        // 备注信息

  // 状态信息
  syncStatus: 'syncing' | 'synced' | 'error'; // 同步状态
  lastSyncTime?: Date;    // 最后同步时间

  // 需求标签
  demandType?: string;    // 需求类型:价格敏感/质量敏感/综合要求
  followUpStatus?: string; // 跟进状态:待报价/待确认需求/已失联
  preferenceTags?: string[]; // 偏好标签数组
}

数据来源

  1. 客服端同步:通过路由查询参数 syncData 传递客户信息

    // 客服端跳转示例
    router.navigate(['/designer/project-detail', projectId], {
     queryParams: {
       syncData: JSON.stringify({
         customerInfo: {...},
         requirementInfo: {...},
         preferenceTags: [...]
       })
     }
    });
    
  2. 实时同步机制

    • 每30秒自动同步一次客户信息
    • 显示同步状态指示器(同步中/已同步/同步失败)
    • 支持手动触发同步

交互特性

  • 卡片可展开/收起(isCustomerInfoExpanded
  • 展开时显示完整客户信息和标签
  • 收起时仅显示客户姓名和联系方式
  • 同步状态实时更新,显示"刚刚/X分钟前/X小时前"

2.1.2 客户搜索功能

适用场景:手动创建订单时快速选择已有客户

搜索逻辑

searchCustomer(): void {
  // 至少输入2个字符才触发搜索
  if (this.customerSearchKeyword.trim().length >= 2) {
    // 模糊搜索客户姓名、手机号、微信号
    this.customerSearchResults = this.customerService.search({
      keyword: this.customerSearchKeyword,
      fields: ['name', 'phone', 'wechat']
    });
  }
}

搜索结果展示

  • 下拉列表形式
  • 每项显示:客户姓名、电话(脱敏)、客户类型、来源
  • 点击选择后自动填充表单

2.2 核心需求表单

2.2.1 必填项配置

表单定义

orderCreationForm = this.fb.group({
  orderAmount: ['', [Validators.required, Validators.min(0)]],
  smallImageDeliveryTime: ['', Validators.required],
  decorationType: ['', Validators.required],
  requirementReason: ['', Validators.required],
  isMultiDesigner: [false]
});

字段详解

字段名 类型 验证规则 说明 UI组件
orderAmount number required, min(0) 订单金额,单位:元 数字输入框,支持千分位格式化
smallImageDeliveryTime Date required 小图交付时间 日期选择器,限制最早日期为今天
decorationType string required 装修类型:全包/半包/清包/软装 下拉选择框
requirementReason string required 需求原因:新房装修/旧房改造/局部翻新 单选框组
isMultiDesigner boolean - 是否需要多设计师协作 复选框

表单验证提示

  • 实时验证:失焦时触发
  • 错误提示:红色边框 + 底部错误文字
  • 提交验证:点击"分配订单"按钮时调用 markAllAsTouched() 显示所有错误

2.2.2 可选信息表单

折叠面板设计

<div class="optional-info-section">
  <div class="section-header" (click)="isOptionalFormExpanded = !isOptionalFormExpanded">
    <span>可选信息</span>
    <span class="toggle-icon">{{ isOptionalFormExpanded ? '▼' : '▶' }}</span>
  </div>
  @if (isOptionalFormExpanded) {
    <div class="section-content">
      <!-- 可选字段表单 -->
    </div>
  }
</div>

可选字段

optionalForm = this.fb.group({
  largeImageDeliveryTime: [''],      // 大图交付时间
  spaceRequirements: [''],           // 空间需求描述
  designAngles: [''],                // 设计角度要求
  specialAreaHandling: [''],         // 特殊区域处理说明
  materialRequirements: [''],        // 材质要求
  lightingRequirements: ['']         // 光照需求
});

2.3 报价明细组件

2.3.1 组件集成

组件标签

<app-quotation-details
  [initialData]="quotationData"
  [readonly]="isReadOnly()"
  (dataChange)="onQuotationDataChange($event)">
</app-quotation-details>

数据结构

interface QuotationData {
  items: Array<{
    id: string;
    room: string;           // 空间名称:客餐厅/主卧/次卧/厨房/卫生间
    amount: number;         // 金额
    description?: string;   // 描述
  }>;
  totalAmount: number;      // 总金额
  materialCost: number;     // 材料费
  laborCost: number;        // 人工费
  designFee: number;        // 设计费
  managementFee: number;    // 管理费
}

2.3.2 组件功能

  1. 报价项管理

    • 添加报价项:按空间/按项目添加
    • 删除报价项:确认后删除
    • 编辑金额:实时更新总金额
  2. AI辅助生成(可选功能):

    generateQuotationDetails(): void {
     // 基于项目信息自动生成报价明细
     const rooms = ['客餐厅', '主卧', '次卧', '厨房', '卫生间'];
     this.quotationDetails = rooms.map((room, index) => ({
       id: `quote_${index + 1}`,
       room: room,
       amount: Math.floor(Math.random() * 1000) + 300,
       description: `${room}装修设计费用`
     }));
     this.orderAmount = this.quotationDetails.reduce((total, item) => total + item.amount, 0);
    }
    
  3. 金额自动汇总

    • 实时计算总金额
    • 费用分类占比显示
    • 支持导出报价单(PDF/Excel)

2.4 设计师指派组件

2.4.1 组件集成

组件标签

<app-designer-assignment
  [projectData]="projectData"
  [readonly]="isReadOnly()"
  (assignmentChange)="onDesignerAssignmentChange($event)"
  (designerClick)="onDesignerClick($event)">
</app-designer-assignment>

数据结构

interface DesignerAssignmentData {
  selectedDesigners: Designer[];
  teamId: string;
  teamName: string;
  leaderId: string;
  assignmentDate: Date;
  expectedStartDate: Date;
}

interface Designer {
  id: string;
  name: string;
  avatar: string;
  teamId: string;
  teamName: string;
  isTeamLeader: boolean;
  status: 'idle' | 'busy' | 'unavailable';
  currentProjects: number;
  skillMatch: number;        // 技能匹配度 0-100
  recentOrders: number;      // 近期订单数
  idleDays: number;          // 闲置天数
  workload: number;          // 工作负荷 0-100
  reviewDates: string[];     // 对图评审日期列表
}

2.4.2 设计师选择逻辑

智能推荐算法

calculateDesignerScore(designer: Designer): number {
  let score = 0;

  // 1. 技能匹配度(权重40%)
  score += designer.skillMatch * 0.4;

  // 2. 工作负荷(权重30%,负荷越低分数越高)
  score += (100 - designer.workload) * 0.3;

  // 3. 闲置时间(权重20%,闲置越久分数越高)
  score += Math.min(designer.idleDays * 2, 100) * 0.2;

  // 4. 近期接单数(权重10%,接单越少分数越高)
  score += Math.max(0, 100 - designer.recentOrders * 10) * 0.1;

  return score;
}

设计师列表展示

  • 卡片网格布局,每行3-4个设计师卡片
  • 卡片信息:头像、姓名、团队、状态标签、技能匹配度进度条
  • 状态颜色:
    • idle 空闲 - 绿色
    • busy 繁忙 - 橙色
    • unavailable 不可用 - 灰色
  • 点击卡片可查看设计师详细日历

2.4.3 设计师日历弹窗

触发条件:点击设计师卡片时

弹窗内容

<app-designer-calendar
  [designers]="[selectedDesigner]"
  [groups]="calendarGroups"
  [selectedDate]="selectedCalendarDate"
  (designerSelected)="onCalendarDesignerSelected($event)"
  (assignmentRequested)="onCalendarAssignmentRequested($event)">
</app-designer-calendar>

日历功能

  • 月视图展示设计师日程
  • 标记对图评审日期(红点)
  • 显示已分配项目时间段
  • 计算下一个可用日期
  • 支持按日期筛选空闲设计师

2.5 下单时间自动生成

实现逻辑

ngOnInit(): void {
  // 自动生成下单时间
  this.orderTime = new Date().toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
}

显示格式2025-10-16 14:30:25

用途

  • 记录订单创建的准确时间
  • 作为项目开始时间的参考
  • 用于计算项目周期和延期预警

3. 数据流转

3.1 客服端同步流程

sequenceDiagram
    participant CS as 客服端
    participant Route as 路由
    participant PD as 项目详情页
    participant Form as 订单表单

    CS->>Route: navigate with syncData
    Route->>PD: queryParams.syncData
    PD->>PD: parseJSON(syncData)
    PD->>Form: patchValue(customerInfo)
    PD->>PD: syncRequirementKeyInfo(requirementInfo)
    PD->>PD: 更新 projectData
    PD->>PD: 触发 cdr.detectChanges()
    PD-->>CS: 同步完成

关键代码实现

// project-detail.ts lines 741-816
this.route.queryParamMap.subscribe({
  next: (qp) => {
    const syncDataParam = qp.get('syncData');
    if (syncDataParam) {
      try {
        const syncData = JSON.parse(syncDataParam);

        // 设置同步状态
        this.isSyncingCustomerInfo = true;

        // 存储订单分配数据用于显示
        this.orderCreationData = syncData;

        // 同步客户信息到表单
        if (syncData.customerInfo) {
          this.customerForm.patchValue({
            name: syncData.customerInfo.name || '',
            phone: syncData.customerInfo.phone || '',
            wechat: syncData.customerInfo.wechat || '',
            customerType: syncData.customerInfo.customerType || '新客户',
            source: syncData.customerInfo.source || '小程序',
            remark: syncData.customerInfo.remark || ''
          });
        }

        // 同步需求信息
        if (syncData.requirementInfo) {
          this.syncRequirementKeyInfo(syncData.requirementInfo);
        }

        // 同步偏好标签
        if (syncData.preferenceTags) {
          this.project.customerTags = syncData.preferenceTags;
        }

        // 模拟同步完成
        setTimeout(() => {
          this.isSyncingCustomerInfo = false;
          this.lastSyncTime = new Date();
          this.cdr.detectChanges();
        }, 1500);

      } catch (error) {
        console.error('解析同步数据失败:', error);
        this.isSyncingCustomerInfo = false;
      }
    }
  }
});

3.2 订单创建流程

flowchart TD
    A[客服/设计师填写表单] --> B{表单验证}
    B -->|验证失败| C[显示错误提示]
    C --> A
    B -->|验证成功| D[调用 createOrder]
    D --> E[整合表单数据]
    E --> F[整合报价数据]
    F --> G[整合设计师分配数据]
    G --> H[调用 ProjectService.createProject]
    H --> I{API响应}
    I -->|成功| J[显示成功提示]
    J --> K[推进到需求沟通阶段]
    K --> L[展开需求沟通面板]
    L --> M[滚动到需求沟通区域]
    I -->|失败| N[显示错误信息]

关键方法实现

// project-detail.ts lines 4783-4808
createOrder(): void {
  if (!this.canCreateOrder()) {
    // 标记所有字段为已触摸,以显示验证错误
    this.orderCreationForm.markAllAsTouched();
    return;
  }

  const orderData = {
    ...this.orderCreationForm.value,
    ...this.optionalForm.value,
    customerInfo: this.orderCreationData?.customerInfo,
    quotationData: this.quotationData,
    designerAssignment: this.designerAssignmentData
  };

  console.log('分配订单:', orderData);

  // 调用 ProjectService 创建项目
  this.projectService.createProject(orderData).subscribe({
    next: (result) => {
      if (result.success) {
        alert('订单分配成功!');
        // 订单分配成功后自动切换到下一环节
        this.advanceToNextStage('订单分配');
      }
    },
    error: (error) => {
      console.error('订单分配失败:', error);
      alert('订单分配失败,请重试');
    }
  });
}

3.3 阶段推进机制

推进触发条件

  • 订单分配完成(必填项填写 + 报价确认 + 设计师分配)
  • 点击"分配订单"按钮并验证通过
  • 或者通过 onProjectCreated 事件自动推进

推进逻辑

// project-detail.ts lines 1391-1423
advanceToNextStage(afterStage: ProjectStage): void {
  const idx = this.stageOrder.indexOf(afterStage);
  if (idx >= 0 && idx < this.stageOrder.length - 1) {
    const next = this.stageOrder[idx + 1];

    // 更新项目阶段
    this.updateProjectStage(next);

    // 更新展开状态,折叠当前、展开下一阶段
    this.expandedStages[afterStage] = false;
    this.expandedStages[next] = true;

    // 更新板块展开状态
    const nextSection = this.getSectionKeyForStage(next);
    this.expandedSection = nextSection;

    // 触发变更检测以更新导航栏颜色
    this.cdr.detectChanges();
  }
}

// project-detail.ts lines 3038-3069
onProjectCreated(projectData: any): void {
  console.log('项目创建完成:', projectData);
  this.projectData = projectData;

  // 团队分配已在子组件中完成并触发该事件:推进到需求沟通阶段
  this.updateProjectStage('需求沟通');

  // 更新项目对象的当前阶段,确保四大板块状态正确显示
  if (this.project) {
    this.project.currentStage = '需求沟通';
  }

  // 展开需求沟通阶段,收起订单分配阶段
  this.expandedStages['需求沟通'] = true;
  this.expandedStages['订单分配'] = false;

  // 自动展开确认需求板块
  this.expandedSection = 'requirements';

  // 强制触发变更检测,确保UI更新
  this.cdr.detectChanges();

  // 延迟滚动到需求沟通阶段,确保DOM更新完成
  setTimeout(() => {
    this.scrollToStage('需求沟通');
    this.cdr.detectChanges();
  }, 100);
}

4. 权限控制

4.1 角色权限矩阵

操作 客服 设计师 组长 技术
查看订单分配阶段
创建订单
编辑客户信息
填写报价明细
选择设计师
接收订单通知
查看设计师日历
推进到下一阶段

4.2 权限检查方法

// project-detail.ts lines 890-936
private detectRoleContextFromUrl(): 'customer-service' | 'designer' | 'team-leader' | 'technical' {
  const url = this.router.url || '';

  // 首先检查查询参数中的role
  const queryParams = this.route.snapshot.queryParamMap;
  const roleParam = queryParams.get('roleName');
  if (roleParam === 'customer-service') {
    return 'customer-service';
  }
  if (roleParam === 'technical') {
    return 'technical';
  }

  // 如果没有role查询参数,则根据URL路径判断
  if (url.includes('/customer-service/')) return 'customer-service';
  if (url.includes('/team-leader/')) return 'team-leader';
  if (url.includes('/technical/')) return 'technical';
  return 'designer';
}

isDesignerView(): boolean { return this.roleContext === 'designer'; }
isTeamLeaderView(): boolean { return this.roleContext === 'team-leader'; }
isCustomerServiceView(): boolean { return this.roleContext === 'customer-service'; }
isTechnicalView(): boolean { return this.roleContext === 'technical'; }
isReadOnly(): boolean { return this.isCustomerServiceView(); }

canEditSection(sectionKey: SectionKey): boolean {
  if (this.isCustomerServiceView()) {
    return sectionKey === 'order' || sectionKey === 'requirements' || sectionKey === 'aftercare';
  }
  return true; // 设计师和组长可以编辑所有板块
}

canEditStage(stage: ProjectStage): boolean {
  if (this.isCustomerServiceView()) {
    const editableStages: ProjectStage[] = [
      '订单分配', '需求沟通', '方案确认', // 订单分配和确认需求板块
      '尾款结算', '客户评价', '投诉处理' // 售后板块
    ];
    return editableStages.includes(stage);
  }
  return true; // 设计师和组长可以编辑所有阶段
}

4.3 UI权限控制

模板权限指令

<!-- 只读模式:客服查看设计师视角 -->
@if (isReadOnly()) {
  <div class="readonly-banner">
    当前为只读模式,您可以查看项目信息但无法编辑
  </div>
}

<!-- 编辑权限检查:表单禁用状态 -->
<app-quotation-details
  [readonly]="!canEditSection('order')"
  ...>
</app-quotation-details>

<!-- 按钮显示控制:只有客服和组长可以看到分配订单按钮 -->
@if (canEditSection('order')) {
  <button
    class="btn-primary"
    [disabled]="!canCreateOrder()"
    (click)="createOrder()">
    分配订单
  </button>
}

5. 关键交互设计

5.1 表单验证交互

实时验证

// 失焦时触发验证
<input
  formControlName="orderAmount"
  (blur)="orderCreationForm.get('orderAmount')?.markAsTouched()"
  ...>

// 错误信息显示
@if (orderCreationForm.get('orderAmount')?.invalid &&
      orderCreationForm.get('orderAmount')?.touched) {
  <div class="error-message">
    @if (orderCreationForm.get('orderAmount')?.errors?.['required']) {
      订单金额不能为空
    }
    @if (orderCreationForm.get('orderAmount')?.errors?.['min']) {
      订单金额不能小于0
    }
  </div>
}

提交验证

createOrder(): void {
  if (!this.canCreateOrder()) {
    // 标记所有字段为已触摸,显示所有验证错误
    this.orderCreationForm.markAllAsTouched();

    // 滚动到第一个错误字段
    const firstError = document.querySelector('.error-message');
    if (firstError) {
      firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
    return;
  }
  // ... 继续提交逻辑
}

5.2 按钮状态控制

分配订单按钮状态

canCreateOrder(): boolean {
  // 检查必填表单是否有效
  const formValid = this.orderCreationForm?.valid;

  // 检查是否已选择设计师
  const designerAssigned = this.designerAssignmentData?.selectedDesigners?.length > 0;

  // 检查是否已填写报价明细
  const quotationFilled = this.quotationData?.items?.length > 0 && this.quotationData?.totalAmount > 0;

  return formValid && designerAssigned && quotationFilled;
}

按钮样式

<button
  class="btn-primary"
  [class.disabled]="!canCreateOrder()"
  [disabled]="!canCreateOrder()"
  (click)="createOrder()">
  @if (isSubmitting) {
    <span class="spinner"></span>
    分配中...
  } @else {
    分配订单
  }
</button>

5.3 阶段推进动画

滚动动画

// project-detail.ts lines 2253-2260
scrollToStage(stage: ProjectStage): void {
  const anchor = this.stageToAnchor(stage);
  const el = document.getElementById(anchor);
  if (el) {
    el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  }
}

展开动画

.stage-content {
  transition: max-height 0.3s ease-in-out, opacity 0.2s ease-in-out;

  &.collapsed {
    max-height: 0;
    opacity: 0;
    overflow: hidden;
  }

  &.expanded {
    max-height: 5000px;
    opacity: 1;
  }
}

6. 组件依赖关系

6.1 父子组件通信

graph TD
    A[ProjectDetail 父组件] --> B[QuotationDetailsComponent]
    A --> C[DesignerAssignmentComponent]
    A --> D[DesignerCalendarComponent]

    B -->|dataChange| A
    C -->|assignmentChange| A
    C -->|designerClick| A
    D -->|designerSelected| A
    D -->|assignmentRequested| A

6.2 Input/Output 接口

QuotationDetailsComponent

@Component({
  selector: 'app-quotation-details',
  ...
})
export class QuotationDetailsComponent {
  @Input() initialData?: QuotationData;
  @Input() readonly: boolean = false;
  @Output() dataChange = new EventEmitter<QuotationData>();

  // 当报价数据变化时触发
  onDataChange(): void {
    this.dataChange.emit(this.quotationData);
  }
}

DesignerAssignmentComponent

@Component({
  selector: 'app-designer-assignment',
  ...
})
export class DesignerAssignmentComponent {
  @Input() projectData?: any;
  @Input() readonly: boolean = false;
  @Output() assignmentChange = new EventEmitter<DesignerAssignmentData>();
  @Output() designerClick = new EventEmitter<Designer>();

  // 当设计师分配变化时触发
  onAssignmentChange(): void {
    this.assignmentChange.emit(this.assignmentData);
  }

  // 当点击设计师卡片时触发
  onDesignerCardClick(designer: Designer): void {
    this.designerClick.emit(designer);
  }
}

父组件处理

// project-detail.ts lines 2843-2873
onQuotationDataChange(data: QuotationData): void {
  this.quotationData = { ...data };
  this.orderAmount = data.totalAmount || 0;
}

onDesignerAssignmentChange(data: DesignerAssignmentData): void {
  this.designerAssignmentData = { ...data };
}

onDesignerClick(designer: AssignmentDesigner): void {
  // 映射为日历组件需要的数据格式
  const mapped = this.mapAssignmentDesignerToCalendar(designer);
  this.calendarDesigners = [mapped];
  this.calendarGroups = [{
    id: designer.teamId,
    name: designer.teamName,
    leaderId: designer.id,
    memberIds: [designer.id]
  }];
  this.selectedCalendarDate = new Date();
  this.showDesignerCalendar = true;
}

7. API集成

7.1 项目创建接口

接口地址POST /api/projects

请求参数

interface CreateProjectRequest {
  // 客户信息
  customerId: string;
  customerName: string;
  customerPhone: string;
  customerWechat?: string;
  customerType: string;
  customerSource: string;
  customerRemark?: string;

  // 订单信息
  orderAmount: number;
  smallImageDeliveryTime: Date;
  largeImageDeliveryTime?: Date;
  decorationType: string;
  requirementReason: string;
  isMultiDesigner: boolean;

  // 空间需求(可选)
  spaceRequirements?: string;
  designAngles?: string;
  specialAreaHandling?: string;
  materialRequirements?: string;
  lightingRequirements?: string;

  // 报价明细
  quotation: {
    items: Array<{
      room: string;
      amount: number;
      description?: string;
    }>;
    totalAmount: number;
    materialCost: number;
    laborCost: number;
    designFee: number;
    managementFee: number;
  };

  // 设计师分配
  assignment: {
    designerIds: string[];
    teamId: string;
    leaderId: string;
    assignmentDate: Date;
    expectedStartDate: Date;
  };

  // 偏好标签
  preferenceTags?: string[];
}

响应数据

interface CreateProjectResponse {
  success: boolean;
  message: string;
  projectId: string;
  project: {
    id: string;
    name: string;
    currentStage: ProjectStage;
    createdAt: Date;
    assignedDesigners: string[];
  };
}

7.2 设计师列表接口

接口地址GET /api/designers/available

查询参数

interface DesignerQueryParams {
  projectType?: string;      // 项目类型,用于技能匹配
  requiredSkills?: string[]; // 必需技能
  startDate?: Date;          // 项目预计开始日期
  teamId?: string;           // 指定团队ID
  sortBy?: 'skillMatch' | 'workload' | 'idleDays'; // 排序方式
}

响应数据

interface DesignerListResponse {
  success: boolean;
  designers: Designer[];
  recommendedDesignerId?: string; // AI推荐的最佳设计师
}

8. 异常处理

8.1 表单验证失败

createOrder(): void {
  if (!this.canCreateOrder()) {
    // 1. 标记所有字段为已触摸
    this.orderCreationForm.markAllAsTouched();

    // 2. 收集所有错误信息
    const errors: string[] = [];
    Object.keys(this.orderCreationForm.controls).forEach(key => {
      const control = this.orderCreationForm.get(key);
      if (control?.invalid) {
        errors.push(this.getFieldLabel(key));
      }
    });

    // 3. 显示错误提示
    alert(`请完善以下必填项:\n${errors.join('\n')}`);

    // 4. 滚动到第一个错误字段
    const firstError = document.querySelector('.error-message');
    firstError?.scrollIntoView({ behavior: 'smooth', block: 'center' });

    return;
  }
  // ... 继续提交
}

8.2 API调用失败

this.projectService.createProject(orderData).subscribe({
  next: (result) => {
    if (result.success) {
      alert('订单分配成功!');
      this.advanceToNextStage('订单分配');
    } else {
      // 服务端返回失败
      alert(`订单分配失败:${result.message || '未知错误'}`);
    }
  },
  error: (error) => {
    // 网络错误或服务端异常
    console.error('订单分配失败:', error);

    let errorMessage = '订单分配失败,请重试';

    if (error.status === 400) {
      errorMessage = '请求参数有误,请检查表单填写';
    } else if (error.status === 401) {
      errorMessage = '未登录或登录已过期,请重新登录';
    } else if (error.status === 403) {
      errorMessage = '没有权限执行此操作';
    } else if (error.status === 500) {
      errorMessage = '服务器错误,请稍后重试';
    }

    alert(errorMessage);
  }
});

8.3 设计师资源不足

onDesignerAssignmentChange(data: DesignerAssignmentData): void {
  if (!data.selectedDesigners || data.selectedDesigners.length === 0) {
    // 没有选择设计师
    this.showWarning('请至少选择一位设计师');
    return;
  }

  // 检查设计师是否可用
  const unavailableDesigners = data.selectedDesigners.filter(d => d.status === 'unavailable');
  if (unavailableDesigners.length > 0) {
    const names = unavailableDesigners.map(d => d.name).join('、');
    this.showWarning(`以下设计师当前不可用:${names}\n请重新选择`);
    return;
  }

  // 检查工作负荷
  const overloadedDesigners = data.selectedDesigners.filter(d => d.workload > 90);
  if (overloadedDesigners.length > 0) {
    const names = overloadedDesigners.map(d => d.name).join('、');
    const confirmed = confirm(`以下设计师工作负荷较高(>90%):${names}\n确定要继续分配吗?`);
    if (!confirmed) {
      return;
    }
  }

  this.designerAssignmentData = { ...data };
}

8.4 数据同步失败

// 客服端同步数据解析失败
this.route.queryParamMap.subscribe({
  next: (qp) => {
    const syncDataParam = qp.get('syncData');
    if (syncDataParam) {
      try {
        const syncData = JSON.parse(syncDataParam);
        // ... 同步逻辑
      } catch (error) {
        console.error('解析同步数据失败:', error);
        this.isSyncingCustomerInfo = false;

        // 显示错误提示
        alert('客户信息同步失败,请手动填写表单');

        // 回退到手动模式
        this.orderCreationMethod = 'manual';
      }
    }
  }
});

9. 性能优化

9.1 变更检测优化

// 使用 OnPush 策略的子组件
@Component({
  selector: 'app-quotation-details',
  changeDetection: ChangeDetectionStrategy.OnPush,
  ...
})

// 父组件手动触发变更检测
onQuotationDataChange(data: QuotationData): void {
  this.quotationData = { ...data }; // 不可变更新
  this.orderAmount = data.totalAmount || 0;
  this.cdr.markForCheck(); // 标记需要检查
}

9.2 懒加载子组件

// 只有展开订单分配阶段时才加载子组件
@if (expandedSection === 'order' || getSectionStatus('order') === 'active') {
  <div class="order-assignment-section">
    <app-quotation-details ...></app-quotation-details>
    <app-designer-assignment ...></app-designer-assignment>
  </div>
}

9.3 防抖处理

// 客户搜索防抖
private searchDebounce = new Subject<string>();

ngOnInit(): void {
  this.searchDebounce
    .pipe(debounceTime(300), distinctUntilChanged())
    .subscribe(keyword => {
      this.performSearch(keyword);
    });
}

onSearchInputChange(keyword: string): void {
  this.searchDebounce.next(keyword);
}

10. 测试用例

10.1 单元测试

表单验证测试

describe('OrderCreationForm Validation', () => {
  it('should require orderAmount', () => {
    const control = component.orderCreationForm.get('orderAmount');
    control?.setValue('');
    expect(control?.valid).toBeFalsy();
    expect(control?.errors?.['required']).toBeTruthy();
  });

  it('should reject negative orderAmount', () => {
    const control = component.orderCreationForm.get('orderAmount');
    control?.setValue(-100);
    expect(control?.valid).toBeFalsy();
    expect(control?.errors?.['min']).toBeTruthy();
  });

  it('should accept valid orderAmount', () => {
    const control = component.orderCreationForm.get('orderAmount');
    control?.setValue(5000);
    expect(control?.valid).toBeTruthy();
  });
});

权限控制测试

describe('Permission Control', () => {
  it('should allow customer-service to edit order section', () => {
    component.roleContext = 'customer-service';
    expect(component.canEditSection('order')).toBeTruthy();
  });

  it('should not allow designer to edit order section', () => {
    component.roleContext = 'designer';
    expect(component.canEditSection('order')).toBeFalsy();
  });

  it('should allow team-leader to edit all sections', () => {
    component.roleContext = 'team-leader';
    expect(component.canEditSection('order')).toBeTruthy();
    expect(component.canEditSection('requirements')).toBeTruthy();
    expect(component.canEditSection('delivery')).toBeTruthy();
    expect(component.canEditSection('aftercare')).toBeTruthy();
  });
});

10.2 集成测试

订单创建流程测试

describe('Order Creation Flow', () => {
  it('should complete full order creation flow', async () => {
    // 1. 填写客户信息
    component.customerForm.patchValue({
      name: '测试客户',
      phone: '13800138000',
      customerType: '新客户'
    });

    // 2. 填写必填项
    component.orderCreationForm.patchValue({
      orderAmount: 10000,
      smallImageDeliveryTime: new Date(),
      decorationType: '全包',
      requirementReason: '新房装修'
    });

    // 3. 添加报价明细
    component.quotationData = {
      items: [{ id: '1', room: '客厅', amount: 5000 }],
      totalAmount: 10000,
      materialCost: 6000,
      laborCost: 3000,
      designFee: 1000,
      managementFee: 0
    };

    // 4. 分配设计师
    component.designerAssignmentData = {
      selectedDesigners: [mockDesigner],
      teamId: 'team1',
      teamName: '设计一组',
      leaderId: 'designer1',
      assignmentDate: new Date(),
      expectedStartDate: new Date()
    };

    // 5. 验证可以提交
    expect(component.canCreateOrder()).toBeTruthy();

    // 6. 提交订单
    spyOn(component.projectService, 'createProject').and.returnValue(
      of({ success: true, projectId: 'proj-001' })
    );

    component.createOrder();

    expect(component.projectService.createProject).toHaveBeenCalled();
  });
});

10.3 E2E测试

完整用户流程测试

describe('Order Assignment E2E', () => {
  it('should complete order assignment as customer-service', async () => {
    // 1. 客服登录
    await page.goto('/customer-service/login');
    await page.fill('#username', 'cs_user');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');

    // 2. 创建咨询订单
    await page.goto('/customer-service/consultation-order/create');
    await page.fill('#customerName', '张三');
    await page.fill('#customerPhone', '13800138000');
    await page.click('button[text="创建订单"]');

    // 3. 跳转到项目详情页
    await page.waitForURL(/\/designer\/project-detail/);

    // 4. 填写订单分配表单
    await page.fill('#orderAmount', '10000');
    await page.fill('#smallImageDeliveryTime', '2025-11-01');
    await page.selectOption('#decorationType', '全包');
    await page.check('#requirementReason[value="新房装修"]');

    // 5. 添加报价明细
    await page.click('button[text="添加报价项"]');
    await page.fill('.quotation-item:last-child #room', '客厅');
    await page.fill('.quotation-item:last-child #amount', '5000');

    // 6. 选择设计师
    await page.click('.designer-card:first-child');

    // 7. 提交订单
    await page.click('button[text="分配订单"]');

    // 8. 验证成功跳转到需求沟通阶段
    await expect(page.locator('.stage-nav-item.active')).toContainText('确认需求');
  });
});

11. 附录

11.1 字段标签映射

private fieldLabelMap: Record<string, string> = {
  'orderAmount': '订单金额',
  'smallImageDeliveryTime': '小图交付时间',
  'decorationType': '装修类型',
  'requirementReason': '需求原因',
  'isMultiDesigner': '多设计师协作',
  'largeImageDeliveryTime': '大图交付时间',
  'spaceRequirements': '空间需求',
  'designAngles': '设计角度',
  'specialAreaHandling': '特殊区域处理',
  'materialRequirements': '材质要求',
  'lightingRequirements': '光照需求'
};

private getFieldLabel(fieldName: string): string {
  return this.fieldLabelMap[fieldName] || fieldName;
}

11.2 阶段锚点映射

// project-detail.ts lines 2237-2251
stageToAnchor(stage: ProjectStage): string {
  const map: Record<ProjectStage, string> = {
    '订单分配': 'order',
    '需求沟通': 'requirements-talk',
    '方案确认': 'proposal-confirm',
    '建模': 'modeling',
    '软装': 'softdecor',
    '渲染': 'render',
    '后期': 'postprocess',
    '尾款结算': 'settlement',
    '客户评价': 'customer-review',
    '投诉处理': 'complaint'
  };
  return `stage-${map[stage] || 'unknown'}`;
}

11.3 设计师状态颜色映射

.designer-card {
  &.status-idle {
    border-left: 4px solid #10b981; // 绿色 - 空闲
  }

  &.status-busy {
    border-left: 4px solid #f59e0b; // 橙色 - 繁忙
  }

  &.status-unavailable {
    border-left: 4px solid #9ca3af; // 灰色 - 不可用
    opacity: 0.6;
    cursor: not-allowed;
  }
}

文档版本:v1.0.0 创建日期:2025-10-16 最后更新:2025-10-16 维护人:产品团队