项目-订单分配.md 54 KB

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

1. 功能概述

1.1 阶段定位

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

1.2 核心目标

  • 多空间产品识别与管理:智能识别单空间/多空间项目,基于Product表管理
  • 完成客户信息的结构化录入和同步
  • 确定项目报价和付款条件(支持多产品设计报价策略)
  • 匹配并分配合适的设计师资源(考虑多产品协调需求)
  • 建立项目基础档案,为后续环节提供数据支撑

1.3 涉及角色

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

2. 基于Product表的空间产品管理

2.1 空间产品识别与分类

2.1.1 智能空间产品识别系统

class ProductIdentificationService {
  // 基于客户需求描述自动识别空间设计产品
  async identifyProductsFromDescription(description: string): Promise<ProductIdentificationResult> {
    const result: ProductIdentificationResult = {
      identifiedProducts: [],
      confidence: 0,
      reasoning: '',
      suggestedQuestions: []
    };

    // 产品类型关键词映射
    const productKeywords = {
      [ProductType.LIVING_ROOM]: ['客厅', '起居室', '会客厅', '茶室', '待客区'],
      [ProductType.BEDROOM]: ['卧室', '主卧', '次卧', '儿童房', '老人房', '客房', '主人房'],
      [ProductType.KITCHEN]: ['厨房', '开放式厨房', '中西厨', '餐厨一体'],
      [ProductType.BATHROOM]: ['卫生间', '浴室', '洗手间', '盥洗室', '主卫', '次卫'],
      [ProductType.DINING_ROOM]: ['餐厅', '餐厅区', '用餐区', '就餐空间'],
      [ProductType.STUDY]: ['书房', '工作室', '办公室', '学习区', '阅读区'],
      [ProductType.BALCONY]: ['阳台', '露台', '花园阳台', '休闲阳台'],
      [ProductType.CORRIDOR]: ['走廊', '过道', '玄关', '门厅', '入户'],
      [ProductType.STORAGE]: ['储物间', '衣帽间', '杂物间', '收纳空间']
    };

    // 分析描述中的空间关键词
    const foundProducts: Array<{ type: ProductType; keywords: string[]; confidence: number }> = [];

    for (const [productType, keywords] of Object.entries(productKeywords)) {
      const matchedKeywords = keywords.filter(keyword =>
        description.toLowerCase().includes(keyword.toLowerCase())
      );

      if (matchedKeywords.length > 0) {
        foundProducts.push({
          type: productType as ProductType,
          keywords: matchedKeywords,
          confidence: matchedKeywords.length / keywords.length
        });
      }
    }

    // 按置信度排序
    foundProducts.sort((a, b) => b.confidence - a.confidence);

    // 构建识别结果
    result.identifiedProducts = foundProducts.map(fp => ({
      type: fp.type,
      productName: this.getDefaultProductName(fp.type),
      priority: this.calculateProductPriority(fp.type, fp.confidence),
      confidence: fp.confidence,
      identifiedKeywords: fp.keywords
    }));

    // 计算整体置信度
    result.confidence = foundProducts.length > 0
      ? foundProducts.reduce((sum, fp) => sum + fp.confidence, 0) / foundProducts.length
      : 0;

    // 生成推理说明
    result.reasoning = this.generateIdentificationReasoning(foundProducts);

    // 生成建议问题
    result.suggestedQuestions = this.generateClarifyingQuestions(foundProducts);

    return result;
  }

  // 基于面积和预算推断产品设计数量
  estimateProductCount(totalArea: number, budget: number): ProductEstimationResult {
    const result: ProductEstimationResult = {
      estimatedProductCount: 1,
      confidence: 0.5,
      reasoning: '',
      possibleProductTypes: []
    };

    // 基于面积的产品数量估算
    const areaBasedCount = Math.max(1, Math.floor(totalArea / 20)); // 每20平米一个主要空间

    // 基于预算的产品数量估算
    const budgetBasedCount = Math.max(1, Math.floor(budget / 30000)); // 每3万一个空间

    // 综合判断
    const finalCount = Math.min(areaBasedCount, budgetBasedCount);
    result.estimatedProductCount = finalCount;

    // 推断可能的空间类型
    result.possibleProductTypes = this.inferPossibleProductTypes(totalArea, budget);

    // 生成推理
    result.reasoning = `基于面积${totalArea}平米和预算${budget}元,估算需要${finalCount}个主要空间设计产品`;

    return result;
  }

  // 生成产品设计配置建议
  generateProductConfiguration(
    identifiedProducts: IdentifiedProduct[],
    totalArea: number,
    budget: number
  ): ProductConfiguration {
    const configuration: ProductConfiguration = {
      products: [],
      totalEstimatedBudget: 0,
      budgetAllocation: {},
      recommendations: []
    };

    // 为识别出的产品创建配置
    for (const product of identifiedProducts) {
      const productConfig = this.createProductConfiguration(product, totalArea, budget);
      configuration.products.push(productConfig);
      configuration.budgetAllocation[product.type] = productConfig.estimatedBudget;
    }

    // 如果没有识别出产品,创建默认配置
    if (configuration.products.length === 0) {
      const defaultProduct = this.createDefaultProductConfiguration(totalArea, budget);
      configuration.products.push(defaultProduct);
      configuration.budgetAllocation[defaultProduct.type] = defaultProduct.estimatedBudget;
    }

    // 计算总预算
    configuration.totalEstimatedBudget = Object.values(configuration.budgetAllocation)
      .reduce((sum, budget) => sum + budget, 0);

    // 生成建议
    configuration.recommendations = this.generateConfigurationRecommendations(configuration);

    return configuration;
  }
}

interface ProductIdentificationResult {
  identifiedProducts: IdentifiedProduct[];
  confidence: number;
  reasoning: string;
  suggestedQuestions: string[];
}

interface IdentifiedProduct {
  type: ProductType;
  productName: string;
  priority: number;
  confidence: number;
  identifiedKeywords: string[];
}

interface ProductEstimationResult {
  estimatedProductCount: number;
  confidence: number;
  reasoning: string;
  possibleProductTypes: ProductType[];
}

interface ProductConfiguration {
  products: ProductConfig[];
  totalEstimatedBudget: number;
  budgetAllocation: Record<ProductType, number>;
  recommendations: string[];
}

interface ProductConfig {
  type: ProductType;
  productName: string;
  estimatedArea: number;
  estimatedBudget: number;
  priority: number;
  complexity: 'simple' | 'medium' | 'complex';
}

2.1.2 空间产品设计管理界面

<!-- 空间产品设计管理面板 -->
<div class="space-management-panel">
  <div class="panel-header">
    <h3>空间配置</h3>
    <div class="space-type-indicator">
      <span class="indicator-label">项目类型:</span>
      <span class="indicator-value"
            [class.single-space]="isSingleSpaceProject"
            [class.multi-space]="!isSingleSpaceProject">
        {{ isSingleSpaceProject ? '单空间项目' : '多空间项目' }}
      </span>
    </div>
  </div>

  <!-- 空间识别结果 -->
  <div class="space-identification-result" *ngIf="spaceIdentificationResult">
    <div class="identification-summary">
      <h4>识别到 {{ spaceIdentificationResult.identifiedSpaces.length }} 个空间</h4>
      <div class="confidence-indicator">
        <span class="confidence-label">置信度:</span>
        <div class="confidence-bar">
          <div class="confidence-fill"
               [style.width.%]="spaceIdentificationResult.confidence * 100"
               [class.high]="spaceIdentificationResult.confidence >= 0.8"
               [class.medium]="spaceIdentificationResult.confidence >= 0.5 && spaceIdentificationResult.confidence < 0.8"
               [class.low]="spaceIdentificationResult.confidence < 0.5">
          </div>
        </div>
        <span class="confidence-value">{{ Math.round(spaceIdentificationResult.confidence * 100) }}%</span>
      </div>
    </div>

    <div class="identification-reasoning">
      <p>{{ spaceIdentificationResult.reasoning }}</p>
    </div>

    <!-- 建议问题 -->
    <div class="suggested-questions" *ngIf="spaceIdentificationResult.suggestedQuestions.length > 0">
      <h5>建议确认的问题:</h5>
      <ul>
        @for (question of spaceIdentificationResult.suggestedQuestions; track question) {
          <li>{{ question }}</li>
        }
      </ul>
    </div>
  </div>

  <!-- 空间列表 -->
  <div class="spaces-list">
    <div class="list-header">
      <h4>空间列表</h4>
      <button class="btn-add-space" (click)="showAddSpaceDialog()">
        <i class="icon-plus"></i> 添加空间
      </button>
    </div>

    <div class="spaces-grid">
      @for (space of projectSpaces; track space.id) {
        <div class="space-card"
             [class.high-priority]="space.priority >= 8"
             [class.medium-priority]="space.priority >= 5 && space.priority < 8"
             [class.low-priority]="space.priority < 5">
          <div class="space-header">
            <div class="space-icon">
              <i class="icon-{{ getSpaceIcon(space.type) }}"></i>
            </div>
            <div class="space-info">
              <input type="text"
                     [(ngModel)]="space.name"
                     class="space-name-input"
                     placeholder="空间名称">
              <select [(ngModel)]="space.type" class="space-type-select">
                <option value="{{ SpaceType.LIVING_ROOM }}">客厅</option>
                <option value="{{ SpaceType.BEDROOM }}">卧室</option>
                <option value="{{ SpaceType.KITCHEN }}">厨房</option>
                <option value="{{ SpaceType.BATHROOM }}">卫生间</option>
                <option value="{{ SpaceType.DINING_ROOM }}">餐厅</option>
                <option value="{{ SpaceType.STUDY }}">书房</option>
                <option value="{{ SpaceType.BALCONY }}">阳台</option>
                <option value="{{ SpaceType.CORRIDOR }}">走廊</option>
                <option value="{{ SpaceType.STORAGE }}">储物间</option>
                <option value="{{ SpaceType.OTHER }}">其他</option>
              </select>
            </div>
            <div class="space-actions">
              <button class="btn-icon" (click)="editSpace(space.id)" title="编辑">
                <i class="icon-edit"></i>
              </button>
              <button class="btn-icon danger" (click)="removeSpace(space.id)" title="删除">
                <i class="icon-delete"></i>
              </button>
            </div>
          </div>

          <div class="space-details">
            <div class="detail-row">
              <label>面积:</label>
              <input type="number"
                     [(ngModel)]="space.area"
                     min="1"
                     class="detail-input">
              <span class="unit">m²</span>
            </div>

            <div class="detail-row">
              <label>优先级:</label>
              <select [(ngModel)]="space.priority" class="priority-select">
                <option [ngValue]="10">最高</option>
                <option [ngValue]="8">高</option>
                <option [ngValue]="5">中</option>
                <option [ngValue]="3">低</option>
                <option [ngValue]="1">最低</option>
              </select>
            </div>

            <div class="detail-row">
              <label>复杂度:</label>
              <select [(ngModel)]="space.complexity" class="complexity-select">
                <option value="simple">简单</option>
                <option value="medium">中等</option>
                <option value="complex">复杂</option>
              </select>
            </div>
          </div>

          <div class="space-budget">
            <label>预估预算:</label>
            <div class="budget-input-group">
              <input type="number"
                     [(ngModel)]="space.estimatedBudget"
                     min="0"
                     class="budget-input">
              <span class="currency">元</span>
            </div>
          </div>
        </div>
      }
    </div>
  </div>

  <!-- 空间统计信息 -->
  <div class="space-statistics">
    <div class="stat-item">
      <span class="stat-label">总空间数:</span>
      <span class="stat-value">{{ projectSpaces.length }}</span>
    </div>
    <div class="stat-item">
      <span class="stat-label">总面积:</span>
      <span class="stat-value">{{ totalSpaceArea }}m²</span>
    </div>
    <div class="stat-item">
      <span class="stat-label">总预算:</span>
      <span class="stat-value">¥{{ totalSpaceBudget.toLocaleString() }}</span>
    </div>
  </div>
</div>

2.2 多空间报价管理

2.2.1 空间报价计算

class MultiSpaceQuotationService {
  // 计算多空间项目报价
  calculateMultiSpaceQuotation(
    spaces: ProjectSpace[],
    globalOptions: QuotationOptions
  ): MultiSpaceQuotation {

    const quotation: MultiSpaceQuotation = {
      projectId: this.getProjectId(),
      quotationDate: new Date(),
      currency: 'CNY',

      // 空间报价明细
      spaceQuotations: [],

      // 全局折扣和优惠
      globalDiscounts: [],

      // 汇总信息
      summary: {
        totalBaseAmount: 0,
        totalDiscountAmount: 0,
        finalAmount: 0,
        averagePricePerSqm: 0
      }
    };

    // 计算各空间报价
    for (const space of spaces) {
      const spaceQuotation = this.calculateSpaceQuotation(space, globalOptions);
      quotation.spaceQuotations.push(spaceQuotation);
    }

    // 计算基础总额
    quotation.summary.totalBaseAmount = quotation.spaceQuotations
      .reduce((sum, sq) => sum + sq.totalAmount, 0);

    // 应用多空间折扣
    quotation.globalDiscounts = this.calculateMultiSpaceDiscounts(
      spaces,
      quotation.summary.totalBaseAmount
    );

    // 计算折扣总额
    quotation.summary.totalDiscountAmount = quotation.globalDiscounts
      .reduce((sum, discount) => sum + discount.value, 0);

    // 计算最终金额
    quotation.summary.finalAmount = quotation.summary.totalBaseAmount - quotation.summary.totalDiscountAmount;

    // 计算平均单价
    const totalArea = spaces.reduce((sum, space) => sum + (space.area || 0), 0);
    quotation.summary.averagePricePerSqm = totalArea > 0 ? quotation.summary.finalAmount / totalArea : 0;

    return quotation;
  }

  // 计算单个空间报价
  private calculateSpaceQuotation(
    space: ProjectSpace,
    options: QuotationOptions
  ): SpaceQuotation {

    // 基础价格计算
    const basePrice = this.calculateBasePrice(space, options);

    // 复杂度调整
    const complexityMultiplier = this.getComplexityMultiplier(space.complexity);

    // 优先级调整
    const priorityMultiplier = this.getPriorityMultiplier(space.priority);

    // 面积系数
    const areaCoefficient = this.getAreaCoefficient(space.area || 0);

    // 计算最终价格
    const finalPrice = basePrice * complexityMultiplier * priorityMultiplier * areaCoefficient;

    const spaceQuotation: SpaceQuotation = {
      spaceId: space.id,
      spaceName: space.name,
      spaceType: space.type,
      area: space.area || 0,

      // 价格明细
      priceBreakdown: {
        basePrice: basePrice,
        complexityAdjustment: basePrice * (complexityMultiplier - 1),
        priorityAdjustment: basePrice * (priorityMultiplier - 1),
        areaAdjustment: basePrice * (areaCoefficient - 1),
      },

      // 总价
      totalAmount: finalPrice,

      // 单价
      unitPrice: space.area ? finalPrice / space.area : 0,

      // 时间预估
      estimatedDays: this.calculateEstimatedDays(space, finalPrice),

      // 设计师配置
      designerRequirements: this.getDesignerRequirements(space)
    };

    return spaceQuotation;
  }

  // 计算多空间折扣
  private calculateMultiSpaceDiscounts(
    spaces: ProjectSpace[],
    baseAmount: number
  ): QuotationDiscount[] {
    const discounts: QuotationDiscount[] = [];

    // 1. 空间数量折扣
    if (spaces.length >= 5) {
      discounts.push({
        type: 'space_count',
        name: '5空间及以上项目折扣',
        description: '5个及以上空间项目享受10%折扣',
        value: baseAmount * 0.1,
        isApplicable: true
      });
    } else if (spaces.length >= 3) {
      discounts.push({
        type: 'space_count',
        name: '3-4空间项目折扣',
        description: '3-4个空间项目享受5%折扣',
        value: baseAmount * 0.05,
        isApplicable: true
      });
    }

    // 2. 总额折扣
    if (baseAmount >= 200000) {
      discounts.push({
        type: 'total_amount',
        name: '高额度项目折扣',
        description: '项目总额超过20万享受额外5%折扣',
        value: baseAmount * 0.05,
        isApplicable: true
      });
    }

    // 3. 复杂度折扣(针对全高优先级空间)
    const allHighPriority = spaces.every(space => space.priority >= 8);
    if (allHighPriority) {
      discounts.push({
        type: 'priority_bonus',
        name: '高优先级项目折扣',
        description: '全高优先级空间项目享受3%折扣',
        value: baseAmount * 0.03,
        isApplicable: true
      });
    }

    return discounts;
  }

  // 生成报价单
  async generateQuotationDocument(quotation: MultiSpaceQuotation): Promise<QuotationDocument> {
    const document: QuotationDocument = {
      id: `quotation_${Date.now()}`,
      projectId: quotation.projectId,
      documentNumber: this.generateQuotationNumber(),
      issueDate: new Date(),
      validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天有效期
      currency: quotation.currency,
      quotation: quotation,

      // 客户信息
      customerInfo: await this.getCustomerInfo(),

      // 项目信息
      projectInfo: await this.getProjectInfo(),

      // 支付条款
      paymentTerms: this.generatePaymentTerms(quotation),

      // 服务内容
      serviceScope: this.generateServiceScope(quotation),

      // 注意事项
      notes: this.generateQuotationNotes(quotation)
    };

    return document;
  }
}

interface MultiSpaceQuotation {
  projectId: string;
  quotationDate: Date;
  currency: string;
  spaceQuotations: SpaceQuotation[];
  globalDiscounts: QuotationDiscount[];
  summary: {
    totalBaseAmount: number;
    totalDiscountAmount: number;
    finalAmount: number;
    averagePricePerSqm: number;
  };
}

interface SpaceQuotation {
  spaceId: string;
  spaceName: string;
  spaceType: SpaceType;
  area: number;
  priceBreakdown: {
    basePrice: number;
    complexityAdjustment: number;
    priorityAdjustment: number;
    areaAdjustment: number;
  };
  totalAmount: number;
  unitPrice: number;
  estimatedDays: number;
  designerRequirements: string[];
}

interface QuotationDiscount {
  type: 'space_count' | 'total_amount' | 'priority_bonus' | 'special_offer';
  name: string;
  description: string;
  value: number;
  isApplicable: boolean;
}

interface QuotationDocument {
  id: string;
  projectId: string;
  documentNumber: string;
  issueDate: Date;
  validUntil: Date;
  currency: string;
  quotation: MultiSpaceQuotation;
  customerInfo: any;
  projectInfo: any;
  paymentTerms: any;
  serviceScope: any;
  notes: string[];
}

3. 核心功能模块

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