|
|
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { PhaseDeadlines, PhaseName, PHASE_INFO, isPhaseDelayed } from '../../../models/project-phase.model';
|
|
|
import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary } from '../../../../modules/project/services/project-space-deliverable.service';
|
|
|
+import { ProjectProgressModalComponent } from '../../../../modules/project/components/project-progress-modal';
|
|
|
|
|
|
export interface ProjectTimeline {
|
|
|
projectId: string;
|
|
|
@@ -50,7 +51,7 @@ interface DesignerInfo {
|
|
|
@Component({
|
|
|
selector: 'app-project-timeline',
|
|
|
standalone: true,
|
|
|
- imports: [CommonModule, FormsModule],
|
|
|
+ imports: [CommonModule, FormsModule, ProjectProgressModalComponent],
|
|
|
templateUrl: './project-timeline.html',
|
|
|
styleUrl: './project-timeline.scss',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
@@ -84,6 +85,10 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
// 🆕 空间与交付物统计缓存
|
|
|
spaceDeliverableCache: Map<string, ProjectSpaceDeliverableSummary> = new Map();
|
|
|
|
|
|
+ // 🆕 进度详情弹窗
|
|
|
+ showProgressModal: boolean = false;
|
|
|
+ selectedProjectSummary: ProjectSpaceDeliverableSummary | null = null;
|
|
|
+
|
|
|
constructor(
|
|
|
private cdr: ChangeDetectorRef,
|
|
|
private projectSpaceDeliverableService: ProjectSpaceDeliverableService
|
|
|
@@ -345,7 +350,36 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- // 小图对图事件(显示所有小图事件,不受今日线限制)
|
|
|
+ // 🆕 阶段截止事件
|
|
|
+ if (project.phaseDeadlines) {
|
|
|
+ // 定义阶段顺序:建模 -> 软装
|
|
|
+ const phaseOrder: PhaseName[] = ['modeling', 'softDecor'];
|
|
|
+
|
|
|
+ phaseOrder.forEach(phaseName => {
|
|
|
+ const phaseInfo = project.phaseDeadlines?.[phaseName];
|
|
|
+ if (phaseInfo && phaseInfo.deadline) {
|
|
|
+ const deadline = new Date(phaseInfo.deadline);
|
|
|
+
|
|
|
+ // 只显示未来的阶段截止事件
|
|
|
+ if (this.isEventInFuture(deadline)) {
|
|
|
+ const phaseConfig = PHASE_INFO[phaseName];
|
|
|
+ const isDelayed = isPhaseDelayed(phaseInfo);
|
|
|
+
|
|
|
+ events.push({
|
|
|
+ date: deadline,
|
|
|
+ label: `${phaseConfig.label}截止`,
|
|
|
+ type: 'phase_deadline',
|
|
|
+ phase: phaseName,
|
|
|
+ projectId: project.projectId,
|
|
|
+ color: isDelayed ? '#dc2626' : phaseConfig.color,
|
|
|
+ icon: phaseConfig.label.charAt(0)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 🔥 小图对图事件(始终显示,位于软装和渲染之间,高亮显示)
|
|
|
if (project.reviewDate && this.isEventInRange(project.reviewDate)) {
|
|
|
const isPast = project.reviewDate.getTime() < this.currentTime.getTime();
|
|
|
events.push({
|
|
|
@@ -353,39 +387,30 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
label: '小图对图',
|
|
|
type: 'review',
|
|
|
projectId: project.projectId,
|
|
|
- color: isPast ? '#94a3b8' : '#8b5cf6', // 过去的事件显示为灰色
|
|
|
- icon: '📋'
|
|
|
+ color: isPast ? '#94a3b8' : '#f59e0b', // 🔥 高亮显示:金黄色
|
|
|
+ icon: '📸' // 🔥 更醒目的图标
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- // 交付事件
|
|
|
- if (project.deliveryDate && this.isEventInFuture(project.deliveryDate)) {
|
|
|
- events.push({
|
|
|
- date: project.deliveryDate,
|
|
|
- label: '交付',
|
|
|
- type: 'delivery',
|
|
|
- projectId: project.projectId,
|
|
|
- color: this.getEventColor('delivery', project),
|
|
|
- icon: '📦'
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 🆕 阶段截止事件
|
|
|
+ // 继续添加剩余阶段:渲染 -> 后期
|
|
|
if (project.phaseDeadlines) {
|
|
|
- Object.entries(project.phaseDeadlines).forEach(([phaseName, phaseInfo]) => {
|
|
|
+ const remainingPhases: PhaseName[] = ['rendering', 'postProcessing'];
|
|
|
+
|
|
|
+ remainingPhases.forEach(phaseName => {
|
|
|
+ const phaseInfo = project.phaseDeadlines?.[phaseName];
|
|
|
if (phaseInfo && phaseInfo.deadline) {
|
|
|
const deadline = new Date(phaseInfo.deadline);
|
|
|
|
|
|
// 只显示未来的阶段截止事件
|
|
|
if (this.isEventInFuture(deadline)) {
|
|
|
- const phaseConfig = PHASE_INFO[phaseName as PhaseName];
|
|
|
+ const phaseConfig = PHASE_INFO[phaseName];
|
|
|
const isDelayed = isPhaseDelayed(phaseInfo);
|
|
|
|
|
|
events.push({
|
|
|
date: deadline,
|
|
|
label: `${phaseConfig.label}截止`,
|
|
|
type: 'phase_deadline',
|
|
|
- phase: phaseName as PhaseName,
|
|
|
+ phase: phaseName,
|
|
|
projectId: project.projectId,
|
|
|
color: isDelayed ? '#dc2626' : phaseConfig.color,
|
|
|
icon: phaseConfig.label.charAt(0)
|
|
|
@@ -395,6 +420,18 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ // 交付事件
|
|
|
+ if (project.deliveryDate && this.isEventInFuture(project.deliveryDate)) {
|
|
|
+ events.push({
|
|
|
+ date: project.deliveryDate,
|
|
|
+ label: '交付',
|
|
|
+ type: 'delivery',
|
|
|
+ projectId: project.projectId,
|
|
|
+ color: this.getEventColor('delivery', project),
|
|
|
+ icon: '📦'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
// 按时间排序
|
|
|
return events.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
|
}
|
|
|
@@ -528,7 +565,38 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
this.applyFilters();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 🆕 点击项目进度条时打开详情弹窗(替代原来的跳转)
|
|
|
+ */
|
|
|
onProjectClick(projectId: string): void {
|
|
|
+ // 获取项目的统计摘要
|
|
|
+ const summary = this.spaceDeliverableCache.get(projectId);
|
|
|
+ if (summary) {
|
|
|
+ this.selectedProjectSummary = summary;
|
|
|
+ this.showProgressModal = true;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 项目统计数据未加载:', projectId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 关闭进度详情弹窗
|
|
|
+ */
|
|
|
+ onCloseProgressModal(): void {
|
|
|
+ this.showProgressModal = false;
|
|
|
+ this.selectedProjectSummary = null;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 从弹窗提交问题
|
|
|
+ */
|
|
|
+ onReportIssueFromModal(projectId: string): void {
|
|
|
+ // 关闭弹窗
|
|
|
+ this.onCloseProgressModal();
|
|
|
+
|
|
|
+ // 发出事件,让父组件处理问题提交
|
|
|
this.projectClick.emit(projectId);
|
|
|
}
|
|
|
|
|
|
@@ -735,7 +803,64 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取今日标签(含时分)
|
|
|
+ * 🆕 获取进度线标签(基于项目完成情况)
|
|
|
+ */
|
|
|
+ getProgressLineLabel(project: ProjectTimeline): string {
|
|
|
+ const summary = this.getSpaceDeliverableSummary(project.projectId);
|
|
|
+ if (!summary) return '加载中...';
|
|
|
+ return `进度:${summary.overallCompletionRate}%`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 获取项目的进度线位置(基于实际完成率)
|
|
|
+ * 进度线表示:在项目时间轴上,根据完成率显示当前进度的位置
|
|
|
+ * @param project 项目信息
|
|
|
+ * @returns CSS left 位置
|
|
|
+ */
|
|
|
+ getProgressLinePosition(project: ProjectTimeline): string {
|
|
|
+ const summary = this.getSpaceDeliverableSummary(project.projectId);
|
|
|
+ if (!summary) return '0%';
|
|
|
+
|
|
|
+ // 根据完成率计算在项目条上的位置
|
|
|
+ const completionRate = summary.overallCompletionRate;
|
|
|
+
|
|
|
+ // 获取项目在时间轴上的起始位置和宽度
|
|
|
+ const rangeStart = this.timeRangeStart.getTime();
|
|
|
+ const rangeEnd = this.timeRangeEnd.getTime();
|
|
|
+ const rangeDuration = rangeEnd - rangeStart;
|
|
|
+
|
|
|
+ const projectStart = Math.max(project.startDate.getTime(), rangeStart);
|
|
|
+ const projectEnd = Math.min(project.endDate.getTime(), rangeEnd);
|
|
|
+ const projectDuration = projectEnd - projectStart;
|
|
|
+
|
|
|
+ // 项目条的起始位置(百分比)
|
|
|
+ const projectLeft = ((projectStart - rangeStart) / rangeDuration) * 100;
|
|
|
+
|
|
|
+ // 项目条的宽度(百分比)
|
|
|
+ const projectWidth = (projectDuration / rangeDuration) * 100;
|
|
|
+
|
|
|
+ // 进度线在项目条内的位置 = 项目起始位置 + 项目宽度 × 完成率
|
|
|
+ const progressPosition = projectLeft + (projectWidth * completionRate / 100);
|
|
|
+
|
|
|
+ // 🔧 考虑左侧项目名称列的宽度(180px)
|
|
|
+ const result = `calc(180px + (100% - 180px) * ${Math.max(0, Math.min(100, progressPosition)) / 100})`;
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 获取进度线的颜色(基于完成率)
|
|
|
+ */
|
|
|
+ getProgressLineColor(completionRate: number): string {
|
|
|
+ if (completionRate >= 80) return '#4CAF50'; // 绿色
|
|
|
+ if (completionRate >= 60) return '#8BC34A'; // 浅绿
|
|
|
+ if (completionRate >= 40) return '#FFC107'; // 黄色
|
|
|
+ if (completionRate >= 20) return '#FF9800'; // 橙色
|
|
|
+ return '#F44336'; // 红色
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取今日标签(含时分)- 保留用于时间参考
|
|
|
*/
|
|
|
getTodayLabel(): string {
|
|
|
const dateStr = this.currentTime.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|