2025-10-28
实现组长端对客服提交的订单分配进行审批的完整流程,包括审批入口、审批操作、状态流转和历史记录。
客服端提交订单分配
    ↓
项目进入"待组长确认"状态 (currentStage: '订单分配', 附带 pendingApproval 标记)
    ↓
组长在工作台看到待审批项目
    ↓
组长点击进入项目详情页
    ↓
组长审核订单内容(报价、设计师分配、项目信息)
    ↓
组长做出决策:
    - 通过审批 → 项目进入"确认需求"阶段
    - 驳回审批 → 项目退回"订单分配"阶段,客服端显示驳回原因
    ↓
记录审批历史
    ↓
发送通知(可选)
Project {
  currentStage: string,  // '订单分配' | '确认需求' | '方案确认' | ...
  status: string,        // '待分配' | '进行中' | '已完成' | ...
  data: {
    // 新增审批相关字段
    approvalStatus?: 'pending' | 'approved' | 'rejected',  // 当前审批状态
    approvalHistory: ApprovalRecord[],  // 审批历史记录
    pendingApprovalBy?: string,  // 待审批人角色 'team-leader'
    lastRejectionReason?: string  // 最近一次驳回原因
  }
}
// 审批记录接口
interface ApprovalRecord {
  stage: string;              // 审批阶段:'订单分配'
  submitter: {                // 提交人信息
    id: string;
    name: string;
    role: string;
  };
  submitTime: Date;           // 提交时间
  status: 'pending' | 'approved' | 'rejected';  // 审批状态
  approver?: {                // 审批人信息(通过/驳回后填写)
    id: string;
    name: string;
    role: string;
  };
  approvalTime?: Date;        // 审批时间
  reason?: string;            // 驳回原因
  comment?: string;           // 审批备注
  quotationTotal: number;     // 报价总额快照
  teams: TeamSnapshot[];      // 团队分配快照
}
interface TeamSnapshot {
  id: string;
  name: string;
  spaces: string[];  // 分配的空间
}
当前已有 pendingApprovalProjects 计算属性,需要调整筛选逻辑:
// 位置:src/app/pages/team-leader/dashboard/dashboard.ts
// 修改现有的 getter
get pendingApprovalProjects(): Project[] {
  return this.projects.filter(p => {
    const stage = (p.currentStage || '').trim();
    const approvalStatus = p.data?.approvalStatus;
    
    // 1. 阶段为"订单分配"且审批状态为 pending
    // 2. 或者阶段为"待确认"/"待审批"
    return (stage === '订单分配' && approvalStatus === 'pending') ||
           stage === '待审批' || 
           stage === '待确认';
  });
}
在项目卡片上添加醒目的"待审批"标识:
<!-- 位置:src/app/pages/team-leader/dashboard/dashboard.html -->
<div class="project-card" 
     [class.pending-approval]="isPendingApproval(project)">
  
  <!-- 添加审批徽章 -->
  @if (isPendingApproval(project)) {
    <div class="approval-badge">
      <span class="badge-icon">📋</span>
      <span class="badge-text">待审批</span>
    </div>
  }
  
  <!-- 原有项目卡片内容 -->
  <div class="project-card-header">
    <h4>{{ project.name }}</h4>
    <!-- ... -->
  </div>
</div>
// dashboard.ts 中添加辅助方法
isPendingApproval(project: Project): boolean {
  return project.currentStage === '订单分配' && 
         project.data?.approvalStatus === 'pending';
}
// dashboard.scss 样式
.project-card {
  &.pending-approval {
    border: 2px solid #ff9800;
    box-shadow: 0 0 10px rgba(255, 152, 0, 0.3);
    position: relative;
    
    .approval-badge {
      position: absolute;
      top: 10px;
      right: 10px;
      background: linear-gradient(135deg, #ff9800, #ff6b00);
      color: white;
      padding: 4px 12px;
      border-radius: 20px;
      font-size: 12px;
      font-weight: bold;
      display: flex;
      align-items: center;
      gap: 4px;
      box-shadow: 0 2px 8px rgba(255, 152, 0, 0.4);
      animation: pulse 2s ease-in-out infinite;
      
      .badge-icon {
        font-size: 14px;
      }
    }
  }
}
@keyframes pulse {
  0%, 100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.05);
  }
}
位置:src/app/shared/components/order-approval-panel/
// order-approval-panel.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
interface ApprovalData {
  projectId: string;
  projectName: string;
  quotationTotal: number;
  assignedTeams: TeamInfo[];
  projectInfo: {
    title: string;
    projectType: string;
    demoday: Date;
    deadline?: Date;
  };
  submitter: {
    id: string;
    name: string;
    role: string;
  };
  submitTime: Date;
}
interface TeamInfo {
  id: string;
  name: string;
  spaces: string[];
}
@Component({
  selector: 'app-order-approval-panel',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './order-approval-panel.component.html',
  styleUrls: ['./order-approval-panel.component.scss']
})
export class OrderApprovalPanelComponent implements OnInit {
  @Input() project: any;  // Parse Project 对象
  @Input() currentUser: any;  // 当前组长用户
  @Output() approvalCompleted = new EventEmitter<{
    action: 'approved' | 'rejected';
    reason?: string;
    comment?: string;
  }>();
  approvalData: ApprovalData | null = null;
  showRejectModal = false;
  rejectReason = '';
  approvalComment = '';
  isSubmitting = false;
  // 驳回原因快捷选项
  rejectReasons = [
    '报价不合理,需要调整',
    '设计师分配不当',
    '项目信息不完整',
    '需要补充项目资料',
    '其他原因(请在下方说明)'
  ];
  selectedRejectReason = '';
  ngOnInit() {
    this.loadApprovalData();
  }
  /**
   * 加载审批数据
   */
  private loadApprovalData() {
    if (!this.project) return;
    const data = this.project.get('data') || {};
    const approvalHistory = data.approvalHistory || [];
    const latestRecord = approvalHistory[approvalHistory.length - 1];
    this.approvalData = {
      projectId: this.project.id,
      projectName: this.project.get('title'),
      quotationTotal: latestRecord?.quotationTotal || 0,
      assignedTeams: latestRecord?.teams || [],
      projectInfo: {
        title: this.project.get('title'),
        projectType: this.project.get('projectType'),
        demoday: this.project.get('demoday'),
        deadline: this.project.get('deadline')
      },
      submitter: latestRecord?.submitter || {},
      submitTime: latestRecord?.submitTime || new Date()
    };
  }
  /**
   * 通过审批
   */
  async approveOrder() {
    if (this.isSubmitting) return;
    
    const confirmed = await window?.fmode?.confirm('确认通过此订单审批吗?');
    if (!confirmed) return;
    this.isSubmitting = true;
    try {
      this.approvalCompleted.emit({
        action: 'approved',
        comment: this.approvalComment || undefined
      });
    } finally {
      this.isSubmitting = false;
    }
  }
  /**
   * 打开驳回弹窗
   */
  openRejectModal() {
    this.showRejectModal = true;
    this.rejectReason = '';
    this.selectedRejectReason = '';
    this.approvalComment = '';
  }
  /**
   * 关闭驳回弹窗
   */
  closeRejectModal() {
    this.showRejectModal = false;
  }
  /**
   * 选择驳回原因
   */
  selectRejectReason(reason: string) {
    this.selectedRejectReason = reason;
    if (reason !== '其他原因(请在下方说明)') {
      this.rejectReason = reason;
    } else {
      this.rejectReason = '';
    }
  }
  /**
   * 提交驳回
   */
  async submitRejection() {
    const finalReason = this.selectedRejectReason === '其他原因(请在下方说明)' 
      ? this.rejectReason 
      : this.selectedRejectReason;
    if (!finalReason || !finalReason.trim()) {
      alert('请填写驳回原因');
      return;
    }
    if (this.isSubmitting) return;
    this.isSubmitting = true;
    try {
      this.approvalCompleted.emit({
        action: 'rejected',
        reason: finalReason,
        comment: this.approvalComment || undefined
      });
      this.closeRejectModal();
    } finally {
      this.isSubmitting = false;
    }
  }
  /**
   * 格式化金额
   */
  formatCurrency(amount: number): string {
    return `¥${amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
  }
}
<!-- order-approval-panel.component.html -->
<div class="order-approval-panel" *ngIf="approvalData">
  <!-- 审批状态头部 -->
  <div class="approval-header">
    <div class="header-icon">📋</div>
    <div class="header-content">
      <h2>订单审批</h2>
      <p class="subtitle">请仔细审核以下订单信息</p>
    </div>
  </div>
  <!-- 审批信息卡片 -->
  <div class="approval-info-cards">
    <!-- 项目信息卡片 -->
    <div class="info-card">
      <div class="card-header">
        <span class="card-icon">📄</span>
        <h3>项目信息</h3>
      </div>
      <div class="card-body">
        <div class="info-row">
          <span class="label">项目名称:</span>
          <span class="value">{{ approvalData.projectInfo.title }}</span>
        </div>
        <div class="info-row">
          <span class="label">项目类型:</span>
          <span class="value">{{ approvalData.projectInfo.projectType }}</span>
        </div>
        <div class="info-row">
          <span class="label">小图日期:</span>
          <span class="value">{{ approvalData.projectInfo.demoday | date:'yyyy-MM-dd' }}</span>
        </div>
        <div class="info-row" *ngIf="approvalData.projectInfo.deadline">
          <span class="label">交付期限:</span>
          <span class="value">{{ approvalData.projectInfo.deadline | date:'yyyy-MM-dd' }}</span>
        </div>
      </div>
    </div>
    <!-- 报价信息卡片 -->
    <div class="info-card highlight">
      <div class="card-header">
        <span class="card-icon">💰</span>
        <h3>报价总额</h3>
      </div>
      <div class="card-body">
        <div class="quotation-amount">
          {{ formatCurrency(approvalData.quotationTotal) }}
        </div>
      </div>
    </div>
    <!-- 设计师分配卡片 -->
    <div class="info-card">
      <div class="card-header">
        <span class="card-icon">👥</span>
        <h3>设计师分配</h3>
      </div>
      <div class="card-body">
        <div class="team-list">
          <div class="team-item" *ngFor="let team of approvalData.assignedTeams">
            <div class="team-name">{{ team.name }}</div>
            <div class="team-spaces">
              <span class="space-tag" *ngFor="let space of team.spaces">{{ space }}</span>
            </div>
          </div>
          <div class="empty-state" *ngIf="approvalData.assignedTeams.length === 0">
            <span class="empty-icon">📦</span>
            <p>暂无分配设计师</p>
          </div>
        </div>
      </div>
    </div>
    <!-- 提交信息卡片 -->
    <div class="info-card">
      <div class="card-header">
        <span class="card-icon">👤</span>
        <h3>提交信息</h3>
      </div>
      <div class="card-body">
        <div class="info-row">
          <span class="label">提交人:</span>
          <span class="value">{{ approvalData.submitter.name }} ({{ approvalData.submitter.role }})</span>
        </div>
        <div class="info-row">
          <span class="label">提交时间:</span>
          <span class="value">{{ approvalData.submitTime | date:'yyyy-MM-dd HH:mm' }}</span>
        </div>
      </div>
    </div>
  </div>
  <!-- 审批备注 -->
  <div class="approval-comment-section">
    <label for="approvalComment">审批备注(可选)</label>
    <textarea 
      id="approvalComment"
      [(ngModel)]="approvalComment"
      placeholder="可以填写审批意见或建议..."
      rows="3"></textarea>
  </div>
  <!-- 审批操作按钮 -->
  <div class="approval-actions">
    <button 
      class="btn-reject" 
      (click)="openRejectModal()"
      [disabled]="isSubmitting">
      <span class="btn-icon">❌</span>
      驳回订单
    </button>
    <button 
      class="btn-approve" 
      (click)="approveOrder()"
      [disabled]="isSubmitting">
      <span class="btn-icon">✅</span>
      通过审批
    </button>
  </div>
</div>
<!-- 驳回弹窗 -->
<div class="reject-modal-overlay" *ngIf="showRejectModal" (click)="closeRejectModal()">
  <div class="reject-modal" (click)="$event.stopPropagation()">
    <div class="modal-header">
      <h3>驳回订单</h3>
      <button class="close-btn" (click)="closeRejectModal()">×</button>
    </div>
    
    <div class="modal-body">
      <div class="form-group">
        <label>驳回原因 <span class="required">*</span></label>
        <div class="reason-options">
          <label 
            class="reason-option" 
            *ngFor="let reason of rejectReasons"
            [class.selected]="selectedRejectReason === reason">
            <input 
              type="radio" 
              name="rejectReason" 
              [value]="reason"
              [(ngModel)]="selectedRejectReason"
              (change)="selectRejectReason(reason)">
            <span>{{ reason }}</span>
          </label>
        </div>
      </div>
      <div class="form-group" *ngIf="selectedRejectReason === '其他原因(请在下方说明)'">
        <label>详细说明 <span class="required">*</span></label>
        <textarea 
          [(ngModel)]="rejectReason"
          placeholder="请详细说明驳回原因..."
          rows="4"></textarea>
      </div>
      <div class="form-group">
        <label>补充说明(可选)</label>
        <textarea 
          [(ngModel)]="approvalComment"
          placeholder="可以补充其他建议或要求..."
          rows="3"></textarea>
      </div>
    </div>
    <div class="modal-footer">
      <button class="btn-cancel" (click)="closeRejectModal()">取消</button>
      <button 
        class="btn-submit" 
        (click)="submitRejection()"
        [disabled]="isSubmitting">
        确认驳回
      </button>
    </div>
  </div>
</div>
// order-approval-panel.component.scss
.order-approval-panel {
  background: #fff;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  margin-bottom: 24px;
  .approval-header {
    display: flex;
    align-items: center;
    gap: 16px;
    padding-bottom: 20px;
    border-bottom: 2px solid #f0f0f0;
    margin-bottom: 24px;
    .header-icon {
      font-size: 48px;
      animation: float 3s ease-in-out infinite;
    }
    .header-content {
      h2 {
        margin: 0;
        font-size: 24px;
        color: #333;
      }
      .subtitle {
        margin: 4px 0 0;
        color: #666;
        font-size: 14px;
      }
    }
  }
  .approval-info-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 16px;
    margin-bottom: 24px;
    .info-card {
      background: #f8f9fa;
      border-radius: 8px;
      padding: 16px;
      border: 1px solid #e9ecef;
      transition: all 0.3s;
      &:hover {
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        transform: translateY(-2px);
      }
      &.highlight {
        background: linear-gradient(135deg, #fff3e0, #ffe0b2);
        border-color: #ff9800;
        .quotation-amount {
          font-size: 32px;
          font-weight: bold;
          color: #f57c00;
          text-align: center;
          padding: 16px 0;
        }
      }
      .card-header {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 12px;
        .card-icon {
          font-size: 24px;
        }
        h3 {
          margin: 0;
          font-size: 16px;
          color: #333;
        }
      }
      .card-body {
        .info-row {
          display: flex;
          justify-content: space-between;
          padding: 8px 0;
          border-bottom: 1px solid #e9ecef;
          &:last-child {
            border-bottom: none;
          }
          .label {
            color: #666;
            font-size: 14px;
          }
          .value {
            color: #333;
            font-weight: 500;
            font-size: 14px;
          }
        }
        .team-list {
          .team-item {
            padding: 12px;
            background: white;
            border-radius: 6px;
            margin-bottom: 8px;
            &:last-child {
              margin-bottom: 0;
            }
            .team-name {
              font-weight: 500;
              color: #333;
              margin-bottom: 8px;
            }
            .team-spaces {
              display: flex;
              flex-wrap: wrap;
              gap: 6px;
              .space-tag {
                background: #e3f2fd;
                color: #1976d2;
                padding: 4px 12px;
                border-radius: 12px;
                font-size: 12px;
              }
            }
          }
          .empty-state {
            text-align: center;
            padding: 24px;
            color: #999;
            .empty-icon {
              font-size: 48px;
              display: block;
              margin-bottom: 8px;
            }
            p {
              margin: 0;
              font-size: 14px;
            }
          }
        }
      }
    }
  }
  .approval-comment-section {
    margin-bottom: 24px;
    label {
      display: block;
      font-weight: 500;
      color: #333;
      margin-bottom: 8px;
    }
    textarea {
      width: 100%;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 6px;
      font-size: 14px;
      resize: vertical;
      font-family: inherit;
      &:focus {
        outline: none;
        border-color: #4CAF50;
        box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
      }
    }
  }
  .approval-actions {
    display: flex;
    gap: 12px;
    justify-content: flex-end;
    button {
      padding: 12px 32px;
      border: none;
      border-radius: 6px;
      font-size: 16px;
      font-weight: 500;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 8px;
      transition: all 0.3s;
      &:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }
      .btn-icon {
        font-size: 18px;
      }
    }
    .btn-reject {
      background: white;
      color: #f44336;
      border: 2px solid #f44336;
      &:hover:not(:disabled) {
        background: #f44336;
        color: white;
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3);
      }
    }
    .btn-approve {
      background: linear-gradient(135deg, #4CAF50, #45a049);
      color: white;
      &:hover:not(:disabled) {
        background: linear-gradient(135deg, #45a049, #3d8b40);
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
      }
    }
  }
}
// 驳回弹窗样式
.reject-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: 10000;
  animation: fadeIn 0.3s;
}
.reject-modal {
  background: white;
  border-radius: 12px;
  width: 90%;
  max-width: 600px;
  max-height: 90vh;
  overflow-y: auto;
  animation: slideUp 0.3s;
  .modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px 24px;
    border-bottom: 1px solid #e9ecef;
    h3 {
      margin: 0;
      font-size: 20px;
      color: #333;
    }
    .close-btn {
      background: none;
      border: none;
      font-size: 32px;
      color: #999;
      cursor: pointer;
      padding: 0;
      width: 32px;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 50%;
      transition: all 0.3s;
      &:hover {
        background: #f5f5f5;
        color: #333;
      }
    }
  }
  .modal-body {
    padding: 24px;
    .form-group {
      margin-bottom: 20px;
      &:last-child {
        margin-bottom: 0;
      }
      label {
        display: block;
        font-weight: 500;
        color: #333;
        margin-bottom: 8px;
        .required {
          color: #f44336;
          margin-left: 4px;
        }
      }
      .reason-options {
        display: flex;
        flex-direction: column;
        gap: 8px;
        .reason-option {
          display: flex;
          align-items: center;
          gap: 12px;
          padding: 12px;
          border: 2px solid #e9ecef;
          border-radius: 6px;
          cursor: pointer;
          transition: all 0.3s;
          &:hover {
            border-color: #f44336;
            background: #fff5f5;
          }
          &.selected {
            border-color: #f44336;
            background: #ffebee;
          }
          input[type="radio"] {
            cursor: pointer;
          }
          span {
            flex: 1;
            font-size: 14px;
            color: #333;
          }
        }
      }
      textarea {
        width: 100%;
        padding: 12px;
        border: 1px solid #ddd;
        border-radius: 6px;
        font-size: 14px;
        resize: vertical;
        font-family: inherit;
        &:focus {
          outline: none;
          border-color: #f44336;
          box-shadow: 0 0 0 3px rgba(244, 67, 54, 0.1);
        }
      }
    }
  }
  .modal-footer {
    padding: 16px 24px;
    border-top: 1px solid #e9ecef;
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    button {
      padding: 10px 24px;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.3s;
      &:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }
    }
    .btn-cancel {
      background: #f5f5f5;
      color: #666;
      &:hover:not(:disabled) {
        background: #e0e0e0;
      }
    }
    .btn-submit {
      background: #f44336;
      color: white;
      &:hover:not(:disabled) {
        background: #d32f2f;
        box-shadow: 0 2px 8px rgba(244, 67, 54, 0.3);
      }
    }
  }
}
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes slideUp {
  from { 
    opacity: 0;
    transform: translateY(30px);
  }
  to { 
    opacity: 1;
    transform: translateY(0);
  }
}
修改组长查看的项目详情页(复用设计师详情页):
// src/app/pages/designer/project-detail/project-detail.ts
import { OrderApprovalPanelComponent } from '../../shared/components/order-approval-panel/order-approval-panel.component';
@Component({
  // ... 其他配置
  imports: [
    // ... 其他导入
    OrderApprovalPanelComponent
  ]
})
export class ProjectDetail implements OnInit, OnDestroy {
  // 添加审批相关属性
  showApprovalPanel = false;
  
  ngOnInit() {
    // ... 现有初始化代码
    
    // 检查是否需要显示审批面板
    this.checkApprovalStatus();
  }
  
  /**
   * 检查是否需要显示审批面板
   */
  private checkApprovalStatus() {
    if (!this.project) return;
    
    const isTeamLeader = this.roleContext === 'team-leader';
    const currentStage = this.project.get('currentStage');
    const approvalStatus = this.project.get('data')?.approvalStatus;
    
    // 组长视角 + 订单分配阶段 + 待审批状态
    this.showApprovalPanel = isTeamLeader && 
                             currentStage === '订单分配' && 
                             approvalStatus === 'pending';
  }
  
  /**
   * 处理审批完成事件
   */
  async onApprovalCompleted(event: { action: 'approved' | 'rejected'; reason?: string; comment?: string }) {
    if (!this.project || !this.currentUser) return;
    
    try {
      const data = this.project.get('data') || {};
      const approvalHistory = data.approvalHistory || [];
      const latestRecord = approvalHistory[approvalHistory.length - 1];
      
      if (!latestRecord) {
        alert('审批记录不存在');
        return;
      }
      
      // 更新最新的审批记录
      latestRecord.status = event.action === 'approved' ? 'approved' : 'rejected';
      latestRecord.approver = {
        id: this.currentUser.id,
        name: this.currentUser.get('name'),
        role: this.currentUser.get('roleName')
      };
      latestRecord.approvalTime = new Date();
      
      if (event.reason) {
        latestRecord.reason = event.reason;
      }
      if (event.comment) {
        latestRecord.comment = event.comment;
      }
      
      // 更新项目状态
      if (event.action === 'approved') {
        // 通过:推进到"确认需求"阶段
        this.project.set('currentStage', '确认需求');
        data.approvalStatus = 'approved';
        delete data.pendingApprovalBy;
      } else {
        // 驳回:保持在"订单分配"阶段,但标记为已驳回
        data.approvalStatus = 'rejected';
        data.lastRejectionReason = event.reason;
        delete data.pendingApprovalBy;
      }
      
      data.approvalHistory = approvalHistory;
      this.project.set('data', data);
      
      // 保存到数据库
      await this.project.save(null, { useMasterKey: true });
      
      // 提示用户
      if (event.action === 'approved') {
        alert('✅ 审批通过!项目已进入"确认需求"阶段');
      } else {
        alert('❌ 已驳回订单,客服将收到驳回通知');
      }
      
      // 刷新页面或返回列表
      this.showApprovalPanel = false;
      this.router.navigate(['/wxwork', this.companyId, 'team-leader', 'dashboard']);
      
    } catch (error) {
      console.error('审批操作失败:', error);
      alert('操作失败,请重试');
    }
  }
}
<!-- src/app/pages/designer/project-detail/project-detail.html -->
<!-- 在页面顶部添加审批面板 -->
@if (showApprovalPanel) {
  <app-order-approval-panel
    [project]="project"
    [currentUser]="currentUser"
    (approvalCompleted)="onApprovalCompleted($event)">
  </app-order-approval-panel>
}
<!-- 原有的项目详情内容 -->
<!-- ... -->
// src/modules/project/pages/project-detail/stages/stage-order.component.ts
async submitForOrder() {
  // ... 现有验证逻辑 ...
  
  try {
    this.saving = true;
    
    // ... 现有保存逻辑 ...
    
    // ✨ 修改:不直接推进到"确认需求",而是标记为待审批
    // this.project.set('currentStage', '确认需求');  // 删除这行
    
    // 记录审批历史(包含团队快照)
    const data = this.project.get('data') || {};
    const approvalHistory = data.approvalHistory || [];
    
    const teamSnapshot = assignedTeams.map(team => {
      const profile = team.get('profile');
      const spaces = team.get('data')?.spaces || [];
      return {
        id: profile?.id,
        name: profile?.get('name'),
        spaces
      };
    });
    
    approvalHistory.push({
      stage: '订单分配',
      submitter: {
        id: this.currentUser?.id,
        name: this.currentUser?.get('name'),
        role: this.currentUser?.get('roleName')
      },
      submitTime: new Date(),
      status: 'pending',  // ✨ 标记为待审批
      quotationTotal: this.quotation.total,
      teams: teamSnapshot
    });
    
    // ✨ 新增:设置审批状态
    data.approvalHistory = approvalHistory;
    data.approvalStatus = 'pending';  // 待审批
    data.pendingApprovalBy = 'team-leader';  // 待组长审批
    this.project.set('data', data);
    
    // ✨ 保持在"订单分配"阶段
    // 但可以通过 approvalStatus 字段区分是否已提交
    
    await this.project.save();
    
    alert('✅ 提交成功!等待组长审批');
    
  } catch (err) {
    console.error('提交失败:', err);
    alert('提交失败');
  } finally {
    this.saving = false;
  }
}
在客服端项目列表或详情页显示审批状态:
<!-- 审批状态徽章 -->
@if (project.data?.approvalStatus === 'pending') {
  <span class="status-badge pending">等待组长审批</span>
}
@else if (project.data?.approvalStatus === 'approved') {
  <span class="status-badge approved">已通过</span>
}
@else if (project.data?.approvalStatus === 'rejected') {
  <span class="status-badge rejected">已驳回</span>
}
<!-- 驳回提示 -->
@if (project.data?.approvalStatus === 'rejected') {
  <div class="rejection-notice">
    <div class="notice-icon">⚠️</div>
    <div class="notice-content">
      <h4>订单已被驳回</h4>
      <p><strong>驳回原因:</strong>{{ project.data?.lastRejectionReason }}</p>
      <button class="btn-primary" (click)="editAndResubmit()">修改并重新提交</button>
    </div>
  </div>
}
.rejection-notice {
  background: #fff3e0;
  border: 2px solid #ff9800;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 20px;
  display: flex;
  gap: 16px;
  align-items: flex-start;
  .notice-icon {
    font-size: 32px;
  }
  .notice-content {
    flex: 1;
    h4 {
      margin: 0 0 8px;
      color: #f57c00;
    }
    p {
      margin: 0 0 12px;
      color: #666;
    }
    .btn-primary {
      background: #ff9800;
      color: white;
      border: none;
      padding: 8px 20px;
      border-radius: 4px;
      cursor: pointer;
      &:hover {
        background: #f57c00;
      }
    }
  }
}
// src/app/pages/team-leader/services/project-data.service.ts
/**
 * 获取待审批项目列表
 */
async getPendingApprovalProjects(companyId: string): Promise<any[]> {
  const Parse = await this.ensureParse();
  if (!Parse) return [];
  
  try {
    const query = new Parse.Query('Project');
    query.equalTo('company', companyId);
    query.equalTo('currentStage', '订单分配');
    query.notEqualTo('isDeleted', true);
    query.include('contact');
    query.descending('updatedAt');
    query.limit(100);
    
    const projects = await query.find({ useMasterKey: true });
    
    // 过滤出待审批的项目
    return projects.filter(p => {
      const data = p.get('data') || {};
      return data.approvalStatus === 'pending';
    });
    
  } catch (error) {
    console.error('获取待审批项目失败:', error);
    return [];
  }
}
// src/app/services/notification.service.ts
@Injectable({ providedIn: 'root' })
export class NotificationService {
  
  /**
   * 发送审批通知给客服
   */
  async notifyCustomerServiceOfApproval(
    project: any, 
    approvalResult: 'approved' | 'rejected',
    reason?: string
  ) {
    // 获取项目的客服人员
    const customerServiceId = project.get('data')?.submitter?.id;
    if (!customerServiceId) return;
    
    // 构建消息内容
    const message = approvalResult === 'approved' 
      ? `✅ 您提交的订单"${project.get('title')}"已通过审批,可以进入下一阶段。`
      : `❌ 您提交的订单"${project.get('title')}"被驳回。\n驳回原因:${reason}`;
    
    // 调用企业微信API发送消息(需要后端支持)
    try {
      // await this.wxworkApi.sendMessage({
      //   toUser: customerServiceId,
      //   message: message,
      //   agentId: 'your-agent-id'
      // });
      
      console.log('📨 发送通知:', message);
    } catch (error) {
      console.error('发送通知失败:', error);
    }
  }
}
批量审批功能
审批提醒
移动端适配
审批流程可配置
审批数据分析
智能审批建议
预计总工时:5-6天
文档创建: 2025-10-28
最后更新: 2025-10-28
版本: v1.1.0
状态: ✅ 已完成
问题描述:
组长工作台点击待审批项目后,跳转到了开发版本的项目详情页(src/app/pages/designer/project-detail),而不是真实的项目详情页(src/modules/project/pages/project-detail),导致审批面板无法显示。
根本原因:
项目中存在两个项目详情组件:
src/app/pages/designer/project-detail/project-detail.ts - 用于开发测试src/modules/project/pages/project-detail/project-detail.component.ts - 真实项目使用的组件审批功能被误加到了开发版本,而路由配置也指向了开发版本。
解决方案:
修改路由配置(src/app/app.routes.ts):
// wxwork/:cid/team-leader 路由
{
 path: 'team-leader',
 children: [
   {
     path: 'dashboard',
     loadComponent: () => import('./pages/team-leader/dashboard/dashboard').then(m => m.Dashboard),
     title: '组长工作台'
   },
   {
     path: 'project-detail/:projectId',
     // ✅ 修改为真实的项目详情组件
     loadComponent: () => import('../modules/project/pages/project-detail/project-detail.component').then(m => m.ProjectDetailComponent),
     title: '项目详情',
     children: [
       // ... 四阶段子路由
     ]
   }
 ]
}
将审批功能集成到真实组件:
src/modules/project/pages/project-detail/project-detail.component.ts:
OrderApprovalPanelComponentshowApprovalPanel 和 companyId 属性checkApprovalStatus() 方法onApprovalCompleted() 方法src/modules/project/pages/project-detail/project-detail.component.html:
<router-outlet> 之前添加审批面板修复后效果:
相关文件:
src/app/app.routes.ts - 路由配置src/modules/project/pages/project-detail/project-detail.component.ts - 真实项目详情组件src/modules/project/pages/project-detail/project-detail.component.html - 真实项目详情模板src/app/pages/team-leader/dashboard/dashboard.ts - 组长工作台(跳转逻辑)测试验证步骤:
http://localhost:4200/wxwork/cDL6R1hgSi/team-leader/dashboardhttp://localhost:4200/wxwork/cDL6R1hgSi/team-leader/project-detail/B2wtFHIF6k新增时间: 2025-10-28
功能描述:
组长在审批订单时,可以直接在审批面板中修改设计师分配,无需返回订单页面,提高审批效率。
实现内容:
编辑按钮
编辑模式界面
编辑操作
保存/取消
视觉效果:
技术实现:
// TypeScript 关键方法
startEditTeams()      // 开启编辑模式
cancelEditTeams()     // 取消编辑
saveTeamsEdit()       // 保存修改
removeTeam(index)     // 移除设计师
addTeamMember()       // 添加设计师
editTeamSpaces(team)  // 编辑空间
涉及文件:
src/app/shared/components/order-approval-panel/order-approval-panel.component.tssrc/app/shared/components/order-approval-panel/order-approval-panel.component.htmlsrc/app/shared/components/order-approval-panel/order-approval-panel.component.scss使用流程:
改进更新(2025-10-28):
集成了项目详情页的设计师分配弹窗组件(DesignerTeamAssignmentModalComponent),提供更专业的设计师选择体验:
弹窗功能特性:
交互流程:
技术实现:
// 关键方法更新
addTeamMember()              // 打开设计师选择弹窗
closeDesignerModal()         // 关闭弹窗
handleDesignerAssignment()   // 处理弹窗返回的选择结果
涉及组件:
DesignerTeamAssignmentModalComponent - 设计师分配弹窗(复用)DesignerCalendarComponent - 设计师日历组件(弹窗内部使用)