Compare commits

..

3 Commits

Author SHA1 Message Date
9646e5d0e1 sync 2025-08-08 07:20:05 +08:00
520efbbb0d Merge branch 'master' of ssh://ssh.keitsuki.top:222/mmosaic/mosaicmap 2025-08-07 21:45:42 +08:00
06ba5d7ab1 render real data 2025-08-07 21:42:04 +08:00
13 changed files with 331 additions and 151 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

@ -18,20 +18,8 @@ import { ThemeToggle } from '@/components/theme-toggle';
// import { Timeline } from '@/app/timeline'; // import { Timeline } from '@/app/timeline';
import { Timeline } from '@/app/tl'; import { Timeline } from '@/app/tl';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTimeline } from '@/hooks/use-timeline';
import { useEffect } from 'react'
import { useRadarTile } from '@/hooks/use-radartile'
import { gql, useSubscription } from '@apollo/client'
const SUBSCRIPTION_QUERY = gql`
subscription {
statusUpdates {
id
message
status
}
}
`
export default function Page() { export default function Page() {
@ -39,16 +27,6 @@ export default function Page() {
const now = new Date(); const now = new Date();
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前 const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后 const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
const { setTime } = useTimeline()
const { imgBitmap, fetchRadarTile } = useRadarTile({})
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
useEffect(() => {
if (data) {
console.log(data.statusUpdates)
}
}, [data])
return ( return (
<SidebarProvider> <SidebarProvider>
@ -62,22 +40,7 @@ export default function Page() {
</div> </div>
</header> </header>
<div className="relative h-full w-full flex flex-col"> <div className="relative h-full w-full flex flex-col">
<MapComponent imgBitmap={imgBitmap} /> <MapComponent />
{/* <Timeline
className={
cn(
"backdrop-blur-lg border shadow-lg",
"bg-background/90 border-border",
"z-10"
)
}
startDate={startDate}
endDate={endDate}
onDateChange={(date) => {
console.log('Selected date:', date);
setTime(date)
}}
/> */}
<Timeline /> <Timeline />
</div> </div>
</SidebarInset> </SidebarInset>

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

@ -7,6 +7,8 @@ import { useMapLocation } from '@/hooks/use-map-location'
import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh' import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh'
import { createColorMap, ColorMapType, } from '@/lib/color-maps' import { createColorMap, ColorMapType, } from '@/lib/color-maps'
import { Colorbar } from './colorbar' import { Colorbar } from './colorbar'
import { useRadarTile } from '@/hooks/use-radartile'
import { format, formatInTimeZone } from 'date-fns-tz'
interface MapComponentProps { interface MapComponentProps {
style?: string style?: string
@ -24,13 +26,14 @@ 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) {
const { fetchRadarTile, imgBitmap } = useRadarTile();
const mapContainer = useRef<HTMLDivElement>(null) const mapContainer = useRef<HTMLDivElement>(null)
const { setMap } = useMap() const { setMap, mapRef, currentDatetime, isMapReady } = useMap()
const { location } = useMapLocation() const { location } = useMapLocation()
const imgBitmap = propImgBitmap
const texRef = useRef<WebGLTexture | null>(null) const texRef = useRef<WebGLTexture | null>(null)
const lutTexRef = useRef<WebGLTexture | null>(null) const lutTexRef = useRef<WebGLTexture | null>(null)
const glRef = useRef<WebGL2RenderingContext | null>(null) const glRef = useRef<WebGL2RenderingContext | null>(null)
@ -38,6 +41,13 @@ export function MapComponent({
const [isReady, setIsReady] = useState<boolean>(false) const [isReady, setIsReady] = useState<boolean>(false)
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType) const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
useEffect(() => {
if (!isMapReady || !currentDatetime) return;
const utc_time_str = formatInTimeZone(currentDatetime, 'UTC', 'yyyyMMddHHmmss')
const new_url = `http://localhost:3050/api/v1/data?datetime=${utc_time_str}&area=cn`
fetchRadarTile(new_url)
}, [currentDatetime, isMapReady])
useEffect(() => { useEffect(() => {
if (!mapContainer.current) return if (!mapContainer.current) return
@ -101,8 +111,9 @@ export function MapComponent({
// 对于灰度图RGB通道通常相同取红色通道作为灰度值 // 对于灰度图RGB通道通常相同取红色通道作为灰度值
float value = texColor.r * 3.4; float value = texColor.r * 3.4;
if (value == 0.0) { if (value < 0.07) {
discard; fragColor= vec4(1.0,1.0,1.0,0.2);
return;
} }
// normalizedValue = clamp(normalizedValue, 0.0, 1.0); // normalizedValue = clamp(normalizedValue, 0.0, 1.0);
@ -111,8 +122,9 @@ export function MapComponent({
vec4 lutColor = texture(u_lut, vec2(value, 0.5)); vec4 lutColor = texture(u_lut, vec2(value, 0.5));
// 添加一些透明度,使低值区域更透明 // 添加一些透明度,使低值区域更透明
// float alpha = smoothstep(0.0, 0.1, value); // float alpha = smoothstep(0.0, 0.1, value);
float alpha = 1.0; float alpha = 0.7;
fragColor = vec4(lutColor.rgb, alpha); fragColor = vec4(lutColor.rgb, alpha);
// fragColor = vec4(1.0,1.0,1.0,0.2);
}` }`
console.log(vertexSource, fragmentSource) console.log(vertexSource, fragmentSource)
@ -166,8 +178,8 @@ export function MapComponent({
} }
gl.bindTexture(gl.TEXTURE_2D, tex); gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
@ -463,6 +475,10 @@ export function MapComponent({
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null); gl.bindTexture(gl.TEXTURE_2D, null);
// Redraw the map
mapRef.current?.triggerRepaint()
} }
}, [imgBitmap, isReady]) }, [imgBitmap, isReady])
@ -556,8 +572,8 @@ function createLutTexture(gl: WebGL2RenderingContext, colorMapType: ColorMapType
lut lut
) )
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

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

@ -24,7 +24,7 @@ const LOCATIONS = {
export type LocationKey = keyof typeof LOCATIONS export type LocationKey = keyof typeof LOCATIONS
export function useMapLocation() { export function useMapLocation() {
const [currentLocation, setCurrentLocation] = useState<LocationKey>('usa') const [currentLocation, setCurrentLocation] = useState<LocationKey>('china')
const { flyTo, isMapReady } = useMap() const { flyTo, isMapReady } = useMap()
const flyToLocation = useCallback((location: LocationKey) => { const flyToLocation = useCallback((location: LocationKey) => {

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

@ -1,5 +1,5 @@
// 色标函数集合 // 色标函数集合
export type ColorMapType = 'radar' | 'rainbow' | 'heatmap' | 'viridis' | 'plasma' | 'grayscale'; export type ColorMapType = 'radar' | 'rainbow' | 'heatmap' | 'viridis' | 'plasma' | 'grayscale' | 'meteorological';
// 雷达色标 (深蓝到红色,类似气象雷达) // 雷达色标 (深蓝到红色,类似气象雷达)
export function createRadarColorMap(): Uint8Array { export function createRadarColorMap(): Uint8Array {
@ -207,11 +207,109 @@ export function createColorMap(type: ColorMapType): Uint8Array {
return createPlasmaColorMap(); return createPlasmaColorMap();
case 'grayscale': case 'grayscale':
return createGrayscaleColorMap(); return createGrayscaleColorMap();
case 'meteorological':
return createMeteorologicalColorMap();
default: default:
return createRadarColorMap(); return createRadarColorMap();
} }
} }
// COLOR_MAP = [
// ("#01a0f6", (5, 10)),
// ("#00ecec", (10, 15)),
// ("#6dfa3d", (15, 20)),
// ("#00d802", (20, 25)),
// ("#019001", (25, 30)),
// ("#ffff04", (30, 35)),
// ("#e7c002", (35, 40)),
// ("#ff9002", (40, 45)),
// ("#ff0201", (45, 50)),
// ("#d60101", (50, 55)),
// ("#c00100", (55, 60)),
// ("#ff00f0", (60, 65)),
// ("#9600b4", (65, 70)),
// ("#ad90f0", (70, 75)),
// ]
// 根据具体数值区间创建的气象雷达色标
export function createMeteorologicalColorMap(): Uint8Array {
const lut = new Uint8Array(256 * 4);
// 定义颜色和数值区间对应关系
const colorRanges: Array<{ color: [number, number, number]; range: [number, number] }> = [
{ color: [1, 160, 246], range: [5, 10] }, // #01a0f6
{ color: [0, 236, 236], range: [10, 15] }, // #00ecec
{ color: [109, 250, 61], range: [15, 20] }, // #6dfa3d
{ color: [0, 216, 2], range: [20, 25] }, // #00d802
{ color: [1, 144, 1], range: [25, 30] }, // #019001
{ color: [255, 255, 4], range: [30, 35] }, // #ffff04 (黄色,重要节点)
{ color: [231, 192, 2], range: [35, 40] }, // #e7c002
{ color: [255, 144, 2], range: [40, 45] }, // #ff9002
{ color: [255, 2, 1], range: [45, 50] }, // #ff0201
{ color: [214, 1, 1], range: [50, 55] }, // #d60101
{ color: [192, 1, 0], range: [55, 60] }, // #c00100
{ color: [255, 0, 240], range: [60, 65] }, // #ff00f0 (紫色,重要节点)
{ color: [150, 0, 180], range: [65, 70] }, // #9600b4
{ color: [173, 144, 240], range: [70, 75] } // #ad90f0
];
for (let i = 0; i < 256; i++) {
// 将0-255映射到0-75的数值范围
const value = (i / 255.0) * 75;
let r = 0, g = 0, b = 0;
// 找到对应的颜色区间
let found = false;
for (let j = 0; j < colorRanges.length; j++) {
const range = colorRanges[j];
if (value >= range.range[0] && value <= range.range[1]) {
// 在区间内进行线性插值
const localT = (value - range.range[0]) / (range.range[1] - range.range[0]);
if (j < colorRanges.length - 1) {
const nextRange = colorRanges[j + 1];
// 与下一个颜色进行插值
r = Math.floor(range.color[0] + localT * (nextRange.color[0] - range.color[0]));
g = Math.floor(range.color[1] + localT * (nextRange.color[1] - range.color[1]));
b = Math.floor(range.color[2] + localT * (nextRange.color[2] - range.color[2]));
} else {
// 最后一个区间,使用固定颜色
r = range.color[0];
g = range.color[1];
b = range.color[2];
}
found = true;
break;
}
}
// 如果没有找到对应区间,使用最近的颜色
if (!found) {
if (value < 5) {
const firstRange = colorRanges[0]!;
r = firstRange.color[0];
g = firstRange.color[1];
b = firstRange.color[2];
} else {
const lastRange = colorRanges[colorRanges.length - 1]!;
r = lastRange.color[0];
g = lastRange.color[1];
b = lastRange.color[2];
}
}
lut[i * 4] = r;
lut[i * 4 + 1] = g;
lut[i * 4 + 2] = b;
lut[i * 4 + 3] = 255;
}
return lut;
}
// 获取所有可用的色标类型 // 获取所有可用的色标类型
export function getAvailableColorMaps(): { value: ColorMapType; label: string }[] { export function getAvailableColorMaps(): { value: ColorMapType; label: string }[] {
return [ return [
@ -220,6 +318,7 @@ export function getAvailableColorMaps(): { value: ColorMapType; label: string }[
{ value: 'heatmap', label: '热力图色标' }, { value: 'heatmap', label: '热力图色标' },
{ value: 'viridis', label: 'Viridis色标' }, { value: 'viridis', label: 'Viridis色标' },
{ value: 'plasma', label: 'Plasma色标' }, { value: 'plasma', label: 'Plasma色标' },
{ value: 'grayscale', label: '灰度色标' } { value: 'grayscale', label: '灰度色标' },
{ value: 'meteorological', label: '气象雷达色标' }
]; ];
} }

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",