# 项目管理 - 交付执行阶段 PRD
## 1. 功能概述
### 1.1 阶段定位
交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
### 1.2 核心目标
- 按空间维度组织文件上传和进度管理
- 实现四个执行阶段的串行推进
- 提供实时进度跟踪和状态可视化
- 支持组长审核和质量把控
- 确保交付物符合质量标准
### 1.3 涉及角色
- **设计师**:负责建模、软装、后期等设计工作
- **渲染师**:负责渲染阶段的大图输出
- **组长**:审核各阶段交付物、把控质量
- **技术**:验收最终交付物、确认质量
### 1.4 四大执行子阶段
```mermaid
graph LR
A[方案确认] --> B[建模]
B --> C[软装]
C --> D[渲染]
D --> E[后期]
E --> F[尾款结算]
style B fill:#e3f2fd
style C fill:#fff3e0
style D fill:#fce4ec
style E fill:#f3e5f5
```
## 2. 空间管理系统
### 2.1 空间数据结构
#### 2.1.1 DeliveryProcess 接口
```typescript
interface DeliveryProcess {
id: string; // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
name: string; // 流程名称:建模/软装/渲染/后期
type: 'modeling' | 'softDecor' | 'rendering' | 'postProcess';
isExpanded: boolean; // 是否展开
spaces: DeliverySpace[]; // 空间列表
content: {
[spaceId: string]: SpaceContent; // 按空间ID索引的内容
};
}
interface DeliverySpace {
id: string; // 空间ID
name: string; // 空间名称:卧室/客厅/厨房等
isExpanded: boolean; // 是否展开
order: number; // 排序顺序
}
interface SpaceContent {
images: Array<{
id: string;
name: string;
url: string;
size?: string;
reviewStatus?: 'pending' | 'approved' | 'rejected';
synced?: boolean; // 是否已同步到客户端
}>;
progress: number; // 进度 0-100
status: 'pending' | 'in_progress' | 'completed' | 'approved';
notes: string; // 备注信息
lastUpdated: Date; // 最后更新时间
}
```
#### 2.1.2 初始空间配置
```typescript
// project-detail.ts lines 458-523
deliveryProcesses: DeliveryProcess[] = [
{
id: 'modeling',
name: '建模',
type: 'modeling',
isExpanded: true, // 默认展开第一个
spaces: [
{ id: 'bedroom', name: '卧室', isExpanded: false, order: 1 },
{ id: 'living', name: '客厅', isExpanded: false, order: 2 },
{ id: 'kitchen', name: '厨房', isExpanded: false, order: 3 }
],
content: {
'bedroom': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
'living': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() },
'kitchen': { images: [], progress: 0, status: 'pending', notes: '', lastUpdated: new Date() }
}
},
// 软装、渲染、后期流程结构相同
];
```
### 2.2 空间管理功能
#### 2.2.1 添加新空间
```typescript
// project-detail.ts lines 5150-5184
addSpace(processId: string): void {
const spaceName = this.newSpaceName[processId]?.trim();
if (!spaceName) return;
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process) return;
// 生成新的空间ID
const spaceId = `space_${Date.now()}`;
// 添加到spaces数组
const newSpace: DeliverySpace = {
id: spaceId,
name: spaceName,
isExpanded: false,
order: process.spaces.length + 1
};
process.spaces.push(newSpace);
// 初始化content数据
process.content[spaceId] = {
images: [],
progress: 0,
status: 'pending',
notes: '',
lastUpdated: new Date()
};
// 清空输入框并隐藏
this.newSpaceName[processId] = '';
this.showAddSpaceInput[processId] = false;
console.log(`已添加空间: ${spaceName} 到流程 ${process.name}`);
}
```
**UI交互**:
```html
@if (showAddSpaceInput[process.id]) {
} @else {
}
```
#### 2.2.2 删除空间
```typescript
// project-detail.ts lines 5219-5242
removeSpace(processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process) return;
// 从spaces数组中移除
const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
if (spaceIndex > -1) {
const spaceName = process.spaces[spaceIndex].name;
process.spaces.splice(spaceIndex, 1);
// 清理content数据
if (process.content[spaceId]) {
// 释放图片URL资源
process.content[spaceId].images.forEach(img => {
if (img.url && img.url.startsWith('blob:')) {
URL.revokeObjectURL(img.url);
}
});
delete process.content[spaceId];
}
console.log(`已删除空间: ${spaceName} 从流程 ${process.name}`);
}
}
```
#### 2.2.3 空间展开/收起
```typescript
// project-detail.ts lines 5200-5208
toggleSpace(processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process) return;
const space = process.spaces.find(s => s.id === spaceId);
if (space) {
space.isExpanded = !space.isExpanded;
}
}
```
### 2.3 进度管理
#### 2.3.1 进度计算逻辑
```typescript
// project-detail.ts lines 5377-5397
private updateSpaceProgress(processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
const content = process.content[spaceId];
const imageCount = content.images.length;
// 根据图片数量和状态计算进度
if (imageCount === 0) {
content.progress = 0;
content.status = 'pending';
} else if (imageCount < 3) {
content.progress = Math.min(imageCount * 30, 90);
content.status = 'in_progress';
} else {
content.progress = 100;
content.status = 'completed';
}
content.lastUpdated = new Date();
}
```
**进度规则**:
- 0张图片:0%进度,状态为待开始
- 1-2张图片:30%-60%进度,状态为进行中
- 3张及以上:100%进度,状态为已完成
#### 2.3.2 获取空间进度
```typescript
// project-detail.ts lines 5211-5216
getSpaceProgress(processId: string, spaceId: string): number {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return 0;
return process.content[spaceId].progress || 0;
}
```
#### 2.3.3 进度可视化
```html
0 && getSpaceProgress(process.id, space.id) < 100"
[class.completed]="getSpaceProgress(process.id, space.id) === 100">
{{ getSpaceProgress(process.id, space.id) }}%
```
## 3. 建模阶段
### 3.1 功能特点
- 白模图片上传
- 模型检查项验证
- 户型匹配度检查
- 尺寸精度验证
### 3.2 白模上传
#### 3.2.1 文件上传处理
```typescript
// project-detail.ts lines 1838-1850
onWhiteModelSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
const items = files.map(f => this.makeImageItem(f));
this.whiteModelImages.unshift(...items);
input.value = '';
}
removeWhiteModelImage(id: string): void {
const target = this.whiteModelImages.find(i => i.id === id);
if (target) this.revokeUrl(target.url);
this.whiteModelImages = this.whiteModelImages.filter(i => i.id !== id);
}
```
#### 3.2.2 图片对象生成
```typescript
// project-detail.ts lines 1826-1830
private makeImageItem(file: File): { id: string; name: string; url: string; size: string } {
const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const url = URL.createObjectURL(file);
return { id, name: file.name, url, size: this.formatFileSize(file.size) };
}
```
#### 3.2.3 文件大小格式化
```typescript
// project-detail.ts lines 1815-1823
private formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(1)}KB`;
const mb = kb / 1024;
if (mb < 1024) return `${mb.toFixed(1)}MB`;
const gb = mb / 1024;
return `${gb.toFixed(2)}GB`;
}
```
### 3.3 模型检查项
#### 3.3.1 检查项数据结构
```typescript
interface ModelCheckItem {
id: string;
name: string;
isPassed: boolean;
notes: string;
}
// project-detail.ts lines 449-455
modelCheckItems: ModelCheckItem[] = [
{ id: 'check-1', name: '户型匹配度检查', isPassed: false, notes: '' },
{ id: 'check-2', name: '尺寸精度验证', isPassed: false, notes: '' },
{ id: 'check-3', name: '材质贴图检查', isPassed: false, notes: '' },
{ id: 'check-4', name: '光影效果验证', isPassed: false, notes: '' },
{ id: 'check-5', name: '细节完整性检查', isPassed: false, notes: '' }
];
```
#### 3.3.2 检查项UI
```html
```
### 3.4 建模阶段完成
#### 3.4.1 确认上传方法
```typescript
// project-detail.ts lines 1853-1866
confirmWhiteModelUpload(): void {
// 检查建模阶段的图片数据
const modelingProcess = this.deliveryProcesses.find(p => p.id === 'modeling');
if (!modelingProcess) return;
// 检查是否有任何空间上传了图片
const hasImages = modelingProcess.spaces.some(space => {
const content = modelingProcess.content[space.id];
return content && content.images && content.images.length > 0;
});
if (!hasImages) return;
this.advanceToNextStage('建模');
}
```
#### 3.4.2 阶段推进逻辑
```typescript
// project-detail.ts lines 1391-1423
advanceToNextStage(afterStage: ProjectStage): void {
const idx = this.stageOrder.indexOf(afterStage);
if (idx >= 0 && idx < this.stageOrder.length - 1) {
const next = this.stageOrder[idx + 1];
// 更新项目阶段
this.updateProjectStage(next);
// 更新展开状态,折叠当前、展开下一阶段
this.expandedStages[afterStage] = false;
this.expandedStages[next] = true;
// 更新板块展开状态
const nextSection = this.getSectionKeyForStage(next);
this.expandedSection = nextSection;
// 触发变更检测以更新导航栏颜色
this.cdr.detectChanges();
}
}
```
## 4. 软装阶段
### 4.1 功能特点
- 小图上传(建议≤1MB,不强制)
- 支持拖拽上传
- 实时预览功能
- 按空间组织
### 4.2 小图上传
#### 4.2.1 文件选择处理
```typescript
// project-detail.ts lines 1869-1881
onSoftDecorSmallPicsSelected(event: Event): void {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
const warnOversize = files.filter(f => f.size > 1024 * 1024);
if (warnOversize.length > 0) {
// 仅提示,不阻断
console.warn('软装小图建议≤1MB,以下文件较大:', warnOversize.map(f => f.name));
}
const items = files.map(f => this.makeImageItem(f));
this.softDecorImages.unshift(...items);
input.value = '';
}
```
**文件大小校验**:
- 建议≤1MB,超过仅警告不阻断
- 支持 JPG、JPEG、PNG 格式
- 自动过滤非图片文件
#### 4.2.2 拖拽上传支持
```typescript
// project-detail.ts lines 1956-1998
onDragOver(event: DragEvent): void {
event.preventDefault();
event.stopPropagation();
this.isDragOver = true;
}
onDragLeave(event: DragEvent): void {
event.preventDefault();
event.stopPropagation();
this.isDragOver = false;
}
onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render' | 'postProcess'): void {
event.preventDefault();
event.stopPropagation();
this.isDragOver = false;
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return;
// 创建模拟的input事件
const mockEvent = {
target: {
files: files
}
} as any;
// 根据类型调用相应的处理方法
switch (type) {
case 'softDecor':
this.onSoftDecorSmallPicsSelected(mockEvent);
break;
// ... 其他类型
}
}
```
**拖拽区域样式**:
```html
拖拽图片到此处上传
或点击选择文件(建议≤1MB)
```
### 4.3 图片预览
#### 4.3.1 预览功能
```typescript
// project-detail.ts lines 1890-1903
previewImage(img: any): void {
const isRenderLarge = !!this.renderLargeImages.find(i => i.id === img?.id);
if (isRenderLarge && img?.locked) {
alert('该渲染大图已加锁,需完成尾款结算并上传/识别支付凭证后方可预览。');
return;
}
this.previewImageData = img;
this.showImagePreview = true;
}
closeImagePreview(): void {
this.showImagePreview = false;
this.previewImageData = null;
}
```
#### 4.3.2 预览弹窗
```html
@if (showImagePreview && previewImageData) {
}
```
### 4.4 软装阶段完成
```typescript
// project-detail.ts lines 2098-2111
confirmSoftDecorUpload(): void {
// 检查软装阶段的图片数据
const softDecorProcess = this.deliveryProcesses.find(p => p.id === 'soft-decoration');
if (!softDecorProcess) return;
// 检查是否有任何空间上传了图片
const hasImages = softDecorProcess.spaces.some(space => {
const content = softDecorProcess.content[space.id];
return content && content.images && content.images.length > 0;
});
if (!hasImages) return;
this.advanceToNextStage('软装');
}
```
## 5. 渲染阶段
### 5.1 功能特点
- 4K图片强制校验(最大边≥4000像素)
- 渲染大图自动加锁
- 渲染进度监控
- 异常反馈系统
### 5.2 4K图片校验
#### 5.2.1 图片尺寸验证
```typescript
// 4K校验方法
private async validateImage4K(file: File): Promise {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
const maxDimension = Math.max(img.width, img.height);
// 4K标准:最大边需≥4000像素
if (maxDimension >= 4000) {
resolve(true);
} else {
resolve(false);
}
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('图片加载失败'));
};
img.src = url;
});
}
```
#### 5.2.2 渲染大图上传
```typescript
// project-detail.ts lines 2142-2164
async onRenderLargePicsSelected(event: Event): Promise {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
for (const f of files) {
const ok = await this.validateImage4K(f).catch(() => false);
if (!ok) {
alert(`图片不符合4K标准(最大边需≥4000像素):${f.name}`);
continue;
}
const item = this.makeImageItem(f);
// 直接添加到正式列表,渲染大图默认加锁
this.renderLargeImages.unshift({
id: item.id,
name: item.name,
url: item.url,
size: this.formatFileSize(f.size),
locked: true // 渲染大图默认加锁
});
}
input.value = '';
}
```
**校验规则**:
- 支持 JPG、JPEG、PNG 格式
- 最大边(宽或高)必须≥4000像素
- 不符合标准的图片拒绝上传并提示
### 5.3 渲染大图加锁机制
#### 5.3.1 加锁逻辑
```typescript
// 渲染大图默认加锁
renderLargeImages: Array<{
id: string;
name: string;
url: string;
size?: string;
locked?: boolean; // 加锁标记
reviewStatus?: 'pending' | 'approved' | 'rejected';
synced?: boolean;
}> = [];
```
**加锁规则**:
- 所有渲染大图上传后自动加锁
- 加锁状态下不可预览和下载
- 需完成尾款结算后自动解锁
#### 5.3.2 解锁逻辑
```typescript
// 尾款到账后自动解锁
onPaymentReceived(paymentInfo?: any): void {
// 更新结算状态
this.settlementRecord.status = 'completed';
this.settlementRecord.paidAmount = paymentInfo?.amount || this.settlementRecord.remainingAmount;
this.settlementRecord.paidAt = new Date();
// 解锁渲染大图
this.autoUnlockAndSendImages();
// 发送支付确认通知
this.sendPaymentConfirmationNotifications();
}
private autoUnlockAndSendImages(): void {
// 解锁所有渲染大图
this.renderLargeImages.forEach(img => {
img.locked = false;
});
console.log('✅ 渲染大图已自动解锁');
alert('尾款已到账,渲染大图已解锁!客服可发送给客户。');
}
```
### 5.4 渲染异常反馈
#### 5.4.1 异常类型
```typescript
type ExceptionType = 'failed' | 'stuck' | 'quality' | 'other';
interface ExceptionHistory {
id: string;
type: ExceptionType;
description: string;
submitTime: Date;
status: '待处理' | '处理中' | '已解决';
screenshotUrl?: string;
resolver?: string;
resolvedAt?: Date;
}
```
#### 5.4.2 提交异常反馈
```typescript
// project-detail.ts lines 1715-1749
submitExceptionFeedback(): void {
if (!this.exceptionDescription.trim() || this.isSubmittingFeedback) {
alert('请填写异常类型和描述');
return;
}
this.isSubmittingFeedback = true;
// 模拟提交反馈到服务器
setTimeout(() => {
const newException: ExceptionHistory = {
id: `exception-${Date.now()}`,
type: this.exceptionType,
description: this.exceptionDescription,
submitTime: new Date(),
status: '待处理'
};
// 添加到历史记录中
this.exceptionHistories.unshift(newException);
// 通知客服和技术支持
this.notifyTechnicalSupport(newException);
// 清空表单
this.exceptionDescription = '';
this.clearExceptionScreenshot();
this.showExceptionForm = false;
// 显示成功消息
alert('异常反馈已提交,技术支持将尽快处理');
this.isSubmittingFeedback = false;
}, 1000);
}
```
#### 5.4.3 异常反馈UI
```html
渲染异常反馈
@if (showExceptionForm) {
}
异常记录
@for (exception of exceptionHistories; track exception.id) {
{{ exception.description }}
{{ formatDateTime(exception.submitTime) }}
}
```
### 5.5 渲染阶段完成
```typescript
// project-detail.ts lines 2114-2127
confirmRenderUpload(): void {
// 检查渲染阶段的图片数据
const renderProcess = this.deliveryProcesses.find(p => p.id === 'rendering');
if (!renderProcess) return;
// 检查是否有任何空间上传了图片
const hasImages = renderProcess.spaces.some(space => {
const content = renderProcess.content[space.id];
return content && content.images && content.images.length > 0;
});
if (!hasImages) return;
this.advanceToNextStage('渲染');
}
```
## 6. 后期阶段
### 6.1 功能特点
- 最终图片处理
- 色彩校正确认
- 细节优化验证
- 交付物整理
### 6.2 后期图片上传
```typescript
// project-detail.ts lines 2030-2051
async onPostProcessPicsSelected(event: Event): Promise {
const input = event.target as HTMLInputElement;
if (!input.files || input.files.length === 0) return;
const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
for (const f of files) {
const item = this.makeImageItem(f);
this.postProcessImages.unshift({
id: item.id,
name: item.name,
url: item.url,
size: this.formatFileSize(f.size)
});
}
input.value = '';
}
removePostProcessImage(id: string): void {
const target = this.postProcessImages.find(i => i.id === id);
if (target) this.revokeUrl(target.url);
this.postProcessImages = this.postProcessImages.filter(i => i.id !== id);
}
```
### 6.3 后期处理项
**常见后期处理任务**:
- 色彩校正和调整
- 亮度/对比度优化
- 细节锐化
- 瑕疵修复
- 水印添加(可选)
- 文件格式转换
### 6.4 后期阶段完成
```typescript
// project-detail.ts lines 2054-2067
confirmPostProcessUpload(): void {
// 检查后期阶段的图片数据
const postProcessProcess = this.deliveryProcesses.find(p => p.id === 'post-processing');
if (!postProcessProcess) return;
// 检查是否有任何空间上传了图片
const hasImages = postProcessProcess.spaces.some(space => {
const content = postProcessProcess.content[space.id];
return content && content.images && content.images.length > 0;
});
if (!hasImages) return;
this.advanceToNextStage('后期');
}
```
## 7. 统一空间文件处理
### 7.1 空间文件上传
#### 7.1.1 触发文件选择
```typescript
// project-detail.ts lines 5244-5251
triggerSpaceFileInput(processId: string, spaceId: string): void {
const inputId = `space-file-input-${processId}-${spaceId}`;
const input = document.getElementById(inputId) as HTMLInputElement;
if (input) {
input.click();
}
}
```
#### 7.1.2 处理空间文件
```typescript
// project-detail.ts lines 5265-5284
private handleSpaceFiles(files: File[], processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
files.forEach(file => {
if (/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)) {
const imageItem = this.makeImageItem(file);
process.content[spaceId].images.push({
id: imageItem.id,
name: imageItem.name,
url: imageItem.url,
size: this.formatFileSize(file.size),
reviewStatus: 'pending'
});
// 更新进度
this.updateSpaceProgress(processId, spaceId);
}
});
}
```
#### 7.1.3 空间文件拖拽
```typescript
// project-detail.ts lines 5254-5262
onSpaceFileDrop(event: DragEvent, processId: string, spaceId: string): void {
event.preventDefault();
event.stopPropagation();
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return;
this.handleSpaceFiles(Array.from(files), processId, spaceId);
}
```
### 7.2 获取空间图片列表
```typescript
// project-detail.ts lines 5287-5292
getSpaceImages(processId: string, spaceId: string): Array<{
id: string;
name: string;
url: string;
size?: string;
reviewStatus?: 'pending' | 'approved' | 'rejected'
}> {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return [];
return process.content[spaceId].images || [];
}
```
### 7.3 空间图片删除
```typescript
// 从空间中删除图片
removeSpaceImage(processId: string, spaceId: string, imageId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
const images = process.content[spaceId].images;
const imageIndex = images.findIndex(img => img.id === imageId);
if (imageIndex > -1) {
// 释放URL资源
const image = images[imageIndex];
if (image.url && image.url.startsWith('blob:')) {
URL.revokeObjectURL(image.url);
}
// 从数组中移除
images.splice(imageIndex, 1);
// 更新进度
this.updateSpaceProgress(processId, spaceId);
}
}
```
## 8. 审核流程
### 8.1 审核状态管理
#### 8.1.1 审核状态枚举
```typescript
type ReviewStatus = 'pending' | 'approved' | 'rejected';
interface ImageWithReview {
id: string;
name: string;
url: string;
size?: string;
reviewStatus?: ReviewStatus;
reviewNotes?: string;
reviewedBy?: string;
reviewedAt?: Date;
synced?: boolean; // 是否已同步到客户端
}
```
#### 8.1.2 审核操作
```typescript
// 组长审核图片
reviewSpaceImage(
processId: string,
spaceId: string,
imageId: string,
status: ReviewStatus,
notes?: string
): void {
if (!this.isTeamLeaderView()) {
alert('仅组长可以审核图片');
return;
}
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
const image = process.content[spaceId].images.find(img => img.id === imageId);
if (!image) return;
// 更新审核状态
image.reviewStatus = status;
image.reviewNotes = notes;
image.reviewedBy = this.getCurrentUserName();
image.reviewedAt = new Date();
// 如果审核通过,标记为已同步
if (status === 'approved') {
image.synced = true;
}
console.log(`图片审核完成: ${image.name} - ${status}`);
}
```
### 8.2 批量审核
```typescript
// 批量审核空间内所有图片
batchReviewSpaceImages(
processId: string,
spaceId: string,
status: ReviewStatus
): void {
if (!this.isTeamLeaderView()) {
alert('仅组长可以批量审核图片');
return;
}
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
const images = process.content[spaceId].images;
const pendingImages = images.filter(img => img.reviewStatus === 'pending');
if (pendingImages.length === 0) {
alert('没有待审核的图片');
return;
}
const confirmed = confirm(
`确定要批量${status === 'approved' ? '通过' : '驳回'}${pendingImages.length}张图片吗?`
);
if (!confirmed) return;
pendingImages.forEach(image => {
image.reviewStatus = status;
image.reviewedBy = this.getCurrentUserName();
image.reviewedAt = new Date();
if (status === 'approved') {
image.synced = true;
}
});
alert(`已批量审核${pendingImages.length}张图片`);
}
```
### 8.3 审核统计
```typescript
// 获取空间审核统计
getSpaceReviewStats(processId: string, spaceId: string): {
total: number;
pending: number;
approved: number;
rejected: number;
} {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) {
return { total: 0, pending: 0, approved: 0, rejected: 0 };
}
const images = process.content[spaceId].images;
return {
total: images.length,
pending: images.filter(img => img.reviewStatus === 'pending').length,
approved: images.filter(img => img.reviewStatus === 'approved').length,
rejected: images.filter(img => img.reviewStatus === 'rejected').length
};
}
```
## 9. 权限控制
### 9.1 角色权限矩阵
| 操作 | 设计师 | 渲染师 | 组长 | 技术 |
|-----|--------|--------|------|------|
| 查看交付执行板块 | ✅ | ✅ | ✅ | ✅ |
| 上传建模图片 | ✅ | ❌ | ✅ | ❌ |
| 上传软装图片 | ✅ | ❌ | ✅ | ❌ |
| 上传渲染图片 | ❌ | ✅ | ✅ | ❌ |
| 上传后期图片 | ✅ | ❌ | ✅ | ❌ |
| 添加/删除空间 | ✅ | ✅ | ✅ | ❌ |
| 审核图片 | ❌ | ❌ | ✅ | ❌ |
| 确认阶段完成 | ✅ | ✅ | ✅ | ❌ |
| 报告渲染异常 | ❌ | ✅ | ✅ | ❌ |
| 最终验收 | ❌ | ❌ | ❌ | ✅ |
### 9.2 权限检查方法
```typescript
// project-detail.ts lines 911-936
isDesignerView(): boolean {
return this.roleContext === 'designer';
}
isTeamLeaderView(): boolean {
return this.roleContext === 'team-leader';
}
isTechnicalView(): boolean {
return this.roleContext === 'technical';
}
canEditSection(sectionKey: SectionKey): boolean {
if (this.isCustomerServiceView()) {
return sectionKey === 'order' ||
sectionKey === 'requirements' ||
sectionKey === 'aftercare';
}
return true; // 设计师和组长可以编辑所有板块
}
canEditStage(stage: ProjectStage): boolean {
if (this.isCustomerServiceView()) {
const editableStages: ProjectStage[] = [
'订单分配', '需求沟通', '方案确认',
'尾款结算', '客户评价', '投诉处理'
];
return editableStages.includes(stage);
}
return true;
}
```
### 9.3 UI权限控制
```html
@if (!isReadOnly() && canEditStage('建模')) {
}
@if (isTeamLeaderView()) {
}
@if (!isReadOnly() && (isDesignerView() || isTeamLeaderView())) {
}
```
## 10. 数据流转
### 10.1 阶段推进流程
```mermaid
sequenceDiagram
participant Designer as 设计师
participant System as 系统
participant Leader as 组长
participant Next as 下一阶段
Designer->>System: 上传图片到空间
System->>System: 更新空间进度
System->>System: 计算阶段完成度
Designer->>System: 确认阶段上传
System->>System: 验证图片数量
alt 有图片
System->>Leader: 通知审核
Leader->>System: 审核图片
System->>Next: 推进到下一阶段
else 无图片
System->>Designer: 提示先上传图片
end
```
### 10.2 进度同步机制
```typescript
// 更新空间进度后同步到项目
private syncProgressToProject(processId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process) return;
// 计算整体进度
const spaces = process.spaces;
const totalProgress = spaces.reduce((sum, space) => {
return sum + (process.content[space.id]?.progress || 0);
}, 0);
const averageProgress = spaces.length > 0
? Math.round(totalProgress / spaces.length)
: 0;
// 更新项目进度
if (this.project) {
this.project.progress = averageProgress;
}
// 触发变更检测
this.cdr.detectChanges();
}
```
### 10.3 客户端数据同步
```typescript
// 审核通过后同步到客户端
private syncApprovedImagesToClient(processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
const approvedImages = process.content[spaceId].images
.filter(img => img.reviewStatus === 'approved' && !img.synced);
if (approvedImages.length === 0) return;
// 调用API同步到客户端
this.projectService.syncImagesToClient(
this.projectId,
processId,
spaceId,
approvedImages.map(img => img.id)
).subscribe({
next: (result) => {
if (result.success) {
// 标记为已同步
approvedImages.forEach(img => {
img.synced = true;
});
console.log(`已同步${approvedImages.length}张图片到客户端`);
}
},
error: (error) => {
console.error('同步图片失败:', error);
}
});
}
```
## 11. 异常处理
### 11.1 文件上传失败
```typescript
// 文件上传错误处理
private handleFileUploadError(error: any, fileName: string): void {
let errorMessage = '文件上传失败';
if (error.status === 413) {
errorMessage = `文件过大:${fileName}(最大10MB)`;
} else if (error.status === 415) {
errorMessage = `不支持的文件格式:${fileName}`;
} else if (error.status === 500) {
errorMessage = '服务器错误,请稍后重试';
}
alert(errorMessage);
console.error('文件上传失败:', error);
}
```
### 11.2 4K校验失败
```typescript
// 4K校验失败处理
private handle4KValidationFailure(file: File, dimensions: {width: number; height: number}): void {
const maxDimension = Math.max(dimensions.width, dimensions.height);
const message = `
图片不符合4K标准
文件名: ${file.name}
当前尺寸: ${dimensions.width} × ${dimensions.height}
最大边: ${maxDimension}px
要求: 最大边 ≥ 4000px
请使用符合4K标准的图片重新上传。
`;
alert(message);
console.warn('4K校验失败:', file.name, dimensions);
}
```
### 11.3 渲染异常处理
```typescript
// 渲染超时预警
checkRenderTimeout(): void {
if (!this.renderProgress || !this.project) return;
const deliveryTime = new Date(this.project.deadline);
const currentTime = new Date();
const timeDifference = deliveryTime.getTime() - currentTime.getTime();
const hoursRemaining = Math.floor(timeDifference / (1000 * 60 * 60));
if (hoursRemaining <= 3 && hoursRemaining > 0) {
alert('渲染进度预警:交付前3小时,请关注渲染进度');
}
if (hoursRemaining <= 1 && hoursRemaining > 0) {
alert('渲染进度严重预警:交付前1小时,渲染可能无法按时完成!');
this.notifyTeamLeader('render-failed');
}
}
```
### 11.4 空间操作失败
```typescript
// 删除空间时的安全检查
removeSpace(processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process) return;
const space = process.spaces.find(s => s.id === spaceId);
if (!space) return;
// 检查空间是否有图片
const hasImages = process.content[spaceId]?.images?.length > 0;
if (hasImages) {
const confirmed = confirm(
`空间"${space.name}"中有${process.content[spaceId].images.length}张图片,确定要删除吗?\n删除后图片将无法恢复。`
);
if (!confirmed) return;
}
// 执行删除
const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
if (spaceIndex > -1) {
process.spaces.splice(spaceIndex, 1);
// 清理资源
if (process.content[spaceId]) {
process.content[spaceId].images.forEach(img => {
if (img.url && img.url.startsWith('blob:')) {
URL.revokeObjectURL(img.url);
}
});
delete process.content[spaceId];
}
console.log(`已删除空间: ${space.name}`);
}
}
```
## 12. 性能优化
### 12.1 Blob URL管理
```typescript
// 组件销毁时清理所有Blob URL
ngOnDestroy(): void {
// 释放所有 blob 预览 URL
const revokeList: string[] = [];
// 收集所有Blob URL
this.deliveryProcesses.forEach(process => {
Object.values(process.content).forEach(content => {
content.images.forEach(img => {
if (img.url && img.url.startsWith('blob:')) {
revokeList.push(img.url);
}
});
});
});
// 批量释放
revokeList.forEach(url => URL.revokeObjectURL(url));
console.log(`已释放${revokeList.length}个Blob URL`);
}
```
### 12.2 图片懒加载
```typescript
// 使用Intersection Observer实现懒加载
private setupImageLazyLoading(): void {
if (!('IntersectionObserver' in window)) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
const src = img.dataset['src'];
if (src) {
img.src = src;
observer.unobserve(img);
}
}
});
}, {
rootMargin: '50px' // 提前50px开始加载
});
// 观察所有懒加载图片
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
}
```
### 12.3 进度计算优化
```typescript
// 使用防抖避免频繁计算
private progressUpdateDebounce: any;
private updateSpaceProgress(processId: string, spaceId: string): void {
// 清除之前的定时器
if (this.progressUpdateDebounce) {
clearTimeout(this.progressUpdateDebounce);
}
// 延迟300ms执行
this.progressUpdateDebounce = setTimeout(() => {
this.doUpdateSpaceProgress(processId, spaceId);
}, 300);
}
private doUpdateSpaceProgress(processId: string, spaceId: string): void {
const process = this.deliveryProcesses.find(p => p.id === processId);
if (!process || !process.content[spaceId]) return;
const content = process.content[spaceId];
const imageCount = content.images.length;
// 计算进度
if (imageCount === 0) {
content.progress = 0;
content.status = 'pending';
} else if (imageCount < 3) {
content.progress = Math.min(imageCount * 30, 90);
content.status = 'in_progress';
} else {
content.progress = 100;
content.status = 'completed';
}
content.lastUpdated = new Date();
// 同步到项目
this.syncProgressToProject(processId);
}
```
## 13. 测试用例
### 13.1 空间管理测试
```typescript
describe('Space Management', () => {
it('should add new space to process', () => {
component.newSpaceName['modeling'] = '书房';
component.addSpace('modeling');
const modelingProcess = component.deliveryProcesses.find(p => p.id === 'modeling');
expect(modelingProcess?.spaces.length).toBe(4);
expect(modelingProcess?.spaces[3].name).toBe('书房');
expect(modelingProcess?.content['space_*']).toBeDefined();
});
it('should remove space and clean up resources', () => {
const process = component.deliveryProcesses[0];
const spaceId = process.spaces[0].id;
// 添加一些图片
process.content[spaceId].images = [
{ id: '1', name: 'test.jpg', url: 'blob:test', size: '1MB' }
];
component.removeSpace(process.id, spaceId);
expect(process.spaces.length).toBe(2);
expect(process.content[spaceId]).toBeUndefined();
});
it('should toggle space expansion', () => {
const process = component.deliveryProcesses[0];
const space = process.spaces[0];
const initialState = space.isExpanded;
component.toggleSpace(process.id, space.id);
expect(space.isExpanded).toBe(!initialState);
});
});
```
### 13.2 文件上传测试
```typescript
describe('File Upload', () => {
it('should validate 4K images correctly', async () => {
const file = new File([''], 'test-4k.jpg', { type: 'image/jpeg' });
// Mock image dimensions
spyOn(component, 'validateImage4K').and.returnValue(Promise.resolve(true));
await component.onRenderLargePicsSelected({
target: { files: [file] }
} as any);
expect(component.renderLargeImages.length).toBeGreaterThan(0);
expect(component.renderLargeImages[0].locked).toBe(true);
});
it('should reject non-4K images', async () => {
const file = new File([''], 'small.jpg', { type: 'image/jpeg' });
spyOn(component, 'validateImage4K').and.returnValue(Promise.resolve(false));
spyOn(window, 'alert');
await component.onRenderLargePicsSelected({
target: { files: [file] }
} as any);
expect(window.alert).toHaveBeenCalledWith(jasmine.stringContaining('不符合4K标准'));
});
it('should handle soft decor upload with size warning', () => {
const largeFile = new File(['x'.repeat(2 * 1024 * 1024)], 'large.jpg', { type: 'image/jpeg' });
spyOn(console, 'warn');
component.onSoftDecorSmallPicsSelected({
target: { files: [largeFile] }
} as any);
expect(console.warn).toHaveBeenCalled();
expect(component.softDecorImages.length).toBeGreaterThan(0);
});
});
```
### 13.3 进度更新测试
```typescript
describe('Progress Tracking', () => {
it('should update space progress based on image count', () => {
const process = component.deliveryProcesses[0];
const spaceId = process.spaces[0].id;
// 添加2张图片
process.content[spaceId].images = [
{ id: '1', name: 'img1.jpg', url: 'blob:1' },
{ id: '2', name: 'img2.jpg', url: 'blob:2' }
];
component['updateSpaceProgress'](process.id, spaceId);
expect(process.content[spaceId].progress).toBe(60);
expect(process.content[spaceId].status).toBe('in_progress');
});
it('should mark as completed with 3+ images', () => {
const process = component.deliveryProcesses[0];
const spaceId = process.spaces[0].id;
process.content[spaceId].images = [
{ id: '1', name: 'img1.jpg', url: 'blob:1' },
{ id: '2', name: 'img2.jpg', url: 'blob:2' },
{ id: '3', name: 'img3.jpg', url: 'blob:3' }
];
component['updateSpaceProgress'](process.id, spaceId);
expect(process.content[spaceId].progress).toBe(100);
expect(process.content[spaceId].status).toBe('completed');
});
});
```
### 13.4 阶段推进测试
```typescript
describe('Stage Progression', () => {
it('should advance to next stage after confirmation', () => {
// 设置建模阶段有图片
const modelingProcess = component.deliveryProcesses.find(p => p.id === 'modeling');
if (modelingProcess) {
modelingProcess.content['bedroom'].images = [
{ id: '1', name: 'test.jpg', url: 'blob:test' }
];
}
component.currentStage = '建模';
component.confirmWhiteModelUpload();
expect(component.currentStage).toBe('软装');
expect(component.expandedStages['软装']).toBe(true);
expect(component.expandedStages['建模']).toBe(false);
});
it('should not advance without images', () => {
component.currentStage = '建模';
const initialStage = component.currentStage;
component.confirmWhiteModelUpload();
expect(component.currentStage).toBe(initialStage);
});
});
```
---
**文档版本**:v1.0.0
**创建日期**:2025-10-16
**最后更新**:2025-10-16
**维护人**:产品团队