为时间轴新增 draggable 特性

This commit is contained in:
JIANG
2025-11-26 11:44:54 +08:00
parent dc7271e3da
commit 70ac7ba177
4 changed files with 280 additions and 266 deletions

29
package-lock.json generated
View File

@@ -38,6 +38,7 @@
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-draggable": "^4.5.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
@@ -3752,20 +3753,6 @@
} }
} }
}, },
"node_modules/@mui/base/node_modules/@mui/types": {
"version": "7.2.24",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
"integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/core-downloads-tracker": { "node_modules/@mui/core-downloads-tracker": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz",
@@ -16388,6 +16375,20 @@
"react": "^19.1.1" "react": "^19.1.1"
} }
}, },
"node_modules/react-draggable": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.63.0", "version": "7.63.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",

View File

@@ -43,6 +43,7 @@
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-draggable": "^4.5.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"

View File

@@ -2,6 +2,7 @@
import React, { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import Draggable from "react-draggable";
import { import {
Box, Box,
@@ -84,6 +85,9 @@ const Timeline: React.FC<TimelineProps> = ({
setSelectedDate(schemeDate); setSelectedDate(schemeDate);
} }
}, [schemeDate]); }, [schemeDate]);
// 新增:用于 Draggable 的 nodeRef
const draggableRef = useRef<HTMLDivElement>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timelineRef = useRef<HTMLDivElement>(null); const timelineRef = useRef<HTMLDivElement>(null);
// 添加缓存引用 // 添加缓存引用
@@ -527,256 +531,263 @@ const Timeline: React.FC<TimelineProps> = ({
}; };
return ( return (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[920px] opacity-90 hover:opacity-100 transition-opacity duration-300"> <Draggable nodeRef={draggableRef}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn"> <div
<Paper ref={draggableRef}
elevation={3} className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[920px] opacity-90 hover:opacity-100 transition-opacity duration-300"
sx={{ >
position: "absolute", <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
bottom: 0, <Paper
left: 0, elevation={3}
right: 0, sx={{
zIndex: 1000, position: "absolute",
p: 2, bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.95)", left: 0,
backdropFilter: "blur(10px)", right: 0,
}} zIndex: 1000,
> p: 2,
<Box sx={{ width: "100%" }}> backgroundColor: "rgba(255, 255, 255, 0.95)",
{/* 控制按钮栏 */} backdropFilter: "blur(10px)",
<Stack }}
direction="row" >
spacing={2} <Box sx={{ width: "100%" }}>
alignItems="center" {/* 控制按钮栏 */}
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }} <Stack
> direction="row"
<Tooltip title="后退一天"> spacing={2}
<IconButton alignItems="center"
color="primary" sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
onClick={handleDayStepBackward}
size="small"
disabled={disableDateSelection}
>
<FiSkipBack />
</IconButton>
</Tooltip>
{/* 日期选择器 */}
<DatePicker
label="模拟数据日期选择"
value={dayjs(selectedDate)}
onChange={(value) => value && handleDateChange(value.toDate())}
enableAccessibleFieldDOMStructure={false}
format="YYYY-MM-DD"
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
maxDate={dayjs(new Date())} // 禁止选取未来的日期
disabled={disableDateSelection}
/>
<Tooltip title="前进一天">
<IconButton
color="primary"
onClick={handleDayStepForward}
size="small"
disabled={
disableDateSelection ||
selectedDate.toDateString() === new Date().toDateString()
}
>
<FiSkipForward />
</IconButton>
</Tooltip>
{/* 播放控制按钮 */}
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
{/* 播放间隔选择 */}
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={playInterval}
label="播放间隔"
onChange={handleIntervalChange}
>
{intervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<Tooltip title="后退一步">
<IconButton
color="primary"
onClick={handleStepBackward}
size="small"
>
<TbRewindBackward15 />
</IconButton>
</Tooltip>
<Tooltip title={isPlaying ? "暂停" : "播放"}>
<IconButton
color="primary"
onClick={isPlaying ? handlePause : handlePlay}
size="small"
>
{isPlaying ? <Pause /> : <PlayArrow />}
</IconButton>
</Tooltip>
<Tooltip title="前进一步">
<IconButton
color="primary"
onClick={handleStepForward}
size="small"
>
<TbRewindForward15 />
</IconButton>
</Tooltip>
<Tooltip title="停止">
<IconButton
color="secondary"
onClick={handleStop}
size="small"
>
<Stop />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
{/* 强制计算时间段 */}
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={calculatedInterval}
label="强制计算时间段"
onChange={handleCalculatedIntervalChange}
>
{calculatedIntervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
{/* 功能按钮 */}
<Tooltip title="强制计算">
<Button
variant="outlined"
size="small"
startIcon={<Refresh />}
onClick={handleForceCalculate}
disabled={isCalculating}
>
</Button>
</Tooltip>
</Box>
{/* 当前时间显示 */}
<Typography
variant="h6"
sx={{
ml: "auto",
fontWeight: "bold",
color: "primary.main",
}}
> >
{formatTime(currentTime)} <Tooltip title="后退一天">
</Typography> <IconButton
</Stack> color="primary"
onClick={handleDayStepBackward}
size="small"
disabled={disableDateSelection}
>
<FiSkipBack />
</IconButton>
</Tooltip>
{/* 日期选择器 */}
<DatePicker
label="模拟数据日期选择"
value={dayjs(selectedDate)}
onChange={(value) =>
value && handleDateChange(value.toDate())
}
enableAccessibleFieldDOMStructure={false}
format="YYYY-MM-DD"
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
maxDate={dayjs(new Date())} // 禁止选取未来的日期
disabled={disableDateSelection}
/>
<Tooltip title="前进一天">
<IconButton
color="primary"
onClick={handleDayStepForward}
size="small"
disabled={
disableDateSelection ||
selectedDate.toDateString() === new Date().toDateString()
}
>
<FiSkipForward />
</IconButton>
</Tooltip>
{/* 播放控制按钮 */}
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
{/* 播放间隔选择 */}
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={playInterval}
label="播放间隔"
onChange={handleIntervalChange}
>
{intervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<Box ref={timelineRef} sx={{ px: 2, position: "relative" }}> <Tooltip title="后退一步">
<Slider <IconButton
value={currentTime} color="primary"
min={0} onClick={handleStepBackward}
max={1440} // 24:00 = 1440分钟 size="small"
step={15} // 每15分钟一个步进 >
marks={timeMarks.filter((_, index) => index % 12 === 0)} // 每小时显示一个标记 <TbRewindBackward15 />
onChange={handleSliderChange} </IconButton>
valueLabelDisplay="auto" </Tooltip>
valueLabelFormat={formatTime}
sx={{ <Tooltip title={isPlaying ? "暂停" : "播放"}>
zIndex: 10, <IconButton
height: 8, color="primary"
"& .MuiSlider-track": { onClick={isPlaying ? handlePause : handlePlay}
backgroundColor: "primary.main", size="small"
height: 6, >
display: timeRange ? "none" : "block", {isPlaying ? <Pause /> : <PlayArrow />}
}, </IconButton>
"& .MuiSlider-rail": { </Tooltip>
backgroundColor: "grey.300",
height: 6, <Tooltip title="前进一步">
}, <IconButton
"& .MuiSlider-thumb": { color="primary"
height: 20, onClick={handleStepForward}
width: 20, size="small"
backgroundColor: "primary.main", >
border: "2px solid #fff", <TbRewindForward15 />
boxShadow: "0 2px 8px rgba(0,0,0,0.2)", </IconButton>
"&:hover": { </Tooltip>
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
<Tooltip title="停止">
<IconButton
color="secondary"
onClick={handleStop}
size="small"
>
<Stop />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
{/* 强制计算时间段 */}
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={calculatedInterval}
label="强制计算时间段"
onChange={handleCalculatedIntervalChange}
>
{calculatedIntervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
{/* 功能按钮 */}
<Tooltip title="强制计算">
<Button
variant="outlined"
size="small"
startIcon={<Refresh />}
onClick={handleForceCalculate}
disabled={isCalculating}
>
</Button>
</Tooltip>
</Box>
{/* 当前时间显示 */}
<Typography
variant="h6"
sx={{
ml: "auto",
fontWeight: "bold",
color: "primary.main",
}}
>
{formatTime(currentTime)}
</Typography>
</Stack>
<Box ref={timelineRef} sx={{ px: 2, position: "relative" }}>
<Slider
value={currentTime}
min={0}
max={1440} // 24:00 = 1440分钟
step={15} // 每15分钟一个步进
marks={timeMarks.filter((_, index) => index % 12 === 0)} // 每小时显示一个标记
onChange={handleSliderChange}
valueLabelDisplay="auto"
valueLabelFormat={formatTime}
sx={{
zIndex: 10,
height: 8,
"& .MuiSlider-track": {
backgroundColor: "primary.main",
height: 6,
display: timeRange ? "none" : "block",
}, },
}, "& .MuiSlider-rail": {
"& .MuiSlider-mark": { backgroundColor: "grey.300",
backgroundColor: "grey.400", height: 6,
height: 4, },
width: 2, "& .MuiSlider-thumb": {
}, height: 20,
"& .MuiSlider-markActive": { width: 20,
backgroundColor: "primary.main", backgroundColor: "primary.main",
}, border: "2px solid #fff",
"& .MuiSlider-markLabel": { boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
fontSize: "0.75rem", "&:hover": {
color: "grey.600", boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
}, },
}} },
/> "& .MuiSlider-mark": {
{/* 禁用区域遮罩 */} backgroundColor: "grey.400",
{timeRange && ( height: 4,
<> width: 2,
{/* 左侧禁用区域 */} },
{minTime > 0 && ( "& .MuiSlider-markActive": {
<Box backgroundColor: "primary.main",
sx={{ },
position: "absolute", "& .MuiSlider-markLabel": {
left: "14px", fontSize: "0.75rem",
top: "30%", color: "grey.600",
transform: "translateY(-50%)", },
width: `${(minTime / 1440) * 856 + 2}px`, }}
height: "20px", />
backgroundColor: "rgba(189, 189, 189, 0.4)", {/* 禁用区域遮罩 */}
pointerEvents: "none", {timeRange && (
backdropFilter: "blur(1px)", <>
borderRadius: "2.5px", {/* 左侧禁用区域 */}
rounded: "true", {minTime > 0 && (
}} <Box
/> sx={{
)} position: "absolute",
{/* 右侧禁用区域 */} left: "14px",
{maxTime < 1440 && ( top: "30%",
<Box transform: "translateY(-50%)",
sx={{ width: `${(minTime / 1440) * 856 + 2}px`,
position: "absolute", height: "20px",
left: `${16 + (maxTime / 1440) * 856}px`, backgroundColor: "rgba(189, 189, 189, 0.4)",
top: "30%", pointerEvents: "none",
transform: "translateY(-50%)", backdropFilter: "blur(1px)",
width: `${((1440 - maxTime) / 1440) * 856}px`, borderRadius: "2.5px",
height: "20px", rounded: "true",
backgroundColor: "rgba(189, 189, 189, 0.4)", }}
pointerEvents: "none", />
backdropFilter: "blur(1px)", )}
borderRadius: "2.5px", {/* 右侧禁用区域 */}
}} {maxTime < 1440 && (
/> <Box
)} sx={{
</> position: "absolute",
)} left: `${16 + (maxTime / 1440) * 856}px`,
top: "30%",
transform: "translateY(-50%)",
width: `${((1440 - maxTime) / 1440) * 856}px`,
height: "20px",
backgroundColor: "rgba(189, 189, 189, 0.4)",
pointerEvents: "none",
backdropFilter: "blur(1px)",
borderRadius: "2.5px",
}}
/>
)}
</>
)}
</Box>
</Box> </Box>
</Box> </Paper>
</Paper> </LocalizationProvider>
</LocalizationProvider> </div>
</div> </Draggable>
); );
}; };

View File

@@ -748,13 +748,14 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
id: "junctionContourLayer", id: "junctionContourLayer",
name: "等值线", name: "等值线",
data: junctionData, data: junctionData,
cellSize: 400, aggregation: "MEAN",
cellSize: 200,
contours: [ contours: [
{ threshold: [0, 10], color: [255, 0, 0] }, { threshold: [0, 16], color: [255, 0, 0] },
{ threshold: [10, 20], color: [255, 127, 0] }, { threshold: [16, 20], color: [255, 127, 0] },
{ threshold: [20, 30], color: [255, 215, 0] }, { threshold: [20, 22], color: [255, 215, 0] },
{ threshold: [30, 40], color: [199, 224, 0] }, { threshold: [22, 24], color: [199, 224, 0] },
{ threshold: [40, 9999], color: [142, 68, 173] }, { threshold: [24, 26], color: [76, 175, 80] },
], ],
getPosition: (d) => d.position, getPosition: (d) => d.position,
getWeight: (d: any) => getWeight: (d: any) =>