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 - 设计师日历组件(弹窗内部使用)