This commit is contained in:
Tsuki 2025-08-08 07:20:05 +08:00
parent 520efbbb0d
commit 9646e5d0e1
10 changed files with 202 additions and 100 deletions

View File

@ -1,15 +1,14 @@
'use client' 'use client'
import React, { createContext, useContext, useRef, useState, ReactNode } from 'react' import React, { createContext, useContext, useRef, useState, ReactNode } from 'react'
// import Map from 'ol/Map';
import { Map } from 'maplibre-gl'; import { Map } from 'maplibre-gl';
import { fromLonLat } from 'ol/proj';
// 定义MapContext的类型 // 定义MapContext的类型
interface MapContextType { interface MapContextType {
mapRef: React.RefObject<Map | null> mapRef: React.RefObject<Map | null>
layers: React.RefObject<any[]> layers: React.RefObject<any[]>
mapState: MapState mapState: MapState
currentDatetime: Date | null
setMap: (map: Map, layers: any[]) => void setMap: (map: Map, layers: any[]) => void
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
zoomIn: () => void zoomIn: () => void
@ -21,6 +20,8 @@ interface MapContextType {
isMapReady: boolean isMapReady: boolean
} }
// 创建Context // 创建Context
const MapContext = createContext<MapContextType | undefined>(undefined) const MapContext = createContext<MapContextType | undefined>(undefined)
@ -42,7 +43,7 @@ export function MapProvider({ children }: MapProviderProps) {
}); });
const layersRef = useRef<any[]>([]) const layersRef = useRef<any[]>([])
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
const setMap = (map: Map, layers: any[]) => { const setMap = (map: Map, layers: any[]) => {
// 如果已经有地图实例,先清理旧的 // 如果已经有地图实例,先清理旧的
@ -51,9 +52,6 @@ export function MapProvider({ children }: MapProviderProps) {
mapRef.current = null; mapRef.current = null;
} }
// 监听视图变化事件
// const view = map.getView();
// 监听视图的缩放变化 // 监听视图的缩放变化
map.on('zoom', () => { map.on('zoom', () => {
setMapState(prevState => ({ setMapState(prevState => ({
@ -78,12 +76,6 @@ export function MapProvider({ children }: MapProviderProps) {
const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => { const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => {
if (mapRef.current) { if (mapRef.current) {
// mapRef.current.getView().animate({
// center: fromLonLat(options.center),
// zoom: options.zoom,
// duration: options.duration || 1000
// })
mapRef.current.flyTo({ mapRef.current.flyTo({
center: options.center, center: options.center,
zoom: options.zoom, zoom: options.zoom,
@ -95,42 +87,28 @@ export function MapProvider({ children }: MapProviderProps) {
const zoomIn = () => { const zoomIn = () => {
if (mapRef.current) { if (mapRef.current) {
// mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! + 1)
mapRef.current.zoomIn() mapRef.current.zoomIn()
} }
} }
const zoomOut = () => { const zoomOut = () => {
if (mapRef.current) { if (mapRef.current) {
// mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! - 1)
mapRef.current.zoomOut() mapRef.current.zoomOut()
} }
} }
const zoomTo = (zoom: number) => { const zoomTo = (zoom: number) => {
if (mapRef.current) { if (mapRef.current) {
// mapRef.current.getView().setZoom(zoom)
mapRef.current.zoomTo(zoom) mapRef.current.zoomTo(zoom)
} }
} }
const setTime = (date: Date) => { const setTime = (date: Date) => {
if (mapRef.current) { setCurrentDatetime(date)
layersRef.current.forEach(layer => {
const source = layer.getSource()
if (source) {
source.updateParams({
'TIME': date.toISOString()
})
}
})
}
} }
const reset = () => { const reset = () => {
if (mapRef.current) { if (mapRef.current) {
// mapRef.current.getView().setCenter([103.851959, 1.290270])
// mapRef.current.getView().setZoom(11)
mapRef.current.flyTo({ mapRef.current.flyTo({
center: [103.851959, 1.290270], center: [103.851959, 1.290270],
zoom: 11, zoom: 11,
@ -154,6 +132,7 @@ export function MapProvider({ children }: MapProviderProps) {
const value: MapContextType = { const value: MapContextType = {
setTime, setTime,
currentDatetime,
mapRef, mapRef,
layers: layersRef, layers: layersRef,
mapState, mapState,

View File

@ -133,7 +133,6 @@ function SelectDemo() {
export const Timeline: React.FC<Props> = ({ export const Timeline: React.FC<Props> = ({
startDate, startDate,
endDate, endDate,
currentDate,
onDateChange, onDateChange,
onPlay, onPlay,
onPause, onPause,
@ -148,19 +147,12 @@ export const Timeline: React.FC<Props> = ({
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const ticksCanvasRef = useRef<HTMLCanvasElement>(null); const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
const { isPlaying, togglePlay, loading, setTime, currentDatetime: currentDate } = useTimeline({})
const { isPlaying, togglePlay } = useTimeline({
initialDate: currentDate ?? new Date(),
onDateChange(date) {
onDateChange?.(date);
}
})
const [state, setState] = useState<Status>({ const [state, setState] = useState<Status>({
isDragging: false, isDragging: false,
isLongPress: false, isLongPress: false,
isPanningTimeline: false, isPanningTimeline: false,
customLineTimestamp: currentDate?.getTime() ?? new Date().getTime(), customLineTimestamp: currentDate?.getTime() ?? null,
panOffset: 0, panOffset: 0,
zoomLevel: initialZoom, zoomLevel: initialZoom,
}); });
@ -188,6 +180,7 @@ export const Timeline: React.FC<Props> = ({
const newDate = new Date(state.customLineTimestamp! + 360000); const newDate = new Date(state.customLineTimestamp! + 360000);
setState({ ...state, customLineTimestamp: newDate.getTime() }); setState({ ...state, customLineTimestamp: newDate.getTime() });
onDateChange?.(newDate); onDateChange?.(newDate);
setTime(newDate);
}, [state.customLineTimestamp]) }, [state.customLineTimestamp])
// 缩放处理函数 // 缩放处理函数
@ -282,7 +275,7 @@ export const Timeline: React.FC<Props> = ({
dpr, dpr,
overrides.startDate ?? currentProps.startDate, overrides.startDate ?? currentProps.startDate,
overrides.endDate ?? currentProps.endDate, overrides.endDate ?? currentProps.endDate,
overrides.currentDate ?? currentProps.currentDate, overrides.currentDate ?? currentProps.currentDate ?? undefined,
overrides.zoomLevel ?? currentState.zoomLevel, overrides.zoomLevel ?? currentState.zoomLevel,
overrides.panOffset ?? currentState.panOffset, overrides.panOffset ?? currentState.panOffset,
overrides.customLineTimestamp ?? currentState.customLineTimestamp, overrides.customLineTimestamp ?? currentState.customLineTimestamp,
@ -386,17 +379,24 @@ export const Timeline: React.FC<Props> = ({
const selectedTimestamp = visibleStartTime + progress * visibleTimeRange; const selectedTimestamp = visibleStartTime + progress * visibleTimeRange;
// 规整到最近的6分钟整数时间
const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒
const roundedTimestamp = Math.round(selectedTimestamp / sixMinutesInMs) * sixMinutesInMs;
setState(prevState => ({ setState(prevState => ({
...prevState, ...prevState,
customLineTimestamp: selectedTimestamp, customLineTimestamp: roundedTimestamp,
isLongPress: false, isLongPress: false,
isDragging: false isDragging: false
})); }));
// 通知父组件 // 通知父组件
if (onDateChange) { if (onDateChange) {
onDateChange(new Date(selectedTimestamp)); onDateChange(new Date(roundedTimestamp));
} }
// 使用setTime更新时间轴状态
setTime(new Date(roundedTimestamp));
} }
} }
} }
@ -601,12 +601,19 @@ export const Timeline: React.FC<Props> = ({
const finalTimestamp = visibleStartTime + progress * visibleTimeRange; const finalTimestamp = visibleStartTime + progress * visibleTimeRange;
// 规整到最近的6分钟整数时间
const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒
const roundedFinalTimestamp = Math.round(finalTimestamp / sixMinutesInMs) * sixMinutesInMs;
setState(prevState => ({ setState(prevState => ({
...prevState, ...prevState,
customLineTimestamp: finalTimestamp, customLineTimestamp: roundedFinalTimestamp,
isDragging: false, isDragging: false,
isLongPress: false isLongPress: false
})); }));
// 使用setTime更新时间轴状态
setTime(new Date(roundedFinalTimestamp));
} else { } else {
setState(prevState => ({ ...prevState, isDragging: false, isLongPress: false })); setState(prevState => ({ ...prevState, isDragging: false, isLongPress: false }));
} }
@ -706,9 +713,6 @@ export const Timeline: React.FC<Props> = ({
current_uniforms.current.currentTimestamp = currentDate.getTime(); current_uniforms.current.currentTimestamp = currentDate.getTime();
} }
// 鼠标滚轮缩放和平移 // 鼠标滚轮缩放和平移
const handleWheel = (e: WheelEvent) => { const handleWheel = (e: WheelEvent) => {
e.preventDefault(); e.preventDefault();

View File

@ -3,12 +3,26 @@ import vsSource from './glsl/timeline/vert.glsl';
import fsSource from './glsl/timeline/frag.glsl'; import fsSource from './glsl/timeline/frag.glsl';
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, HomeIcon, Pause, Play } from "lucide-react"; import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, UnlockIcon } from "lucide-react";
import { formatInTimeZone } from "date-fns-tz";
import { parse } from "date-fns"
import { useTimeline } from "@/hooks/use-timeline"; import { useTimeline } from "@/hooks/use-timeline";
import { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline"; import { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { gql, useSubscription } from "@apollo/client";
const SUBSCRIPTION_QUERY = gql`
subscription {
statusUpdates {
id
message
status
timestamp
newestDt
}
}
`
interface Uniforms { interface Uniforms {
startTimestamp: number; // Unix 时间戳开始 startTimestamp: number; // Unix 时间戳开始
@ -65,7 +79,8 @@ export const Timeline: React.FC<Props> = React.memo(({
timelineConfig, timelineConfig,
...props ...props
}) => { }) => {
const { isPlaying, togglePlay } = useTimeline({}) const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({})
const [lock, setLock] = useState(false)
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const ticksCanvasRef = useRef<HTMLCanvasElement>(null); const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
const timelineEngineRef = useRef<TimelineEngine | null>(null); const timelineEngineRef = useRef<TimelineEngine | null>(null);
@ -77,6 +92,21 @@ export const Timeline: React.FC<Props> = React.memo(({
currentLevel: null as any currentLevel: null as any
}); });
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
useEffect(() => {
if (data) {
if (data.statusUpdates) {
if (!lock && data.statusUpdates.newestDt) {
const newDt = parse(data.statusUpdates.newestDt + 'Z', 'yyyyMMddHHmmssX', new Date())
setTime(newDt)
}
} else {
}
}
}, [data, lock])
// 定时器效果 - 当播放时每隔指定时间执行操作 // 定时器效果 - 当播放时每隔指定时间执行操作
useEffect(() => { useEffect(() => {
let intervalId: NodeJS.Timeout | null = null; let intervalId: NodeJS.Timeout | null = null;
@ -85,7 +115,6 @@ export const Timeline: React.FC<Props> = React.memo(({
intervalId = setInterval(() => { intervalId = setInterval(() => {
// 执行时间前进操作 // 执行时间前进操作
if (timelineEngineRef.current) { if (timelineEngineRef.current) {
// timelineEngineRef.current.forwardTimeMark(timeStep);
timelineEngineRef.current.playAndEnsureMarkInView(timeStep) timelineEngineRef.current.playAndEnsureMarkInView(timeStep)
} }
}, 1000); // 每秒执行一次,你可以根据需要调整这个间隔 }, 1000); // 每秒执行一次,你可以根据需要调整这个间隔
@ -98,6 +127,12 @@ export const Timeline: React.FC<Props> = React.memo(({
}; };
}, [isPlaying, timeStep]); }, [isPlaying, timeStep]);
useEffect(() => {
if (currentDatetime && !lock) {
timelineEngineRef.current?.replaceTimeMarkByTimestamp(currentDatetime.getTime())
}
}, [currentDatetime, lock])
useEffect(() => { useEffect(() => {
if (!ticksCanvasRef.current) return; if (!ticksCanvasRef.current) return;
@ -132,6 +167,20 @@ export const Timeline: React.FC<Props> = React.memo(({
primaryFontSize: 10, primaryFontSize: 10,
secondaryFontSize: 10 secondaryFontSize: 10
}, },
onDateChange: async (date: Date) => {
const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss')
const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`)
if (response.ok) {
const data = await response.json()
const nearestDatetime = data.nearest_data_time
const nearestDate = new Date(Date.parse(nearestDatetime))
setTime(nearestDate)
} else {
console.error('Failed to fetch data:', response.status)
}
},
...timelineConfig ...timelineConfig
}; };
@ -210,7 +259,10 @@ export const Timeline: React.FC<Props> = React.memo(({
variant="secondary" variant="secondary"
size="icon" size="icon"
className="size-5" className="size-5"
onClick={() => togglePlay()} onClick={() => {
togglePlay()
setLock(true)
}}
title={isPlaying ? "暂停" : "播放"} title={isPlaying ? "暂停" : "播放"}
> >
{isPlaying ? <Pause size={10} /> : <Play size={10} />} {isPlaying ? <Pause size={10} /> : <Play size={10} />}
@ -260,6 +312,16 @@ export const Timeline: React.FC<Props> = React.memo(({
</select> </select>
</div> </div>
<Button
variant="secondary"
size="icon"
className="size-5"
onClick={() => setLock(!lock)}
title="锁定时间"
>
{lock ? <LockIcon size={10} /> : <UnlockIcon size={10} />}
</Button>
</div> </div>

View File

@ -26,7 +26,7 @@ export function MapComponent({
// center = [103.851959, 1.290270], // center = [103.851959, 1.290270],
// zoom = 11 // zoom = 11
imgBitmap: propImgBitmap, imgBitmap: propImgBitmap,
colorMapType = 'heatmap', colorMapType = 'meteorological',
onColorMapChange onColorMapChange
}: MapComponentProps) { }: MapComponentProps) {

View File

@ -69,7 +69,8 @@ export function Timeline({
const newDays = Math.round((newProgress / 100) * totalDays) const newDays = Math.round((newProgress / 100) * totalDays)
const newDate = addDays(startDate, newDays) const newDate = addDays(startDate, newDays)
onDateChange?.(newDate) onDateChange?.(newDate)
}, [startDate, totalDays, onDateChange]) timeline.setTime(newDate)
}, [startDate, totalDays, onDateChange, timeline])
const handlePlayPause = useCallback(() => { const handlePlayPause = useCallback(() => {
if (isPlaying) { if (isPlaying) {
@ -82,12 +83,14 @@ export function Timeline({
const handleSkipBack = useCallback(() => { const handleSkipBack = useCallback(() => {
const newDate = subDays(currentDate, 1) const newDate = subDays(currentDate, 1)
onDateChange?.(newDate) onDateChange?.(newDate)
}, [currentDate, onDateChange]) timeline.setTime(newDate)
}, [currentDate, onDateChange, timeline])
const handleSkipForward = useCallback(() => { const handleSkipForward = useCallback(() => {
const newDate = addDays(currentDate, 1) const newDate = addDays(currentDate, 1)
onDateChange?.(newDate) onDateChange?.(newDate)
}, [currentDate, onDateChange]) timeline.setTime(newDate)
}, [currentDate, onDateChange, timeline])
const speedOptions = [ const speedOptions = [
{ value: 'slow', label: '慢速', interval: 2000 }, { value: 'slow', label: '慢速', interval: 2000 },

View File

@ -1,28 +1,11 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { addDays, subDays } from 'date-fns' import { addDays, subDays } from 'date-fns'
import { useMap } from '@/app/map-context' import { useMap } from '@/app/map-context'
import { useSubscription, gql } from '@apollo/client'
import { parse } from 'date-fns'
import { UTCDate } from "@date-fns/utc";
import { toZonedTime } from 'date-fns-tz';
interface UseTimelineOptions {
startDate?: Date
endDate?: Date
initialDate?: Date
onDateChange?: (date: Date) => void
autoPlay?: boolean
}
export function useTimeline({
startDate = subDays(new Date(), 30),
endDate = new Date(),
initialDate = new Date(),
onDateChange,
autoPlay = false
}: UseTimelineOptions = {}) {
const [currentDate, setCurrentDate] = useState(initialDate)
const [isPlaying, setIsPlaying] = useState(autoPlay)
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const { setTime } = useMap()
const speedIntervals = { const speedIntervals = {
slow: 2000, slow: 2000,
@ -30,11 +13,35 @@ export function useTimeline({
fast: 500 fast: 500
} }
interface UseTimelineOptions {
startDate?: Date
endDate?: Date
initialDate?: Date
onDateChange?: (date: Date) => void
autoPlay?: boolean
autoUpdate?: boolean
}
export function useTimeline({
startDate = subDays(new Date(), 30),
endDate = new Date(),
onDateChange,
autoPlay = false
}: UseTimelineOptions = {}) {
const [isPlaying, setIsPlaying] = useState(autoPlay)
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const { setTime, currentDatetime } = useMap()
const updateDate = useCallback((newDate: Date) => { const updateDate = useCallback((newDate: Date) => {
setCurrentDate(newDate) setTime(newDate)
onDateChange?.(newDate) onDateChange?.(newDate)
}, [onDateChange]) }, [onDateChange])
const play = useCallback(() => { const play = useCallback(() => {
setIsPlaying(true) setIsPlaying(true)
}, []) }, [])
@ -48,18 +55,20 @@ export function useTimeline({
}, []) }, [])
const skipForward = useCallback(() => { const skipForward = useCallback(() => {
const newDate = addDays(currentDate, 1) if (!currentDatetime) return;
const newDate = addDays(currentDatetime, 1)
if (newDate <= endDate) { if (newDate <= endDate) {
updateDate(newDate) updateDate(newDate)
} }
}, [currentDate, endDate, updateDate]) }, [currentDatetime, endDate, updateDate])
const skipBackward = useCallback(() => { const skipBackward = useCallback(() => {
const newDate = subDays(currentDate, 1) if (!currentDatetime) return;
const newDate = subDays(currentDatetime, 1)
if (newDate >= startDate) { if (newDate >= startDate) {
updateDate(newDate) updateDate(newDate)
} }
}, [currentDate, startDate, updateDate]) }, [currentDatetime, startDate, updateDate])
const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => { const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => {
setSpeed(newSpeed) setSpeed(newSpeed)
@ -75,7 +84,8 @@ export function useTimeline({
useEffect(() => { useEffect(() => {
if (isPlaying) { if (isPlaying) {
intervalRef.current = setInterval(() => { intervalRef.current = setInterval(() => {
const nextDate = addDays(currentDate, 1) if (!currentDatetime) return;
const nextDate = addDays(currentDatetime, 1)
if (nextDate <= endDate) { if (nextDate <= endDate) {
updateDate(nextDate) updateDate(nextDate)
} else { } else {
@ -95,7 +105,7 @@ export function useTimeline({
clearInterval(intervalRef.current) clearInterval(intervalRef.current)
} }
} }
}, [isPlaying, currentDate, endDate, speed, updateDate]) }, [isPlaying, currentDatetime, endDate, speed, updateDate])
// 清理定时器 // 清理定时器
useEffect(() => { useEffect(() => {
@ -107,7 +117,7 @@ export function useTimeline({
}, []) }, [])
return { return {
currentDate, currentDatetime,
isPlaying, isPlaying,
speed, speed,
startDate, startDate,

View File

@ -3,6 +3,8 @@
* globe模式下的球面渲染 * globe模式下的球面渲染
*/ */
import { MercatorCoordinate } from 'maplibre-gl'
export interface TileMeshOptions { export interface TileMeshOptions {
/** 经纬度边界 [west, south, east, north] */ /** 经纬度边界 [west, south, east, north] */
bounds: [number, number, number, number]; bounds: [number, number, number, number];
@ -163,10 +165,12 @@ export function detectPerformanceLevel(): PerformanceLevel {
* Web Mercator坐标 (0-1) * Web Mercator坐标 (0-1)
*/ */
function lonLatToMercator(lon: number, lat: number): [number, number] { function lonLatToMercator(lon: number, lat: number): [number, number] {
const x = (lon + 180) / 360; const mercator = MercatorCoordinate.fromLngLat({ lng: lon, lat: lat })
const latRad = (lat * Math.PI) / 180; return [mercator.x, mercator.y]
const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2; // const x = (lon + 180) / 360;
return [x, y]; // const latRad = (lat * Math.PI) / 180;
// const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
// return [x, y];
} }
/** /**

View File

@ -110,6 +110,8 @@ interface TimelineConfig {
highlightWeekends?: boolean; highlightWeekends?: boolean;
/** 时间标记列表 */ /** 时间标记列表 */
timeMarks?: TimeMark[]; timeMarks?: TimeMark[];
onDateChange?: (date: Date) => void;
} }
/** 时间工具类 */ /** 时间工具类 */
@ -649,7 +651,8 @@ class RealTimeTimeline {
}, },
showCurrentTime: true, showCurrentTime: true,
highlightWeekends: true, highlightWeekends: true,
timeMarks: [] timeMarks: [],
onDateChange: () => { }
}; };
if (!config) return defaultConfig; if (!config) return defaultConfig;
@ -664,7 +667,8 @@ class RealTimeTimeline {
sizes: { ...defaultConfig.sizes, ...config.sizes }, sizes: { ...defaultConfig.sizes, ...config.sizes },
showCurrentTime: config.showCurrentTime ?? defaultConfig.showCurrentTime, showCurrentTime: config.showCurrentTime ?? defaultConfig.showCurrentTime,
highlightWeekends: config.highlightWeekends ?? defaultConfig.highlightWeekends, highlightWeekends: config.highlightWeekends ?? defaultConfig.highlightWeekends,
timeMarks: config.timeMarks ?? defaultConfig.timeMarks timeMarks: config.timeMarks ?? defaultConfig.timeMarks,
onDateChange: config.onDateChange ?? defaultConfig.onDateChange
}; };
} }
@ -723,13 +727,7 @@ class RealTimeTimeline {
// 获取当前刻度信息用于吸附 // 获取当前刻度信息用于吸附
const ticks = this.scaleManager.calculateTicks(this.viewport); const ticks = this.scaleManager.calculateTicks(this.viewport);
const date = this.viewport.screenToTime(x, true, ticks); const date = this.viewport.screenToTime(x, true, ticks);
this.interaction.setZoomMode(ZoomMode.MarkMode); this.changeTime(new Date(date))
this.replaceTimeMark({
timestamp: date,
color: '#ff6b6b',
label: format(date, 'HH:mm:ss'),
type: 'custom'
});
} }
}); });
@ -814,6 +812,17 @@ class RealTimeTimeline {
this.drawTimeMarks(); this.drawTimeMarks();
} }
private changeTime(date: Date): void {
this.interaction.setZoomMode(ZoomMode.MarkMode);
this.replaceTimeMark({
timestamp: date.getTime(),
color: '#ff6b6b',
label: format(date, 'HH:mm:ss'),
type: 'custom'
});
this.config.onDateChange?.(date)
}
/** 绘制周末高亮 */ /** 绘制周末高亮 */
private drawWeekends(): void { private drawWeekends(): void {
const [startTime, endTime] = this.viewport.getVisibleRange(); const [startTime, endTime] = this.viewport.getVisibleRange();
@ -1038,19 +1047,25 @@ class RealTimeTimeline {
this.render(); this.render();
} }
replaceTimeMarkByTimestamp(timestamp: number): void {
this.replaceTimeMark({
timestamp: timestamp,
color: '#ff6b6b',
label: format(timestamp, 'HH:mm:ss'),
type: 'custom'
});
this.interaction.setZoomMode(ZoomMode.MarkMode)
}
forwardTimeMark(delta: number, timestamp?: number): void { forwardTimeMark(delta: number, timestamp?: number): void {
if (!timestamp) { if (!timestamp) {
timestamp = this.config.timeMarks[0].timestamp; timestamp = this.config.timeMarks[0].timestamp;
} }
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp); const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
if (index !== -1) { if (index !== -1) {
this.config.timeMarks.splice(index, 1); this.changeTime(new Date(timestamp + delta))
this.replaceTimeMark({
timestamp: timestamp + delta,
color: '#ff6b6b',
label: format(timestamp + delta, 'HH:mm:ss'),
type: 'custom'
});
this.render(); this.render();
} }
} }
@ -1061,13 +1076,7 @@ class RealTimeTimeline {
} }
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp); const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
if (index !== -1) { if (index !== -1) {
this.config.timeMarks.splice(index, 1); this.changeTime(new Date(timestamp - delta))
this.replaceTimeMark({
timestamp: timestamp - delta,
color: '#ff6b6b',
label: format(timestamp - delta, 'HH:mm:ss'),
type: 'custom'
});
this.render(); this.render();
} }
} }

33
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@21st-extension/react": "^0.5.14", "@21st-extension/react": "^0.5.14",
"@21st-extension/toolbar-next": "^0.5.14", "@21st-extension/toolbar-next": "^0.5.14",
"@apollo/client": "^3.13.9", "@apollo/client": "^3.13.9",
"@date-fns/utc": "^2.1.1",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@ -37,6 +38,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dnd-kit": "^0.0.2", "dnd-kit": "^0.0.2",
"framer-motion": "^12.23.6", "framer-motion": "^12.23.6",
"graphql": "^16.11.0", "graphql": "^16.11.0",
@ -196,6 +198,12 @@
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==" "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="
}, },
"node_modules/@date-fns/utc": {
"version": "2.1.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/@date-fns/utc/-/utc-2.1.1.tgz",
"integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==",
"license": "MIT"
},
"node_modules/@dnd-kit/accessibility": { "node_modules/@dnd-kit/accessibility": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", "resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -3254,8 +3262,9 @@
}, },
"node_modules/date-fns": { "node_modules/date-fns": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "resolved": "http://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
@ -3266,6 +3275,15 @@
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==" "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
}, },
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/decimal.js-light": { "node_modules/decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@ -5659,6 +5677,11 @@
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==" "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="
}, },
"@date-fns/utc": {
"version": "2.1.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/@date-fns/utc/-/utc-2.1.1.tgz",
"integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="
},
"@dnd-kit/accessibility": { "@dnd-kit/accessibility": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", "resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -7508,7 +7531,7 @@
}, },
"date-fns": { "date-fns": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "resolved": "http://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
}, },
"date-fns-jalali": { "date-fns-jalali": {
@ -7516,6 +7539,12 @@
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==" "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
}, },
"date-fns-tz": {
"version": "3.2.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"requires": {}
},
"decimal.js-light": { "decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",

View File

@ -12,6 +12,7 @@
"@21st-extension/react": "^0.5.14", "@21st-extension/react": "^0.5.14",
"@21st-extension/toolbar-next": "^0.5.14", "@21st-extension/toolbar-next": "^0.5.14",
"@apollo/client": "^3.13.9", "@apollo/client": "^3.13.9",
"@date-fns/utc": "^2.1.1",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@ -38,6 +39,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dnd-kit": "^0.0.2", "dnd-kit": "^0.0.2",
"framer-motion": "^12.23.6", "framer-motion": "^12.23.6",
"graphql": "^16.11.0", "graphql": "^16.11.0",