|
|
@@ -0,0 +1,544 @@
|
|
|
+import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
|
|
+import { CommonModule } from '@angular/common';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+
|
|
|
+export interface ProjectTimeline {
|
|
|
+ projectId: string;
|
|
|
+ projectName: string;
|
|
|
+ designerId: string;
|
|
|
+ designerName: string;
|
|
|
+ startDate: Date;
|
|
|
+ endDate: Date;
|
|
|
+ deliveryDate: Date;
|
|
|
+ reviewDate: Date;
|
|
|
+ currentStage: 'plan' | 'model' | 'decoration' | 'render' | 'delivery';
|
|
|
+ stageName: string;
|
|
|
+ stageProgress: number;
|
|
|
+ status: 'normal' | 'warning' | 'urgent' | 'overdue';
|
|
|
+ isStalled: boolean;
|
|
|
+ stalledDays: number;
|
|
|
+ urgentCount: number;
|
|
|
+ priority: 'low' | 'medium' | 'high' | 'critical';
|
|
|
+ spaceName?: string;
|
|
|
+ customerName?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface DesignerInfo {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ projectCount: number;
|
|
|
+ urgentCount: number;
|
|
|
+ overdueCount: number;
|
|
|
+ workloadLevel: 'low' | 'medium' | 'high';
|
|
|
+}
|
|
|
+
|
|
|
+@Component({
|
|
|
+ selector: 'app-project-timeline',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, FormsModule],
|
|
|
+ templateUrl: './project-timeline.html',
|
|
|
+ styleUrl: './project-timeline.scss',
|
|
|
+ changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
+})
|
|
|
+export class ProjectTimelineComponent implements OnInit, OnDestroy {
|
|
|
+ @Input() projects: ProjectTimeline[] = [];
|
|
|
+ @Input() companyId: string = '';
|
|
|
+ @Output() projectClick = new EventEmitter<string>();
|
|
|
+
|
|
|
+ // 筛选状态
|
|
|
+ selectedDesigner: string = 'all';
|
|
|
+ selectedStatus: 'all' | 'normal' | 'warning' | 'urgent' | 'overdue' | 'stalled' = 'all';
|
|
|
+ selectedPriority: 'all' | 'low' | 'medium' | 'high' | 'critical' = 'all';
|
|
|
+ viewMode: 'timeline' | 'list' = 'list';
|
|
|
+ sortBy: 'priority' | 'time' = 'priority';
|
|
|
+
|
|
|
+ // 设计师统计
|
|
|
+ designers: DesignerInfo[] = [];
|
|
|
+ filteredProjects: ProjectTimeline[] = [];
|
|
|
+
|
|
|
+ // 时间轴相关
|
|
|
+ timeRange: Date[] = [];
|
|
|
+ timeRangeStart: Date = new Date();
|
|
|
+ timeRangeEnd: Date = new Date();
|
|
|
+ timelineScale: 'week' | 'month' = 'week'; // 默认周视图(7天)
|
|
|
+
|
|
|
+ // 🆕 实时时间相关
|
|
|
+ currentTime: Date = new Date(); // 精确到分钟的当前时间
|
|
|
+ private refreshTimer: any; // 自动刷新定时器
|
|
|
+
|
|
|
+ constructor(private cdr: ChangeDetectorRef) {}
|
|
|
+
|
|
|
+ ngOnInit(): void {
|
|
|
+ this.initializeData();
|
|
|
+ this.startAutoRefresh(); // 🆕 启动自动刷新
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnDestroy(): void {
|
|
|
+ // 🆕 清理定时器
|
|
|
+ if (this.refreshTimer) {
|
|
|
+ clearInterval(this.refreshTimer);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnChanges(): void {
|
|
|
+ this.initializeData();
|
|
|
+ }
|
|
|
+
|
|
|
+ private initializeData(): void {
|
|
|
+ this.loadDesignersData();
|
|
|
+ this.calculateTimeRange();
|
|
|
+ this.applyFilters();
|
|
|
+ }
|
|
|
+
|
|
|
+ private loadDesignersData(): void {
|
|
|
+ this.designers = this.buildDesignerStats();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算时间范围(周视图=7天,月视图=30天)
|
|
|
+ */
|
|
|
+ private calculateTimeRange(): void {
|
|
|
+ const now = new Date();
|
|
|
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
+
|
|
|
+ // 根据时间尺度计算范围
|
|
|
+ const days = this.timelineScale === 'week' ? 7 : 30;
|
|
|
+
|
|
|
+ this.timeRangeStart = today;
|
|
|
+ this.timeRangeEnd = new Date(today.getTime() + days * 24 * 60 * 60 * 1000);
|
|
|
+
|
|
|
+ // 生成时间刻度数组
|
|
|
+ this.timeRange = [];
|
|
|
+ const interval = this.timelineScale === 'week' ? 1 : 5; // 周视图每天一个刻度,月视图每5天
|
|
|
+
|
|
|
+ for (let i = 0; i <= days; i += interval) {
|
|
|
+ const date = new Date(today.getTime() + i * 24 * 60 * 60 * 1000);
|
|
|
+ this.timeRange.push(date);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换时间尺度(周/月)
|
|
|
+ */
|
|
|
+ toggleTimelineScale(scale: 'week' | 'month'): void {
|
|
|
+ if (this.timelineScale !== scale) {
|
|
|
+ this.timelineScale = scale;
|
|
|
+ this.calculateTimeRange();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目在时间轴上的位置和宽度
|
|
|
+ */
|
|
|
+ getProjectPosition(project: ProjectTimeline): { left: string; width: string; background: string } {
|
|
|
+ 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 left = ((projectStart - rangeStart) / rangeDuration) * 100;
|
|
|
+ const width = ((projectEnd - projectStart) / rangeDuration) * 100;
|
|
|
+
|
|
|
+ // 🆕 根据时间紧急度获取颜色(而不是阶段)
|
|
|
+ const background = this.getProjectUrgencyColor(project);
|
|
|
+
|
|
|
+ return {
|
|
|
+ left: `${Math.max(0, left)}%`,
|
|
|
+ width: `${Math.max(1, Math.min(100 - left, width))}%`,
|
|
|
+ background
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取事件标记在时间轴上的位置
|
|
|
+ */
|
|
|
+ getEventPosition(date: Date): string {
|
|
|
+ const rangeStart = this.timeRangeStart.getTime();
|
|
|
+ const rangeEnd = this.timeRangeEnd.getTime();
|
|
|
+ const rangeDuration = rangeEnd - rangeStart;
|
|
|
+
|
|
|
+ const eventTime = date.getTime();
|
|
|
+
|
|
|
+ // 如果事件在范围外,返回null
|
|
|
+ if (eventTime < rangeStart || eventTime > rangeEnd) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const position = ((eventTime - rangeStart) / rangeDuration) * 100;
|
|
|
+ return `${Math.max(0, Math.min(100, position))}%`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 根据时间紧急度获取项目条颜色
|
|
|
+ * 规则:
|
|
|
+ * - 正常进行(距离最近事件2天+):绿色
|
|
|
+ * - 临近事件前一天:黄色
|
|
|
+ * - 事件当天(未到时间):橙色
|
|
|
+ * - 事件当天(已过时间):红色
|
|
|
+ */
|
|
|
+ getProjectUrgencyColor(project: ProjectTimeline): string {
|
|
|
+ const now = this.currentTime.getTime();
|
|
|
+
|
|
|
+ // 找到最近的未来事件(对图或交付)
|
|
|
+ const upcomingEvents: { date: Date; type: string }[] = [];
|
|
|
+
|
|
|
+ if (project.reviewDate && project.reviewDate.getTime() >= now) {
|
|
|
+ upcomingEvents.push({ date: project.reviewDate, type: 'review' });
|
|
|
+ }
|
|
|
+ if (project.deliveryDate && project.deliveryDate.getTime() >= now) {
|
|
|
+ upcomingEvents.push({ date: project.deliveryDate, type: 'delivery' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有未来事件,使用默认绿色(项目正常进行)
|
|
|
+ if (upcomingEvents.length === 0) {
|
|
|
+ return 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)'; // 绿色
|
|
|
+ }
|
|
|
+
|
|
|
+ // 找到最近的事件
|
|
|
+ upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
|
+ const nearestEvent = upcomingEvents[0];
|
|
|
+ const eventTime = nearestEvent.date.getTime();
|
|
|
+
|
|
|
+ // 计算时间差(毫秒)
|
|
|
+ const timeDiff = eventTime - now;
|
|
|
+ const hoursDiff = timeDiff / (1000 * 60 * 60);
|
|
|
+ const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
|
|
|
+
|
|
|
+ // 判断是否是同一天
|
|
|
+ const nowDate = new Date(now);
|
|
|
+ const eventDate = nearestEvent.date;
|
|
|
+ const isSameDay = nowDate.getFullYear() === eventDate.getFullYear() &&
|
|
|
+ nowDate.getMonth() === eventDate.getMonth() &&
|
|
|
+ nowDate.getDate() === eventDate.getDate();
|
|
|
+
|
|
|
+ let color = '';
|
|
|
+ let colorName = '';
|
|
|
+
|
|
|
+ // 🔴 红色:事件当天且已过事件时间(或距离事件时间不到2小时)
|
|
|
+ if (isSameDay && hoursDiff < 2) {
|
|
|
+ color = 'linear-gradient(135deg, #FCA5A5 0%, #EF4444 100%)';
|
|
|
+ colorName = '🔴 红色(紧急)';
|
|
|
+ }
|
|
|
+ // 🟠 橙色:事件当天但还有2小时以上
|
|
|
+ else if (isSameDay) {
|
|
|
+ color = 'linear-gradient(135deg, #FCD34D 0%, #F59E0B 100%)';
|
|
|
+ colorName = '🟠 橙色(当天)';
|
|
|
+ }
|
|
|
+ // 🟡 黄色:距离事件前一天(24小时内但不是当天)
|
|
|
+ else if (hoursDiff < 24) {
|
|
|
+ color = 'linear-gradient(135deg, #FEF08A 0%, #EAB308 100%)';
|
|
|
+ colorName = '🟡 黄色(前一天)';
|
|
|
+ }
|
|
|
+ // 🟢 绿色:正常进行(距离事件2天+)
|
|
|
+ else {
|
|
|
+ color = 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)';
|
|
|
+ colorName = '🟢 绿色(正常)';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调试日志(只在首次加载时输出,避免刷屏)
|
|
|
+ if (Math.random() < 0.1) { // 10%概率输出
|
|
|
+ console.log(`🎨 项目颜色:${project.projectName}`, {
|
|
|
+ 最近事件: `${nearestEvent.type === 'review' ? '对图' : '交付'} - ${nearestEvent.date.toLocaleString('zh-CN')}`,
|
|
|
+ 剩余时间: `${hoursDiff.toFixed(1)}小时 (${daysDiff.toFixed(1)}天)`,
|
|
|
+ 是否当天: isSameDay,
|
|
|
+ 颜色判断: colorName
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return color;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取阶段渐变色(已弃用,保留用于其他地方可能的引用)
|
|
|
+ */
|
|
|
+ getStageGradient(stage: string): string {
|
|
|
+ const gradients: Record<string, string> = {
|
|
|
+ 'plan': 'linear-gradient(135deg, #DDD6FE 0%, #C4B5FD 100%)',
|
|
|
+ 'model': 'linear-gradient(135deg, #BFDBFE 0%, #93C5FD 100%)',
|
|
|
+ 'decoration': 'linear-gradient(135deg, #FBCFE8 0%, #F9A8D4 100%)',
|
|
|
+ 'render': 'linear-gradient(135deg, #FED7AA 0%, #FDBA74 100%)',
|
|
|
+ 'delivery': 'linear-gradient(135deg, #BBF7D0 0%, #86EFAC 100%)'
|
|
|
+ };
|
|
|
+ return gradients[stage] || gradients['model'];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取事件标记颜色
|
|
|
+ */
|
|
|
+ getEventColor(eventType: 'start' | 'review' | 'delivery', project: ProjectTimeline): string {
|
|
|
+ if (eventType === 'start') return '#10b981'; // 绿色
|
|
|
+ if (eventType === 'review') return '#3b82f6'; // 蓝色
|
|
|
+
|
|
|
+ // 交付日期根据状态变色
|
|
|
+ if (eventType === 'delivery') {
|
|
|
+ if (project.status === 'overdue') return '#dc2626'; // 红色
|
|
|
+ if (project.status === 'urgent') return '#ea580c'; // 橙色
|
|
|
+ if (project.status === 'warning') return '#f59e0b'; // 黄色
|
|
|
+ return '#10b981'; // 绿色
|
|
|
+ }
|
|
|
+
|
|
|
+ return '#6b7280';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查事件是否在时间范围内
|
|
|
+ */
|
|
|
+ isEventInRange(date: Date): boolean {
|
|
|
+ const time = date.getTime();
|
|
|
+ return time >= this.timeRangeStart.getTime() && time <= this.timeRangeEnd.getTime();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 判断事件是否在未来(今日线之后)
|
|
|
+ * 只显示未来的事件,隐藏已过去的事件
|
|
|
+ */
|
|
|
+ isEventInFuture(date: Date): boolean {
|
|
|
+ // 必须同时满足:在时间范围内 + 在当前时间之后
|
|
|
+ return this.isEventInRange(date) && date.getTime() >= this.currentTime.getTime();
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildDesignerStats(): DesignerInfo[] {
|
|
|
+ const designerMap = new Map<string, DesignerInfo>();
|
|
|
+
|
|
|
+ this.projects.forEach(project => {
|
|
|
+ const designerName = project.designerName || '未分配';
|
|
|
+
|
|
|
+ if (!designerMap.has(designerName)) {
|
|
|
+ designerMap.set(designerName, {
|
|
|
+ id: project.designerId || designerName,
|
|
|
+ name: designerName,
|
|
|
+ projectCount: 0,
|
|
|
+ urgentCount: 0,
|
|
|
+ overdueCount: 0,
|
|
|
+ workloadLevel: 'low'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const designer = designerMap.get(designerName)!;
|
|
|
+ designer.projectCount++;
|
|
|
+
|
|
|
+ if (project.status === 'urgent' || project.status === 'overdue') {
|
|
|
+ designer.urgentCount++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (project.status === 'overdue') {
|
|
|
+ designer.overdueCount++;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算负载等级
|
|
|
+ designerMap.forEach(designer => {
|
|
|
+ if (designer.projectCount >= 5 || designer.overdueCount >= 2) {
|
|
|
+ designer.workloadLevel = 'high';
|
|
|
+ } else if (designer.projectCount >= 3 || designer.urgentCount >= 1) {
|
|
|
+ designer.workloadLevel = 'medium';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return Array.from(designerMap.values()).sort((a, b) => b.projectCount - a.projectCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ applyFilters(): void {
|
|
|
+ let result = [...this.projects];
|
|
|
+
|
|
|
+ // 设计师筛选
|
|
|
+ if (this.selectedDesigner !== 'all') {
|
|
|
+ result = result.filter(p => p.designerName === this.selectedDesigner);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态筛选
|
|
|
+ if (this.selectedStatus !== 'all') {
|
|
|
+ if (this.selectedStatus === 'stalled') {
|
|
|
+ result = result.filter(p => p.isStalled);
|
|
|
+ } else {
|
|
|
+ result = result.filter(p => p.status === this.selectedStatus);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 优先级筛选
|
|
|
+ if (this.selectedPriority !== 'all') {
|
|
|
+ result = result.filter(p => p.priority === this.selectedPriority);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序
|
|
|
+ if (this.sortBy === 'priority') {
|
|
|
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
|
+ result.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
|
+ } else {
|
|
|
+ result.sort((a, b) => a.endDate.getTime() - b.endDate.getTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ this.filteredProjects = result;
|
|
|
+ }
|
|
|
+
|
|
|
+ selectDesigner(designerId: string): void {
|
|
|
+ this.selectedDesigner = designerId;
|
|
|
+ this.applyFilters();
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleViewMode(mode: 'timeline' | 'list'): void {
|
|
|
+ this.viewMode = mode;
|
|
|
+ this.calculateTimeRange(); // 重新计算时间范围
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleSortBy(sortBy: 'priority' | 'time'): void {
|
|
|
+ this.sortBy = sortBy;
|
|
|
+ this.applyFilters();
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleFilter(type: 'status' | 'priority', value: string): void {
|
|
|
+ if (type === 'status') {
|
|
|
+ this.selectedStatus = this.selectedStatus === value ? 'all' : value as any;
|
|
|
+ } else {
|
|
|
+ this.selectedPriority = this.selectedPriority === value ? 'all' : value as any;
|
|
|
+ }
|
|
|
+ this.applyFilters();
|
|
|
+ }
|
|
|
+
|
|
|
+ onProjectClick(projectId: string): void {
|
|
|
+ this.projectClick.emit(projectId);
|
|
|
+ }
|
|
|
+
|
|
|
+ getWorkloadIcon(level: 'low' | 'medium' | 'high'): string {
|
|
|
+ const icons = {
|
|
|
+ low: '🟢',
|
|
|
+ medium: '🟡',
|
|
|
+ high: '🔴'
|
|
|
+ };
|
|
|
+ return icons[level];
|
|
|
+ }
|
|
|
+
|
|
|
+ formatDate(date: Date): string {
|
|
|
+ if (!date) return '-';
|
|
|
+ const now = new Date();
|
|
|
+ const diff = date.getTime() - now.getTime();
|
|
|
+ const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
|
+
|
|
|
+ if (days < 0) {
|
|
|
+ return `逾期${Math.abs(days)}天`;
|
|
|
+ } else if (days === 0) {
|
|
|
+ return '今天';
|
|
|
+ } else if (days === 1) {
|
|
|
+ return '明天';
|
|
|
+ } else if (days <= 7) {
|
|
|
+ return `${days}天后`;
|
|
|
+ } else {
|
|
|
+ return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ formatTime(date: Date): string {
|
|
|
+ if (!date) return '-';
|
|
|
+ return date.toLocaleString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ getSelectedDesignerName(): string {
|
|
|
+ const designer = this.getSelectedDesigner();
|
|
|
+ return designer ? designer.name : '全部设计师';
|
|
|
+ }
|
|
|
+
|
|
|
+ getSelectedDesigner(): DesignerInfo | null {
|
|
|
+ if (this.selectedDesigner === 'all') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return this.designers.find(d => d.id === this.selectedDesigner || d.name === this.selectedDesigner) || null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 启动自动刷新(每10分钟)
|
|
|
+ */
|
|
|
+ private startAutoRefresh(): void {
|
|
|
+ // 立即更新一次当前时间
|
|
|
+ this.updateCurrentTime();
|
|
|
+
|
|
|
+ // 每10分钟刷新一次(600000毫秒)
|
|
|
+ this.refreshTimer = setInterval(() => {
|
|
|
+ console.log('🔄 项目时间轴:10分钟自动刷新触发');
|
|
|
+ this.updateCurrentTime();
|
|
|
+ this.initializeData(); // 重新加载数据和过滤
|
|
|
+ this.cdr.markForCheck(); // 触发变更检测
|
|
|
+ }, 600000); // 10分钟 = 10 * 60 * 1000 = 600000ms
|
|
|
+
|
|
|
+ console.log('⏰ 项目时间轴:已启动10分钟自动刷新');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 更新当前精确时间
|
|
|
+ */
|
|
|
+ private updateCurrentTime(): void {
|
|
|
+ this.currentTime = new Date();
|
|
|
+ console.log('⏰ 当前精确时间已更新:', this.currentTime.toLocaleString('zh-CN'));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 手动刷新(供外部调用)
|
|
|
+ */
|
|
|
+ refresh(): void {
|
|
|
+ console.log('🔄 手动刷新项目时间轴');
|
|
|
+ this.updateCurrentTime();
|
|
|
+ this.initializeData();
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前日期(精确时间)
|
|
|
+ */
|
|
|
+ getCurrentDate(): Date {
|
|
|
+ return this.currentTime; // 🆕 返回存储的精确时间
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取今日标签(含时分)
|
|
|
+ */
|
|
|
+ getTodayLabel(): string {
|
|
|
+ const dateStr = this.currentTime.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
|
|
+ const timeStr = this.currentTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
|
+ return `今日:${dateStr} ${timeStr}`; // 🆕 添加时分显示
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 🆕 获取今日线的精确位置(含时分)
|
|
|
+ * 注意:需要考虑左侧项目名称列的宽度(180px)
|
|
|
+ */
|
|
|
+ getTodayPosition(): string {
|
|
|
+ const rangeStart = this.timeRangeStart.getTime();
|
|
|
+ const rangeEnd = this.timeRangeEnd.getTime();
|
|
|
+ const rangeDuration = rangeEnd - rangeStart;
|
|
|
+ const currentTimeMs = this.currentTime.getTime();
|
|
|
+
|
|
|
+ // 如果当前时间不在范围内,返回空(不显示今日线)
|
|
|
+ if (currentTimeMs < rangeStart || currentTimeMs > rangeEnd) {
|
|
|
+ console.log('⚠️ 今日线:当前时间不在时间范围内');
|
|
|
+ return 'calc(-1000%)'; // 移出可视区域
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算在时间范围内的相对位置(0-100%)
|
|
|
+ const relativePosition = ((currentTimeMs - rangeStart) / rangeDuration) * 100;
|
|
|
+ const clampedPosition = Math.max(0, Math.min(100, relativePosition));
|
|
|
+
|
|
|
+ // 🔧 关键修复:考虑左侧项目名称列的宽度(180px)
|
|
|
+ // 今日线的位置 = 180px + (剩余宽度 × 相对位置)
|
|
|
+ const result = `calc(180px + (100% - 180px) * ${clampedPosition / 100})`;
|
|
|
+
|
|
|
+ // 调试日志
|
|
|
+ console.log('📍 今日线位置计算:', {
|
|
|
+ 当前时间: this.currentTime.toLocaleString('zh-CN'),
|
|
|
+ 范围开始: new Date(rangeStart).toLocaleString('zh-CN'),
|
|
|
+ 范围结束: new Date(rangeEnd).toLocaleString('zh-CN'),
|
|
|
+ 相对位置百分比: `${clampedPosition.toFixed(2)}%`,
|
|
|
+ 最终CSS值: result,
|
|
|
+ 说明: `在${this.timelineScale === 'week' ? '7天' : '30天'}视图中,当前时间占${clampedPosition.toFixed(2)}%`
|
|
|
+ });
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|