# 项目管理 - 交付执行阶段 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
{{ 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

模型检查项

@for (item of modelCheckItems; track item.id) {
}
``` ### 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) {
@if (exceptionScreenshotUrl) { }
}
异常记录
@for (exception of exceptionHistories; track exception.id) {
{{ getExceptionTypeText(exception.type) }} {{ exception.status }}

{{ 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 **维护人**:产品团队