diff --git a/app/map-context.tsx b/app/map-context.tsx index 374bf0e..0853790 100644 --- a/app/map-context.tsx +++ b/app/map-context.tsx @@ -1,15 +1,14 @@ 'use client' import React, { createContext, useContext, useRef, useState, ReactNode } from 'react' -// import Map from 'ol/Map'; import { Map } from 'maplibre-gl'; -import { fromLonLat } from 'ol/proj'; // 定义MapContext的类型 interface MapContextType { mapRef: React.RefObject layers: React.RefObject mapState: MapState + currentDatetime: Date | null setMap: (map: Map, layers: any[]) => void flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void zoomIn: () => void @@ -21,6 +20,8 @@ interface MapContextType { isMapReady: boolean } + + // 创建Context const MapContext = createContext(undefined) @@ -42,7 +43,7 @@ export function MapProvider({ children }: MapProviderProps) { }); const layersRef = useRef([]) - + const [currentDatetime, setCurrentDatetime] = useState(null) const setMap = (map: Map, layers: any[]) => { // 如果已经有地图实例,先清理旧的 @@ -51,9 +52,6 @@ export function MapProvider({ children }: MapProviderProps) { mapRef.current = null; } - // 监听视图变化事件 - // const view = map.getView(); - // 监听视图的缩放变化 map.on('zoom', () => { setMapState(prevState => ({ @@ -78,12 +76,6 @@ export function MapProvider({ children }: MapProviderProps) { const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => { if (mapRef.current) { - // mapRef.current.getView().animate({ - // center: fromLonLat(options.center), - // zoom: options.zoom, - // duration: options.duration || 1000 - // }) - mapRef.current.flyTo({ center: options.center, zoom: options.zoom, @@ -95,42 +87,28 @@ export function MapProvider({ children }: MapProviderProps) { const zoomIn = () => { if (mapRef.current) { - // mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! + 1) mapRef.current.zoomIn() } } const zoomOut = () => { if (mapRef.current) { - // mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! - 1) mapRef.current.zoomOut() } } const zoomTo = (zoom: number) => { if (mapRef.current) { - // mapRef.current.getView().setZoom(zoom) mapRef.current.zoomTo(zoom) } } const setTime = (date: Date) => { - if (mapRef.current) { - layersRef.current.forEach(layer => { - const source = layer.getSource() - if (source) { - source.updateParams({ - 'TIME': date.toISOString() - }) - } - }) - } + setCurrentDatetime(date) } const reset = () => { if (mapRef.current) { - // mapRef.current.getView().setCenter([103.851959, 1.290270]) - // mapRef.current.getView().setZoom(11) mapRef.current.flyTo({ center: [103.851959, 1.290270], zoom: 11, @@ -154,6 +132,7 @@ export function MapProvider({ children }: MapProviderProps) { const value: MapContextType = { setTime, + currentDatetime, mapRef, layers: layersRef, mapState, diff --git a/app/timeline.tsx b/app/timeline.tsx index bee245e..b178d2f 100644 --- a/app/timeline.tsx +++ b/app/timeline.tsx @@ -133,7 +133,6 @@ function SelectDemo() { export const Timeline: React.FC = ({ startDate, endDate, - currentDate, onDateChange, onPlay, onPause, @@ -148,19 +147,12 @@ export const Timeline: React.FC = ({ const canvasRef = useRef(null); const ticksCanvasRef = useRef(null); - - const { isPlaying, togglePlay } = useTimeline({ - initialDate: currentDate ?? new Date(), - onDateChange(date) { - onDateChange?.(date); - } - }) - + const { isPlaying, togglePlay, loading, setTime, currentDatetime: currentDate } = useTimeline({}) const [state, setState] = useState({ isDragging: false, isLongPress: false, isPanningTimeline: false, - customLineTimestamp: currentDate?.getTime() ?? new Date().getTime(), + customLineTimestamp: currentDate?.getTime() ?? null, panOffset: 0, zoomLevel: initialZoom, }); @@ -188,6 +180,7 @@ export const Timeline: React.FC = ({ const newDate = new Date(state.customLineTimestamp! + 360000); setState({ ...state, customLineTimestamp: newDate.getTime() }); onDateChange?.(newDate); + setTime(newDate); }, [state.customLineTimestamp]) // 缩放处理函数 @@ -282,7 +275,7 @@ export const Timeline: React.FC = ({ dpr, overrides.startDate ?? currentProps.startDate, overrides.endDate ?? currentProps.endDate, - overrides.currentDate ?? currentProps.currentDate, + overrides.currentDate ?? currentProps.currentDate ?? undefined, overrides.zoomLevel ?? currentState.zoomLevel, overrides.panOffset ?? currentState.panOffset, overrides.customLineTimestamp ?? currentState.customLineTimestamp, @@ -386,17 +379,24 @@ export const Timeline: React.FC = ({ const selectedTimestamp = visibleStartTime + progress * visibleTimeRange; + // 规整到最近的6分钟整数时间 + const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒 + const roundedTimestamp = Math.round(selectedTimestamp / sixMinutesInMs) * sixMinutesInMs; + setState(prevState => ({ ...prevState, - customLineTimestamp: selectedTimestamp, + customLineTimestamp: roundedTimestamp, isLongPress: false, isDragging: false })); // 通知父组件 if (onDateChange) { - onDateChange(new Date(selectedTimestamp)); + onDateChange(new Date(roundedTimestamp)); } + + // 使用setTime更新时间轴状态 + setTime(new Date(roundedTimestamp)); } } } @@ -601,12 +601,19 @@ export const Timeline: React.FC = ({ const finalTimestamp = visibleStartTime + progress * visibleTimeRange; + // 规整到最近的6分钟整数时间 + const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒 + const roundedFinalTimestamp = Math.round(finalTimestamp / sixMinutesInMs) * sixMinutesInMs; + setState(prevState => ({ ...prevState, - customLineTimestamp: finalTimestamp, + customLineTimestamp: roundedFinalTimestamp, isDragging: false, isLongPress: false })); + + // 使用setTime更新时间轴状态 + setTime(new Date(roundedFinalTimestamp)); } else { setState(prevState => ({ ...prevState, isDragging: false, isLongPress: false })); } @@ -706,9 +713,6 @@ export const Timeline: React.FC = ({ current_uniforms.current.currentTimestamp = currentDate.getTime(); } - - - // 鼠标滚轮缩放和平移 const handleWheel = (e: WheelEvent) => { e.preventDefault(); diff --git a/app/tl.tsx b/app/tl.tsx index 4f61732..815bf3f 100644 --- a/app/tl.tsx +++ b/app/tl.tsx @@ -3,12 +3,26 @@ import vsSource from './glsl/timeline/vert.glsl'; import fsSource from './glsl/timeline/frag.glsl'; import { cn } from "@/lib/utils"; 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 { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline"; 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 { startTimestamp: number; // Unix 时间戳开始 @@ -65,7 +79,8 @@ export const Timeline: React.FC = React.memo(({ timelineConfig, ...props }) => { - const { isPlaying, togglePlay } = useTimeline({}) + const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({}) + const [lock, setLock] = useState(false) const canvasRef = useRef(null); const ticksCanvasRef = useRef(null); const timelineEngineRef = useRef(null); @@ -77,6 +92,21 @@ export const Timeline: React.FC = React.memo(({ 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(() => { let intervalId: NodeJS.Timeout | null = null; @@ -85,7 +115,6 @@ export const Timeline: React.FC = React.memo(({ intervalId = setInterval(() => { // 执行时间前进操作 if (timelineEngineRef.current) { - // timelineEngineRef.current.forwardTimeMark(timeStep); timelineEngineRef.current.playAndEnsureMarkInView(timeStep) } }, 1000); // 每秒执行一次,你可以根据需要调整这个间隔 @@ -98,6 +127,12 @@ export const Timeline: React.FC = React.memo(({ }; }, [isPlaying, timeStep]); + useEffect(() => { + if (currentDatetime && !lock) { + timelineEngineRef.current?.replaceTimeMarkByTimestamp(currentDatetime.getTime()) + } + }, [currentDatetime, lock]) + useEffect(() => { if (!ticksCanvasRef.current) return; @@ -132,6 +167,20 @@ export const Timeline: React.FC = React.memo(({ primaryFontSize: 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 }; @@ -210,7 +259,10 @@ export const Timeline: React.FC = React.memo(({ variant="secondary" size="icon" className="size-5" - onClick={() => togglePlay()} + onClick={() => { + togglePlay() + setLock(true) + }} title={isPlaying ? "暂停" : "播放"} > {isPlaying ? : } @@ -260,6 +312,16 @@ export const Timeline: React.FC = React.memo(({ + + diff --git a/components/map-component.tsx b/components/map-component.tsx index 776f9f1..b9c7866 100644 --- a/components/map-component.tsx +++ b/components/map-component.tsx @@ -26,7 +26,7 @@ export function MapComponent({ // center = [103.851959, 1.290270], // zoom = 11 imgBitmap: propImgBitmap, - colorMapType = 'heatmap', + colorMapType = 'meteorological', onColorMapChange }: MapComponentProps) { diff --git a/components/timeline.tsx b/components/timeline.tsx index dc8ad44..9592bd8 100644 --- a/components/timeline.tsx +++ b/components/timeline.tsx @@ -69,7 +69,8 @@ export function Timeline({ const newDays = Math.round((newProgress / 100) * totalDays) const newDate = addDays(startDate, newDays) onDateChange?.(newDate) - }, [startDate, totalDays, onDateChange]) + timeline.setTime(newDate) + }, [startDate, totalDays, onDateChange, timeline]) const handlePlayPause = useCallback(() => { if (isPlaying) { @@ -82,12 +83,14 @@ export function Timeline({ const handleSkipBack = useCallback(() => { const newDate = subDays(currentDate, 1) onDateChange?.(newDate) - }, [currentDate, onDateChange]) + timeline.setTime(newDate) + }, [currentDate, onDateChange, timeline]) const handleSkipForward = useCallback(() => { const newDate = addDays(currentDate, 1) onDateChange?.(newDate) - }, [currentDate, onDateChange]) + timeline.setTime(newDate) + }, [currentDate, onDateChange, timeline]) const speedOptions = [ { value: 'slow', label: '慢速', interval: 2000 }, diff --git a/hooks/use-timeline.ts b/hooks/use-timeline.ts index faca144..1b5a1b8 100644 --- a/hooks/use-timeline.ts +++ b/hooks/use-timeline.ts @@ -1,6 +1,19 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { addDays, subDays } from 'date-fns' 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'; + + +const speedIntervals = { + slow: 2000, + normal: 1000, + fast: 500 +} + + interface UseTimelineOptions { startDate?: Date @@ -8,33 +21,27 @@ interface UseTimelineOptions { initialDate?: Date onDateChange?: (date: Date) => void autoPlay?: boolean + autoUpdate?: 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(null) - - const { setTime } = useMap() - - const speedIntervals = { - slow: 2000, - normal: 1000, - fast: 500 - } + const { setTime, currentDatetime } = useMap() const updateDate = useCallback((newDate: Date) => { - setCurrentDate(newDate) + setTime(newDate) onDateChange?.(newDate) }, [onDateChange]) + const play = useCallback(() => { setIsPlaying(true) }, []) @@ -48,18 +55,20 @@ export function useTimeline({ }, []) const skipForward = useCallback(() => { - const newDate = addDays(currentDate, 1) + if (!currentDatetime) return; + const newDate = addDays(currentDatetime, 1) if (newDate <= endDate) { updateDate(newDate) } - }, [currentDate, endDate, updateDate]) + }, [currentDatetime, endDate, updateDate]) const skipBackward = useCallback(() => { - const newDate = subDays(currentDate, 1) + if (!currentDatetime) return; + const newDate = subDays(currentDatetime, 1) if (newDate >= startDate) { updateDate(newDate) } - }, [currentDate, startDate, updateDate]) + }, [currentDatetime, startDate, updateDate]) const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => { setSpeed(newSpeed) @@ -75,7 +84,8 @@ export function useTimeline({ useEffect(() => { if (isPlaying) { intervalRef.current = setInterval(() => { - const nextDate = addDays(currentDate, 1) + if (!currentDatetime) return; + const nextDate = addDays(currentDatetime, 1) if (nextDate <= endDate) { updateDate(nextDate) } else { @@ -95,7 +105,7 @@ export function useTimeline({ clearInterval(intervalRef.current) } } - }, [isPlaying, currentDate, endDate, speed, updateDate]) + }, [isPlaying, currentDatetime, endDate, speed, updateDate]) // 清理定时器 useEffect(() => { @@ -107,7 +117,7 @@ export function useTimeline({ }, []) return { - currentDate, + currentDatetime, isPlaying, speed, startDate, diff --git a/lib/tile-mesh.ts b/lib/tile-mesh.ts index 657d7c4..2904cbe 100644 --- a/lib/tile-mesh.ts +++ b/lib/tile-mesh.ts @@ -3,6 +3,8 @@ * 用于globe模式下的球面渲染 */ +import { MercatorCoordinate } from 'maplibre-gl' + export interface TileMeshOptions { /** 经纬度边界 [west, south, east, north] */ bounds: [number, number, number, number]; @@ -163,10 +165,12 @@ export function detectPerformanceLevel(): PerformanceLevel { * 将经纬度转换为Web Mercator坐标 (归一化到0-1范围) */ function lonLatToMercator(lon: number, lat: number): [number, number] { - const x = (lon + 180) / 360; - const latRad = (lat * Math.PI) / 180; - const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2; - return [x, y]; + const mercator = MercatorCoordinate.fromLngLat({ lng: lon, lat: lat }) + return [mercator.x, mercator.y] + // const x = (lon + 180) / 360; + // const latRad = (lat * Math.PI) / 180; + // const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2; + // return [x, y]; } /** diff --git a/lib/timeline.ts b/lib/timeline.ts index 0812377..9bfe795 100644 --- a/lib/timeline.ts +++ b/lib/timeline.ts @@ -110,6 +110,8 @@ interface TimelineConfig { highlightWeekends?: boolean; /** 时间标记列表 */ timeMarks?: TimeMark[]; + + onDateChange?: (date: Date) => void; } /** 时间工具类 */ @@ -649,7 +651,8 @@ class RealTimeTimeline { }, showCurrentTime: true, highlightWeekends: true, - timeMarks: [] + timeMarks: [], + onDateChange: () => { } }; if (!config) return defaultConfig; @@ -664,7 +667,8 @@ class RealTimeTimeline { sizes: { ...defaultConfig.sizes, ...config.sizes }, showCurrentTime: config.showCurrentTime ?? defaultConfig.showCurrentTime, 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 date = this.viewport.screenToTime(x, true, ticks); - this.interaction.setZoomMode(ZoomMode.MarkMode); - this.replaceTimeMark({ - timestamp: date, - color: '#ff6b6b', - label: format(date, 'HH:mm:ss'), - type: 'custom' - }); + this.changeTime(new Date(date)) } }); @@ -814,6 +812,17 @@ class RealTimeTimeline { 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 { const [startTime, endTime] = this.viewport.getVisibleRange(); @@ -1038,19 +1047,25 @@ class RealTimeTimeline { 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 { if (!timestamp) { timestamp = this.config.timeMarks[0].timestamp; } const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp); if (index !== -1) { - this.config.timeMarks.splice(index, 1); - this.replaceTimeMark({ - timestamp: timestamp + delta, - color: '#ff6b6b', - label: format(timestamp + delta, 'HH:mm:ss'), - type: 'custom' - }); + this.changeTime(new Date(timestamp + delta)) this.render(); } } @@ -1061,13 +1076,7 @@ class RealTimeTimeline { } const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp); if (index !== -1) { - this.config.timeMarks.splice(index, 1); - this.replaceTimeMark({ - timestamp: timestamp - delta, - color: '#ff6b6b', - label: format(timestamp - delta, 'HH:mm:ss'), - type: 'custom' - }); + this.changeTime(new Date(timestamp - delta)) this.render(); } } diff --git a/package-lock.json b/package-lock.json index 930f809..b1d596e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@21st-extension/react": "^0.5.14", "@21st-extension/toolbar-next": "^0.5.14", "@apollo/client": "^3.13.9", + "@date-fns/utc": "^2.1.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -37,6 +38,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dnd-kit": "^0.0.2", "framer-motion": "^12.23.6", "graphql": "^16.11.0", @@ -196,6 +198,12 @@ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", "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": { "version": "3.1.1", "resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -3254,8 +3262,9 @@ }, "node_modules/date-fns": { "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==", + "license": "MIT", "funding": { "type": "github", "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", "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": { "version": "2.5.1", "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", "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": { "version": "3.1.1", "resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -7508,7 +7531,7 @@ }, "date-fns": { "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==" }, "date-fns-jalali": { @@ -7516,6 +7539,12 @@ "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==" }, + "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": { "version": "2.5.1", "resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz", diff --git a/package.json b/package.json index 7ca64fe..331c824 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@21st-extension/react": "^0.5.14", "@21st-extension/toolbar-next": "^0.5.14", "@apollo/client": "^3.13.9", + "@date-fns/utc": "^2.1.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -38,6 +39,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dnd-kit": "^0.0.2", "framer-motion": "^12.23.6", "graphql": "^16.11.0",