/** * 位置计算缓存工具类 * 优化TaskBar位置计算性能:O(n) → O(1) * * 核心思路: * 1. 预计算所有日期的位置映射表(一次性O(n)遍历) * 2. 后续查询直接O(1)查表,避免重复遍历timelineData * 3. timelineData变化时自动重建缓存 * * @author AI Frontend Engineer * @date 2026-02-01 */ import type { TimelineMonth, TimelineYear, TimelineDay } from '../models/types/TimelineDataTypes' import { TimelineScale } from '../models/types/TimelineScale' type TimelineData = TimelineMonth | TimelineYear | TimelineDay export class PositionCache { // 位置缓存表:key格式为 "scale-year-month-day" private cache: Map = new Map() // timelineData的哈希值,用于判断是否需要重建缓存 private timelineDataHash = '' /** * 构建缓存key */ private getCacheKey(date: Date, timeScale: string): string { const year = date.getFullYear() const month = date.getMonth() const day = date.getDate() return `${timeScale}-${year}-${month}-${day}` } /** * 计算timelineData的hash值 * 简化方案:使用时间刻度 + 第一个日期 + 最后一个日期 + 数据长度 */ private computeHash(timelineData: TimelineData[], timeScale: string): string { if (timelineData.length === 0) return '' const first = timelineData[0] const last = timelineData[timelineData.length - 1] // 处理不同数据结构:TimelineDay 使用 date,其他使用 startDate/endDate let firstDate: Date let lastDate: Date if ('date' in first) { // TimelineDay (小时视图) firstDate = first.date } else if ('startDate' in first) { // TimelineMonth, TimelineYear (其他视图) firstDate = first.startDate } else { // 兜底:返回空hash return '' } if ('date' in last) { // TimelineDay (小时视图) lastDate = last.date } else if ('endDate' in last) { // TimelineMonth, TimelineYear (其他视图) lastDate = last.endDate } else { // 兜底:返回空hash return '' } return `${timeScale}-${firstDate.getTime()}-${lastDate.getTime()}-${timelineData.length}` } /** * 预计算所有日期的位置映射表 * 在Timeline层调用,所有TaskBar共享缓存 * * @param timelineData 时间轴数据数组 * @param timeScale 当前时间刻度 */ buildCache(timelineData: TimelineData[], timeScale: string): void { const newHash = this.computeHash(timelineData, timeScale) // 如果timelineData没变化,复用现有缓存 if (newHash === this.timelineDataHash && this.cache.size > 0) { return } // 清空旧缓存,开始重建 this.cache.clear() this.timelineDataHash = newHash let cumulativePosition = 0 const startTime = performance.now() // ⚠️ 关键优化:一次性遍历timelineData,构建完整的日期→位置映射表 for (const periodData of timelineData) { if (timeScale === TimelineScale.HOUR) { // 小时视图:遍历每天的所有小时 const hours = (periodData as TimelineDay).hours || [] for (let i = 0; i < hours.length; i++) { const hourData = hours[i] const date = new Date(hourData.date) const key = this.getCacheKey(date, timeScale) const position = cumulativePosition + i * 30 // 小时视图每小时30px this.cache.set(key, position) } cumulativePosition += hours.length * 30 } else if (timeScale === TimelineScale.DAY) { // 日视图:遍历每个月的所有天数 const days = (periodData as TimelineMonth).days || [] for (let i = 0; i < days.length; i++) { const dayData = days[i] const date = new Date(dayData.date) const key = this.getCacheKey(date, timeScale) const position = cumulativePosition + i * 30 // 日视图每天30px this.cache.set(key, position) } cumulativePosition += days.length * 30 } else if (timeScale === TimelineScale.WEEK) { // 周视图:遍历每个月的所有周 const weeks = (periodData as TimelineMonth).weeks || [] for (const week of weeks) { const subDays = week.subDays || [] const weekWidth = 60 const dayWidth = weekWidth / 7 for (let i = 0; i < subDays.length; i++) { const subDay = subDays[i] const date = new Date(subDay.date) const key = this.getCacheKey(date, timeScale) const position = cumulativePosition + i * dayWidth this.cache.set(key, position) } cumulativePosition += 60 } } else if (timeScale === TimelineScale.MONTH) { // 月视图:为每个月的每一天建立映射 const startDate = new Date(periodData.startDate) const endDate = new Date(periodData.endDate) const daysInMonth = (periodData as TimelineMonth).monthData?.dayCount || 30 const monthWidth = 60 const dayWidth = monthWidth / daysInMonth for (let day = 1; day <= daysInMonth; day++) { const date = new Date(startDate.getFullYear(), startDate.getMonth(), day) // 确保日期在当前月范围内 if (date <= endDate) { const key = this.getCacheKey(date, timeScale) const position = cumulativePosition + (day - 1) * dayWidth this.cache.set(key, position) } } cumulativePosition += 60 } else if (timeScale === TimelineScale.QUARTER) { // 季度视图:遍历每年的所有季度 const quarters = (periodData as TimelineYear).quarters || [] for (const quarter of quarters) { const quarterStart = new Date(quarter.startDate) const quarterEnd = new Date(quarter.endDate) const quarterWidth = 60 // 计算季度的天数 const daysInQuarter = Math.ceil( (quarterEnd.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24), ) + 1 const dayWidth = quarterWidth / daysInQuarter // 为季度中的每一天建立映射 for (let dayOffset = 0; dayOffset < daysInQuarter; dayOffset++) { const date = new Date(quarterStart) date.setDate(quarterStart.getDate() + dayOffset) if (date <= quarterEnd) { const key = this.getCacheKey(date, timeScale) const position = cumulativePosition + dayOffset * dayWidth this.cache.set(key, position) } } cumulativePosition += 60 } } else if (timeScale === TimelineScale.YEAR) { // 年视图:遍历每年的两个半年 const halfYears = (periodData as TimelineYear).halfYears || [] for (const halfYear of halfYears) { const halfYearStart = new Date(halfYear.startDate) const halfYearEnd = new Date(halfYear.endDate) const halfYearWidth = 180 // 年视图每半年180px // 计算半年的天数 const daysInHalfYear = Math.ceil( (halfYearEnd.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24), ) + 1 const dayWidth = halfYearWidth / daysInHalfYear // 为半年中的每一天建立映射 for (let dayOffset = 0; dayOffset < daysInHalfYear; dayOffset++) { const date = new Date(halfYearStart) date.setDate(halfYearStart.getDate() + dayOffset) if (date <= halfYearEnd) { const key = this.getCacheKey(date, timeScale) const position = cumulativePosition + dayOffset * dayWidth this.cache.set(key, position) } } cumulativePosition += 180 } } } const endTime = performance.now() // Performance log for debugging if (this.cache.size > 0) { const duration = (endTime - startTime).toFixed(2) // eslint-disable-next-line no-console console.log(`[PositionCache] Built cache: ${this.cache.size} entries, took ${duration}ms`) } } /** * O(1)查找日期对应的位置 * * @param date 目标日期 * @param timeScale 当前时间刻度 * @returns 位置(px),如果未找到返回null */ getPosition(date: Date, timeScale: string): number | null { const key = this.getCacheKey(date, timeScale) const position = this.cache.get(key) if (position === undefined) { // 缓存未命中,可能是日期在timelineData范围外 return null } return position } /** * 清空缓存 */ clear(): void { this.cache.clear() this.timelineDataHash = '' } /** * 获取缓存统计信息 */ getStats(): { size: number; hash: string } { return { size: this.cache.size, hash: this.timelineDataHash, } } } // 导出单例实例 export const positionCache = new PositionCache()