|
|
@@ -0,0 +1,461 @@
|
|
|
+# 项目空间与交付物统计服务使用指南
|
|
|
+
|
|
|
+## 概述
|
|
|
+
|
|
|
+`ProjectSpaceDeliverableService` 是一个封装的服务,用于计算项目中的空间数量和每个空间对应的交付物上传状态。这个服务可以在不同的地方使用,如组长端的项目时间轴、看板等。
|
|
|
+
|
|
|
+## 功能特性
|
|
|
+
|
|
|
+✅ **空间统计**:根据 Product 表计算项目中有多少个空间
|
|
|
+✅ **交付物统计**:根据 ProjectFile 表统计每个空间的交付物上传情况
|
|
|
+✅ **多类型支持**:支持白模、软装、渲染、后期四种交付类型
|
|
|
+✅ **自动去重**:自动去除重复的空间(按名称)
|
|
|
+✅ **完成率计算**:自动计算每个空间和整体的完成率
|
|
|
+✅ **状态标签**:提供状态文本和颜色标识
|
|
|
+
|
|
|
+## 服务位置
|
|
|
+
|
|
|
+```
|
|
|
+src/modules/project/services/project-space-deliverable.service.ts
|
|
|
+```
|
|
|
+
|
|
|
+## 数据结构
|
|
|
+
|
|
|
+### 空间交付物信息 (SpaceDeliverableInfo)
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface SpaceDeliverableInfo {
|
|
|
+ spaceId: string; // 空间ID(Product ID)
|
|
|
+ spaceName: string; // 空间名称
|
|
|
+ spaceType: string; // 空间类型
|
|
|
+ deliverableTypes: {
|
|
|
+ whiteModel: number; // 白模文件数量
|
|
|
+ softDecor: number; // 软装文件数量
|
|
|
+ rendering: number; // 渲染文件数量
|
|
|
+ postProcess: number; // 后期文件数量
|
|
|
+ };
|
|
|
+ totalFiles: number; // 总文件数
|
|
|
+ hasDeliverables: boolean; // 是否已上传交付物
|
|
|
+ completionRate: number; // 完成度(0-100)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 项目统计摘要 (ProjectSpaceDeliverableSummary)
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface ProjectSpaceDeliverableSummary {
|
|
|
+ projectId: string; // 项目ID
|
|
|
+ projectName: string; // 项目名称
|
|
|
+ totalSpaces: number; // 空间总数
|
|
|
+ spacesWithDeliverables: number; // 已上传交付物的空间数
|
|
|
+ spaces: SpaceDeliverableInfo[]; // 空间详细列表
|
|
|
+ totalDeliverableFiles: number; // 总交付文件数
|
|
|
+ totalByType: { // 各类型总计
|
|
|
+ whiteModel: number;
|
|
|
+ softDecor: number;
|
|
|
+ rendering: number;
|
|
|
+ postProcess: number;
|
|
|
+ };
|
|
|
+ overallCompletionRate: number; // 整体完成率(0-100)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 主要方法
|
|
|
+
|
|
|
+### 1. 获取项目空间与交付物统计摘要
|
|
|
+
|
|
|
+```typescript
|
|
|
+async getProjectSpaceDeliverableSummary(projectId: string): Promise<ProjectSpaceDeliverableSummary>
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:获取项目的完整统计信息
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { ProjectSpaceDeliverableService } from '@modules/project/services/project-space-deliverable.service';
|
|
|
+
|
|
|
+constructor(
|
|
|
+ private projectSpaceDeliverableService: ProjectSpaceDeliverableService
|
|
|
+) {}
|
|
|
+
|
|
|
+async loadProjectStats() {
|
|
|
+ const summary = await this.projectSpaceDeliverableService
|
|
|
+ .getProjectSpaceDeliverableSummary('project123');
|
|
|
+
|
|
|
+ console.log(`项目有 ${summary.totalSpaces} 个空间`);
|
|
|
+ console.log(`已上传交付物的空间:${summary.spacesWithDeliverables} 个`);
|
|
|
+ console.log(`总完成率:${summary.overallCompletionRate}%`);
|
|
|
+
|
|
|
+ // 遍历每个空间的详细信息
|
|
|
+ summary.spaces.forEach(space => {
|
|
|
+ console.log(`空间:${space.spaceName}`);
|
|
|
+ console.log(` 白模:${space.deliverableTypes.whiteModel} 个文件`);
|
|
|
+ console.log(` 软装:${space.deliverableTypes.softDecor} 个文件`);
|
|
|
+ console.log(` 渲染:${space.deliverableTypes.rendering} 个文件`);
|
|
|
+ console.log(` 后期:${space.deliverableTypes.postProcess} 个文件`);
|
|
|
+ console.log(` 完成率:${space.completionRate}%`);
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 检查项目是否所有空间都已交付
|
|
|
+
|
|
|
+```typescript
|
|
|
+async isAllSpacesDelivered(projectId: string): Promise<boolean>
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:快速检查项目是否全部完成
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+const isCompleted = await this.projectSpaceDeliverableService
|
|
|
+ .isAllSpacesDelivered('project123');
|
|
|
+
|
|
|
+if (isCompleted) {
|
|
|
+ console.log('✅ 项目所有空间都已完成交付');
|
|
|
+} else {
|
|
|
+ console.log('⚠️ 还有空间未完成交付');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 获取未完成空间列表
|
|
|
+
|
|
|
+```typescript
|
|
|
+async getIncompleteSpaces(projectId: string): Promise<string[]>
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:获取还未上传交付物的空间名称列表
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+const incompleteSpaces = await this.projectSpaceDeliverableService
|
|
|
+ .getIncompleteSpaces('project123');
|
|
|
+
|
|
|
+if (incompleteSpaces.length > 0) {
|
|
|
+ console.log('未完成的空间:', incompleteSpaces.join(', '));
|
|
|
+ // 例如:未完成的空间:客厅, 主卧, 厨房
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4. 获取项目交付进度百分比
|
|
|
+
|
|
|
+```typescript
|
|
|
+async getProjectDeliveryProgress(projectId: string): Promise<number>
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:获取项目的整体完成进度(0-100)
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+const progress = await this.projectSpaceDeliverableService
|
|
|
+ .getProjectDeliveryProgress('project123');
|
|
|
+
|
|
|
+console.log(`项目完成进度:${progress}%`);
|
|
|
+
|
|
|
+// 在进度条中使用
|
|
|
+this.progressWidth = `${progress}%`;
|
|
|
+```
|
|
|
+
|
|
|
+### 5. 获取交付状态标签
|
|
|
+
|
|
|
+```typescript
|
|
|
+getDeliveryStatusLabel(completionRate: number): string
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:根据完成率获取状态文本
|
|
|
+
|
|
|
+**返回值**:
|
|
|
+- `0%`: "未开始"
|
|
|
+- `1-24%`: "刚开始"
|
|
|
+- `25-49%`: "进行中"
|
|
|
+- `50-74%`: "接近完成"
|
|
|
+- `75-99%`: "即将完成"
|
|
|
+- `100%`: "已完成"
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+const rate = 65;
|
|
|
+const status = this.projectSpaceDeliverableService
|
|
|
+ .getDeliveryStatusLabel(rate);
|
|
|
+
|
|
|
+console.log(status); // "接近完成"
|
|
|
+```
|
|
|
+
|
|
|
+### 6. 获取交付状态颜色
|
|
|
+
|
|
|
+```typescript
|
|
|
+getDeliveryStatusColor(completionRate: number): string
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:根据完成率获取对应的颜色值
|
|
|
+
|
|
|
+**返回值**:
|
|
|
+- `0%`: `#94a3b8` (灰色)
|
|
|
+- `1-24%`: `#fbbf24` (黄色)
|
|
|
+- `25-49%`: `#fb923c` (橙色)
|
|
|
+- `50-74%`: `#60a5fa` (蓝色)
|
|
|
+- `75-99%`: `#818cf8` (紫色)
|
|
|
+- `100%`: `#34d399` (绿色)
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+const rate = 75;
|
|
|
+const color = this.projectSpaceDeliverableService
|
|
|
+ .getDeliveryStatusColor(rate);
|
|
|
+
|
|
|
+// 在HTML中使用
|
|
|
+<div [style.background-color]="color">
|
|
|
+ {{ rate }}%
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+### 7. 格式化统计摘要为文本
|
|
|
+
|
|
|
+```typescript
|
|
|
+formatSummaryText(summary: ProjectSpaceDeliverableSummary): string
|
|
|
+```
|
|
|
+
|
|
|
+**用途**:将统计摘要格式化为易读的文本
|
|
|
+
|
|
|
+**示例**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+const summary = await this.projectSpaceDeliverableService
|
|
|
+ .getProjectSpaceDeliverableSummary('project123');
|
|
|
+
|
|
|
+const text = this.projectSpaceDeliverableService
|
|
|
+ .formatSummaryText(summary);
|
|
|
+
|
|
|
+console.log(text);
|
|
|
+// 输出:
|
|
|
+// 项目:某某项目
|
|
|
+// 空间总数:3
|
|
|
+// 已完成空间:2/3
|
|
|
+// 总文件数:15
|
|
|
+// - 白模:4
|
|
|
+// - 软装:5
|
|
|
+// - 渲染:4
|
|
|
+// - 后期:2
|
|
|
+// 完成率:67%
|
|
|
+```
|
|
|
+
|
|
|
+## 在组件中使用
|
|
|
+
|
|
|
+### 示例1:在组长端时间轴中使用
|
|
|
+
|
|
|
+```typescript
|
|
|
+// project-timeline.ts
|
|
|
+import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary }
|
|
|
+ from '@modules/project/services/project-space-deliverable.service';
|
|
|
+
|
|
|
+@Component({
|
|
|
+ selector: 'app-project-timeline',
|
|
|
+ // ...
|
|
|
+})
|
|
|
+export class ProjectTimelineComponent implements OnInit {
|
|
|
+ spaceDeliverableCache: Map<string, ProjectSpaceDeliverableSummary> = new Map();
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private projectSpaceDeliverableService: ProjectSpaceDeliverableService
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ async ngOnInit() {
|
|
|
+ await this.loadProjectStats();
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadProjectStats() {
|
|
|
+ for (const project of this.projects) {
|
|
|
+ try {
|
|
|
+ const summary = await this.projectSpaceDeliverableService
|
|
|
+ .getProjectSpaceDeliverableSummary(project.projectId);
|
|
|
+
|
|
|
+ // 缓存统计数据
|
|
|
+ this.spaceDeliverableCache.set(project.projectId, summary);
|
|
|
+
|
|
|
+ console.log(`✅ 项目 ${project.projectName} 统计完成:`, {
|
|
|
+ 空间数: summary.totalSpaces,
|
|
|
+ 已完成空间: summary.spacesWithDeliverables,
|
|
|
+ 完成率: `${summary.overallCompletionRate}%`
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(`⚠️ 加载项目统计失败:`, error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取项目空间数量
|
|
|
+ getProjectSpaceCount(projectId: string): number {
|
|
|
+ const summary = this.spaceDeliverableCache.get(projectId);
|
|
|
+ return summary?.totalSpaces || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取项目交付完成率
|
|
|
+ getProjectDeliveryCompletionRate(projectId: string): number {
|
|
|
+ const summary = this.spaceDeliverableCache.get(projectId);
|
|
|
+ return summary?.overallCompletionRate || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化工具提示
|
|
|
+ formatTooltip(projectId: string): string {
|
|
|
+ const summary = this.spaceDeliverableCache.get(projectId);
|
|
|
+ if (!summary) return '加载中...';
|
|
|
+
|
|
|
+ return `📦 空间与交付物统计\n\n` +
|
|
|
+ `空间总数: ${summary.totalSpaces}\n` +
|
|
|
+ `已完成空间: ${summary.spacesWithDeliverables}/${summary.totalSpaces}\n` +
|
|
|
+ `完成率: ${summary.overallCompletionRate}%`;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 示例2:在HTML模板中显示
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 显示空间统计徽章 -->
|
|
|
+<div class="project-card">
|
|
|
+ <h3>{{ project.name }}</h3>
|
|
|
+
|
|
|
+ @if (getSpaceDeliverableSummary(project.id); as summary) {
|
|
|
+ <span class="badge"
|
|
|
+ [title]="formatTooltip(project.id)"
|
|
|
+ [style.background-color]="getProjectDeliveryStatusColor(project.id)">
|
|
|
+ 📦 {{ summary.spacesWithDeliverables }}/{{ summary.totalSpaces }}
|
|
|
+ </span>
|
|
|
+ }
|
|
|
+</div>
|
|
|
+
|
|
|
+<!-- 显示进度条 -->
|
|
|
+<div class="progress-bar">
|
|
|
+ <div class="progress-fill"
|
|
|
+ [style.width.%]="getProjectDeliveryCompletionRate(project.id)"
|
|
|
+ [style.background-color]="getProjectDeliveryStatusColor(project.id)">
|
|
|
+ {{ getProjectDeliveryCompletionRate(project.id) }}%
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<!-- 显示详细统计 -->
|
|
|
+<div class="stats-panel">
|
|
|
+ @if (getSpaceDeliverableSummary(project.id); as summary) {
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>空间总数</label>
|
|
|
+ <span>{{ summary.totalSpaces }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>已完成空间</label>
|
|
|
+ <span>{{ summary.spacesWithDeliverables }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>总文件数</label>
|
|
|
+ <span>{{ summary.totalDeliverableFiles }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>白模</label>
|
|
|
+ <span>{{ summary.totalByType.whiteModel }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>软装</label>
|
|
|
+ <span>{{ summary.totalByType.softDecor }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>渲染</label>
|
|
|
+ <span>{{ summary.totalByType.rendering }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-item">
|
|
|
+ <label>后期</label>
|
|
|
+ <span>{{ summary.totalByType.postProcess }}</span>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+## 性能优化建议
|
|
|
+
|
|
|
+### 1. 使用缓存
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 缓存统计数据,避免重复查询
|
|
|
+private cache: Map<string, ProjectSpaceDeliverableSummary> = new Map();
|
|
|
+
|
|
|
+async getStats(projectId: string) {
|
|
|
+ // 检查缓存
|
|
|
+ if (this.cache.has(projectId)) {
|
|
|
+ return this.cache.get(projectId)!;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载并缓存
|
|
|
+ const summary = await this.projectSpaceDeliverableService
|
|
|
+ .getProjectSpaceDeliverableSummary(projectId);
|
|
|
+ this.cache.set(projectId, summary);
|
|
|
+
|
|
|
+ return summary;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. 异步加载
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 异步加载,不阻塞主流程
|
|
|
+private async loadStatsInBackground() {
|
|
|
+ for (const project of this.projects) {
|
|
|
+ // 使用 setTimeout 避免阻塞UI
|
|
|
+ setTimeout(async () => {
|
|
|
+ const summary = await this.projectSpaceDeliverableService
|
|
|
+ .getProjectSpaceDeliverableSummary(project.id);
|
|
|
+ this.updateProjectStats(project.id, summary);
|
|
|
+ }, 0);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3. 批量加载
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 并行加载多个项目的统计数据
|
|
|
+private async loadStatsInBatch(projectIds: string[]) {
|
|
|
+ const promises = projectIds.map(id =>
|
|
|
+ this.projectSpaceDeliverableService.getProjectSpaceDeliverableSummary(id)
|
|
|
+ );
|
|
|
+
|
|
|
+ const results = await Promise.allSettled(promises);
|
|
|
+
|
|
|
+ results.forEach((result, index) => {
|
|
|
+ if (result.status === 'fulfilled') {
|
|
|
+ this.cache.set(projectIds[index], result.value);
|
|
|
+ } else {
|
|
|
+ console.warn(`加载项目 ${projectIds[index]} 统计失败:`, result.reason);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 注意事项
|
|
|
+
|
|
|
+1. **空间去重**:服务会自动去除重复的空间(按名称去重,忽略大小写和首尾空格)
|
|
|
+2. **文件类型**:只统计 `delivery_*` 类型的文件(白模、软装、渲染、后期)
|
|
|
+3. **异步操作**:所有方法都是异步的,需要使用 `await` 或 `.then()`
|
|
|
+4. **错误处理**:建议在调用时添加 try-catch 处理错误情况
|
|
|
+5. **缓存策略**:建议在组件中实现缓存,避免频繁查询数据库
|
|
|
+
|
|
|
+## 完整应用示例
|
|
|
+
|
|
|
+请参考以下文件中的实际应用:
|
|
|
+
|
|
|
+- **时间轴组件**:`src/app/pages/team-leader/project-timeline/project-timeline.ts`
|
|
|
+- **交付阶段组件**:`src/modules/project/pages/project-detail/stages/stage-delivery.component.ts`
|
|
|
+
|
|
|
+## 相关文档
|
|
|
+
|
|
|
+- [项目文件服务](./PROJECT-FILE-SERVICE.md)
|
|
|
+- [产品空间服务](./PRODUCT-SPACE-SERVICE.md)
|
|
|
+- [组长端时间轴使用指南](./TEAM-LEADER-TIMELINE-GUIDE.md)
|
|
|
+
|
|
|
+## 技术支持
|
|
|
+
|
|
|
+如有问题,请联系开发团队或查看源码注释。
|
|
|
+
|