# 组长端订单审批功能实施方案 ## 📅 创建日期 2025-10-28 ## 🎯 功能目标 实现组长端对客服提交的订单分配进行审批的完整流程,包括审批入口、审批操作、状态流转和历史记录。 --- ## 一、业务流程梳理 ### 1.1 完整流程 ``` 客服端提交订单分配 ↓ 项目进入"待组长确认"状态 (currentStage: '订单分配', 附带 pendingApproval 标记) ↓ 组长在工作台看到待审批项目 ↓ 组长点击进入项目详情页 ↓ 组长审核订单内容(报价、设计师分配、项目信息) ↓ 组长做出决策: - 通过审批 → 项目进入"确认需求"阶段 - 驳回审批 → 项目退回"订单分配"阶段,客服端显示驳回原因 ↓ 记录审批历史 ↓ 发送通知(可选) ``` ### 1.2 关键状态定义 #### Project 数据结构扩展 ```typescript 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[]; // 分配的空间 } ``` --- ## 二、前端实现方案 ### 2.1 组长工作台增强(dashboard.ts) #### 2.1.1 待审批项目标识优化 当前已有 `pendingApprovalProjects` 计算属性,需要调整筛选逻辑: ```typescript // 位置: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 === '待确认'; }); } ``` #### 2.1.2 待审批项目卡片视觉增强 在项目卡片上添加醒目的"待审批"标识: ```html
@if (isPendingApproval(project)) {
📋 待审批
}

{{ project.name }}

``` ```typescript // dashboard.ts 中添加辅助方法 isPendingApproval(project: Project): boolean { return project.currentStage === '订单分配' && project.data?.approvalStatus === 'pending'; } ``` ```scss // 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); } } ``` ### 2.2 项目详情页审批组件 #### 2.2.1 创建订单审批组件 位置:`src/app/shared/components/order-approval-panel/` ```typescript // 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 = 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 })}`; } } ``` ```html
📋

订单审批

请仔细审核以下订单信息

📄

项目信息

项目名称: {{ approvalData.projectInfo.title }}
项目类型: {{ approvalData.projectInfo.projectType }}
小图日期: {{ approvalData.projectInfo.demoday | date:'yyyy-MM-dd' }}
交付期限: {{ approvalData.projectInfo.deadline | date:'yyyy-MM-dd' }}
💰

报价总额

{{ formatCurrency(approvalData.quotationTotal) }}
👥

设计师分配

{{ team.name }}
{{ space }}
📦

暂无分配设计师

👤

提交信息

提交人: {{ approvalData.submitter.name }} ({{ approvalData.submitter.role }})
提交时间: {{ approvalData.submitTime | date:'yyyy-MM-dd HH:mm' }}
``` ```scss // 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); } } ``` #### 2.2.2 集成到项目详情页 修改组长查看的项目详情页(复用设计师详情页): ```typescript // 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('操作失败,请重试'); } } } ``` ```html @if (showApprovalPanel) { } ``` --- ## 三、客服端适配 ### 3.1 修改订单提交逻辑 ```typescript // 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; } } ``` ### 3.2 显示审批状态 在客服端项目列表或详情页显示审批状态: ```html @if (project.data?.approvalStatus === 'pending') { 等待组长审批 } @else if (project.data?.approvalStatus === 'approved') { 已通过 } @else if (project.data?.approvalStatus === 'rejected') { 已驳回 } ``` ### 3.3 显示驳回原因 ```html @if (project.data?.approvalStatus === 'rejected') {
⚠️

订单已被驳回

驳回原因:{{ project.data?.lastRejectionReason }}

} ``` ```scss .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; } } } } ``` --- ## 四、数据库查询优化 ### 4.1 为组长工作台创建专用查询 ```typescript // src/app/pages/team-leader/services/project-data.service.ts /** * 获取待审批项目列表 */ async getPendingApprovalProjects(companyId: string): Promise { 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 []; } } ``` --- ## 五、通知机制(可选扩展) ### 5.1 企业微信消息通知 ```typescript // 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); } } } ``` --- ## 六、测试清单 ### 6.1 功能测试 - [ ] 客服提交订单后,项目标记为"待审批" - [ ] 组长工作台正确显示待审批项目数量 - [ ] 组长可以筛选查看待审批项目 - [ ] 点击待审批项目进入详情页,显示审批面板 - [ ] 审批面板正确展示项目信息、报价、设计师分配 - [ ] 通过审批后,项目进入"确认需求"阶段 - [ ] 驳回审批后,项目保持在"订单分配"阶段,显示驳回状态 - [ ] 客服端可以看到审批状态(待审批/已通过/已驳回) - [ ] 驳回后客服可以查看驳回原因 - [ ] 驳回后客服可以修改并重新提交 - [ ] 审批历史正确记录 ### 6.2 权限测试 - [ ] 只有组长角色可以看到审批按钮 - [ ] 非组长角色无法进行审批操作 - [ ] 审批记录中正确记录审批人信息 ### 6.3 边界测试 - [ ] 项目没有审批记录时的处理 - [ ] 多次提交审批的历史记录 - [ ] 审批过程中项目被删除的处理 - [ ] 并发审批的处理 --- ## 七、后续优化建议 ### 7.1 短期优化(1-2周) 1. **批量审批功能** - 组长可以批量选择多个项目进行审批 - 快速通过/驳回多个订单 2. **审批提醒** - 超过24小时未审批的项目高亮显示 - 每日汇总待审批项目发送给组长 3. **移动端适配** - 审批面板响应式设计 - 支持企业微信内审批 ### 7.2 中期优化(1-2月) 1. **审批流程可配置** - 支持自定义审批规则 - 根据项目金额自动路由审批人 2. **审批数据分析** - 统计审批通过率 - 分析驳回原因分布 - 监控审批时效 3. **智能审批建议** - 基于历史数据提供审批建议 - 自动标记异常项目 --- ## 八、实施步骤 ### Phase 1: 核心功能(2-3天) 1. ✅ 创建 OrderApprovalPanelComponent 2. ✅ 修改客服端提交逻辑 3. ✅ 集成到项目详情页 4. ✅ 实现审批操作(通过/驳回) 5. ✅ 测试基本流程 ### Phase 2: 视觉优化(1天) 1. ✅ 组长工作台待审批项目视觉标识 2. ✅ 审批面板样式优化 3. ✅ 驳回弹窗交互优化 ### Phase 3: 客服端适配(1天) 1. ✅ 显示审批状态 2. ✅ 显示驳回原因 3. ✅ 重新提交功能 ### Phase 4: 测试与发布(1天) 1. ✅ 功能测试 2. ✅ 权限测试 3. ✅ 用户验收测试 4. ✅ 正式发布 **预计总工时:5-6天** --- ## 九、相关文档 - [项目管理PRD](../prd/wxwork-project-management.md) - [数据库表结构](../Database/database-tables-overview.md) - [权限管理说明](../prd/wxwork-project-management.md#权限代码) --- **文档创建:** 2025-10-28 **最后更新:** 2025-10-28 **版本:** v1.1.0 **状态:** ✅ 已完成 --- ## 十、问题与修复记录 ### 问题1:路由跳转到错误的项目详情页 **问题描述:** 组长工作台点击待审批项目后,跳转到了开发版本的项目详情页(`src/app/pages/designer/project-detail`),而不是真实的项目详情页(`src/modules/project/pages/project-detail`),导致审批面板无法显示。 **根本原因:** 项目中存在两个项目详情组件: 1. **开发版本**:`src/app/pages/designer/project-detail/project-detail.ts` - 用于开发测试 2. **真实版本**:`src/modules/project/pages/project-detail/project-detail.component.ts` - 真实项目使用的组件 审批功能被误加到了开发版本,而路由配置也指向了开发版本。 **解决方案:** 1. **修改路由配置**(`src/app/app.routes.ts`): ```typescript // 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: [ // ... 四阶段子路由 ] } ] } ``` 2. **将审批功能集成到真实组件**: - 修改 `src/modules/project/pages/project-detail/project-detail.component.ts`: - 导入 `OrderApprovalPanelComponent` - 添加 `showApprovalPanel` 和 `companyId` 属性 - 添加 `checkApprovalStatus()` 方法 - 添加 `onApprovalCompleted()` 方法 - 修改 `src/modules/project/pages/project-detail/project-detail.component.html`: - 在 `` 之前添加审批面板 **修复后效果:** - ✅ 组长工作台点击待审批项目正确跳转到真实项目详情页 - ✅ 审批面板正常显示 - ✅ 四阶段导航正常工作 - ✅ 审批通过/驳回功能正常 **相关文件:** - `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` - 组长工作台(跳转逻辑) --- **测试验证步骤:** 1. 访问组长工作台:`http://localhost:4200/wxwork/cDL6R1hgSi/team-leader/dashboard` 2. 点击待审批项目(红桥新村) 3. 验证URL:应为 `http://localhost:4200/wxwork/cDL6R1hgSi/team-leader/project-detail/B2wtFHIF6k` 4. 验证页面显示: - ✅ 四阶段导航显示 - ✅ 审批面板显示(橙色横幅) - ✅ "通过审批"和"驳回订单"按钮显示 - ✅ 订单信息正确显示 --- ### 功能增强2:审批时修改设计师分配 **新增时间:** 2025-10-28 **功能描述:** 组长在审批订单时,可以直接在审批面板中修改设计师分配,无需返回订单页面,提高审批效率。 **实现内容:** 1. **编辑按钮** - 在"设计师分配"卡片右上角添加"✏️ 编辑"按钮 - 点击后进入编辑模式 2. **编辑模式界面** - 显示当前分配的设计师列表 - 每个设计师显示:头像、姓名、负责空间 - 提供操作按钮: - 🗑️ 移除设计师 - 编辑空间 - 修改该设计师负责的空间 - ➕ 添加设计师 - 添加新的设计师 3. **编辑操作** - **移除设计师**:点击🗑️按钮移除该设计师 - **编辑空间**:弹出输入框,修改空间列表(逗号分隔) - **添加设计师**:弹出输入框,输入设计师姓名 4. **保存/取消** - **保存修改**:更新审批数据和项目数据 - **取消**:放弃修改,恢复原始数据 **视觉效果:** - 编辑模式卡片:蓝色边框 + 浅蓝背景 - 设计师项:白色背景 + 悬停高亮 - 按钮:彩色图标 + 悬停动画 - 操作反馈:保存后显示"设计师分配已更新"提示 **技术实现:** ```typescript // TypeScript 关键方法 startEditTeams() // 开启编辑模式 cancelEditTeams() // 取消编辑 saveTeamsEdit() // 保存修改 removeTeam(index) // 移除设计师 addTeamMember() // 添加设计师 editTeamSpaces(team) // 编辑空间 ``` **涉及文件:** - `src/app/shared/components/order-approval-panel/order-approval-panel.component.ts` - `src/app/shared/components/order-approval-panel/order-approval-panel.component.html` - `src/app/shared/components/order-approval-panel/order-approval-panel.component.scss` **使用流程:** 1. 组长查看待审批订单 2. 在审批面板中点击"设计师分配"卡片右上角的"✏️ 编辑"按钮 3. 进入编辑模式,修改设计师分配: - 移除不合适的设计师 - 添加新的设计师(打开专业的设计师选择弹窗) - 调整各设计师负责的空间 4. 点击"保存修改"确认,或点击"取消"放弃修改 5. 继续进行审批操作(通过/驳回) **改进更新(2025-10-28):** 集成了项目详情页的设计师分配弹窗组件(`DesignerTeamAssignmentModalComponent`),提供更专业的设计师选择体验: **弹窗功能特性:** - ✅ **项目组视图**:按项目组展示设计师列表 - ✅ **设计师状态**:显示设计师工作状态(空闲/忙碌/对图中) - ✅ **工作量显示**:查看每个设计师当前工作量 - ✅ **日历视图**:查看设计师时间安排和可用性 - ✅ **空间分配**:选择设计师时同时分配负责空间 - ✅ **跨组协作**:支持选择多个项目组的设计师 - ✅ **实时数据**:从数据库加载真实的设计师和项目数据 **交互流程:** 1. 点击 **"➕ 添加设计师"** 按钮 2. 弹出专业的设计师选择界面 3. 选择项目组,查看该组设计师 4. 查看设计师状态、工作量、时间安排 5. 选择合适的设计师 6. 为设计师分配负责的空间 7. 确认选择,设计师自动添加到审批面板 8. 继续编辑或保存修改 **技术实现:** ```typescript // 关键方法更新 addTeamMember() // 打开设计师选择弹窗 closeDesignerModal() // 关闭弹窗 handleDesignerAssignment() // 处理弹窗返回的选择结果 ``` **涉及组件:** - `DesignerTeamAssignmentModalComponent` - 设计师分配弹窗(复用) - `DesignerCalendarComponent` - 设计师日历组件(弹窗内部使用)