Compare commits
3 Commits
aafc5078fa
...
9646e5d0e1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9646e5d0e1 | |||
| 520efbbb0d | |||
| 06ba5d7ab1 |
@ -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<Map | null>
|
||||
layers: React.RefObject<any[]>
|
||||
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<MapContextType | undefined>(undefined)
|
||||
|
||||
@ -42,7 +43,7 @@ export function MapProvider({ children }: MapProviderProps) {
|
||||
});
|
||||
|
||||
const layersRef = useRef<any[]>([])
|
||||
|
||||
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(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,
|
||||
|
||||
41
app/page.tsx
41
app/page.tsx
@ -18,20 +18,8 @@ import { ThemeToggle } from '@/components/theme-toggle';
|
||||
// import { Timeline } from '@/app/timeline';
|
||||
import { Timeline } from '@/app/tl';
|
||||
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() {
|
||||
|
||||
@ -39,16 +27,6 @@ export default function Page() {
|
||||
const now = new Date();
|
||||
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 { setTime } = useTimeline()
|
||||
const { imgBitmap, fetchRadarTile } = useRadarTile({})
|
||||
|
||||
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
console.log(data.statusUpdates)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
@ -62,22 +40,7 @@ export default function Page() {
|
||||
</div>
|
||||
</header>
|
||||
<div className="relative h-full w-full flex flex-col">
|
||||
<MapComponent imgBitmap={imgBitmap} />
|
||||
{/* <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)
|
||||
}}
|
||||
/> */}
|
||||
<MapComponent />
|
||||
<Timeline />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
|
||||
@ -133,7 +133,6 @@ function SelectDemo() {
|
||||
export const Timeline: React.FC<Props> = ({
|
||||
startDate,
|
||||
endDate,
|
||||
currentDate,
|
||||
onDateChange,
|
||||
onPlay,
|
||||
onPause,
|
||||
@ -148,19 +147,12 @@ export const Timeline: React.FC<Props> = ({
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const ticksCanvasRef = useRef<HTMLCanvasElement>(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<Status>({
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
current_uniforms.current.currentTimestamp = currentDate.getTime();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 鼠标滚轮缩放和平移
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
72
app/tl.tsx
72
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<Props> = React.memo(({
|
||||
timelineConfig,
|
||||
...props
|
||||
}) => {
|
||||
const { isPlaying, togglePlay } = useTimeline({})
|
||||
const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({})
|
||||
const [lock, setLock] = useState(false)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const timelineEngineRef = useRef<TimelineEngine | null>(null);
|
||||
@ -77,6 +92,21 @@ export const Timeline: React.FC<Props> = 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<Props> = 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<Props> = 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<Props> = 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<Props> = React.memo(({
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="size-5"
|
||||
onClick={() => togglePlay()}
|
||||
onClick={() => {
|
||||
togglePlay()
|
||||
setLock(true)
|
||||
}}
|
||||
title={isPlaying ? "暂停" : "播放"}
|
||||
>
|
||||
{isPlaying ? <Pause size={10} /> : <Play size={10} />}
|
||||
@ -260,6 +312,16 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="size-5"
|
||||
onClick={() => setLock(!lock)}
|
||||
title="锁定时间"
|
||||
>
|
||||
{lock ? <LockIcon size={10} /> : <UnlockIcon size={10} />}
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,8 @@ import { useMapLocation } from '@/hooks/use-map-location'
|
||||
import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh'
|
||||
import { createColorMap, ColorMapType, } from '@/lib/color-maps'
|
||||
import { Colorbar } from './colorbar'
|
||||
import { useRadarTile } from '@/hooks/use-radartile'
|
||||
import { format, formatInTimeZone } from 'date-fns-tz'
|
||||
|
||||
interface MapComponentProps {
|
||||
style?: string
|
||||
@ -24,13 +26,14 @@ export function MapComponent({
|
||||
// center = [103.851959, 1.290270],
|
||||
// zoom = 11
|
||||
imgBitmap: propImgBitmap,
|
||||
colorMapType = 'heatmap',
|
||||
colorMapType = 'meteorological',
|
||||
onColorMapChange
|
||||
}: MapComponentProps) {
|
||||
|
||||
const { fetchRadarTile, imgBitmap } = useRadarTile();
|
||||
const mapContainer = useRef<HTMLDivElement>(null)
|
||||
const { setMap } = useMap()
|
||||
const { setMap, mapRef, currentDatetime, isMapReady } = useMap()
|
||||
const { location } = useMapLocation()
|
||||
const imgBitmap = propImgBitmap
|
||||
const texRef = useRef<WebGLTexture | null>(null)
|
||||
const lutTexRef = useRef<WebGLTexture | null>(null)
|
||||
const glRef = useRef<WebGL2RenderingContext | null>(null)
|
||||
@ -38,6 +41,13 @@ export function MapComponent({
|
||||
const [isReady, setIsReady] = useState<boolean>(false)
|
||||
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(() => {
|
||||
if (!mapContainer.current) return
|
||||
|
||||
@ -101,8 +111,9 @@ export function MapComponent({
|
||||
// 对于灰度图,RGB通道通常相同,取红色通道作为灰度值
|
||||
float value = texColor.r * 3.4;
|
||||
|
||||
if (value == 0.0) {
|
||||
discard;
|
||||
if (value < 0.07) {
|
||||
fragColor= vec4(1.0,1.0,1.0,0.2);
|
||||
return;
|
||||
}
|
||||
|
||||
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
|
||||
@ -111,8 +122,9 @@ export function MapComponent({
|
||||
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
|
||||
// 添加一些透明度,使低值区域更透明
|
||||
// float alpha = smoothstep(0.0, 0.1, value);
|
||||
float alpha = 1.0;
|
||||
float alpha = 0.7;
|
||||
fragColor = vec4(lutColor.rgb, alpha);
|
||||
// fragColor = vec4(1.0,1.0,1.0,0.2);
|
||||
}`
|
||||
|
||||
console.log(vertexSource, fragmentSource)
|
||||
@ -166,8 +178,8 @@ export function MapComponent({
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
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_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.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
// Redraw the map
|
||||
mapRef.current?.triggerRepaint()
|
||||
|
||||
}
|
||||
|
||||
}, [imgBitmap, isReady])
|
||||
@ -556,8 +572,8 @@ function createLutTexture(gl: WebGL2RenderingContext, colorMapType: ColorMapType
|
||||
lut
|
||||
)
|
||||
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
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_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -24,7 +24,7 @@ const LOCATIONS = {
|
||||
export type LocationKey = keyof typeof LOCATIONS
|
||||
|
||||
export function useMapLocation() {
|
||||
const [currentLocation, setCurrentLocation] = useState<LocationKey>('usa')
|
||||
const [currentLocation, setCurrentLocation] = useState<LocationKey>('china')
|
||||
const { flyTo, isMapReady } = useMap()
|
||||
|
||||
const flyToLocation = useCallback((location: LocationKey) => {
|
||||
|
||||
@ -1,28 +1,11 @@
|
||||
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';
|
||||
|
||||
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 = {
|
||||
slow: 2000,
|
||||
@ -30,11 +13,35 @@ export function useTimeline({
|
||||
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) => {
|
||||
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,
|
||||
|
||||
@ -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 {
|
||||
@ -207,11 +207,109 @@ export function createColorMap(type: ColorMapType): Uint8Array {
|
||||
return createPlasmaColorMap();
|
||||
case 'grayscale':
|
||||
return createGrayscaleColorMap();
|
||||
case 'meteorological':
|
||||
return createMeteorologicalColorMap();
|
||||
default:
|
||||
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 }[] {
|
||||
return [
|
||||
@ -220,6 +318,7 @@ export function getAvailableColorMaps(): { value: ColorMapType; label: string }[
|
||||
{ value: 'heatmap', label: '热力图色标' },
|
||||
{ value: 'viridis', label: 'Viridis色标' },
|
||||
{ value: 'plasma', label: 'Plasma色标' },
|
||||
{ value: 'grayscale', label: '灰度色标' }
|
||||
{ value: 'grayscale', label: '灰度色标' },
|
||||
{ value: 'meteorological', label: '气象雷达色标' }
|
||||
];
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
33
package-lock.json
generated
33
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user