|
@@ -0,0 +1,1638 @@
|
|
|
|
|
+# 组长端订单审批功能实施方案
|
|
|
|
|
+
|
|
|
|
|
+## 📅 创建日期
|
|
|
|
|
+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
|
|
|
|
|
+<!-- 位置: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>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```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
|
|
|
|
|
+<!-- 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>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```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
|
|
|
|
|
+<!-- 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>
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+<!-- 原有的项目详情内容 -->
|
|
|
|
|
+<!-- ... -->
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 三、客服端适配
|
|
|
|
|
+
|
|
|
|
|
+### 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') {
|
|
|
|
|
+ <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>
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3.3 显示驳回原因
|
|
|
|
|
+
|
|
|
|
|
+```html
|
|
|
|
|
+<!-- 驳回提示 -->
|
|
|
|
|
+@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>
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+```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<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 [];
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 五、通知机制(可选扩展)
|
|
|
|
|
+
|
|
|
|
|
+### 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`:
|
|
|
|
|
+ - 在 `<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` - 组长工作台(跳转逻辑)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**测试验证步骤:**
|
|
|
|
|
+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` - 设计师日历组件(弹窗内部使用)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|