爆管分析页面,新增时间轴、工具栏,修改部分组件以满足页面功能需求

This commit is contained in:
JIANG
2025-10-24 16:28:57 +08:00
parent ad893ac19d
commit 7a615e08fc
14 changed files with 989 additions and 667 deletions

View File

@@ -28,7 +28,20 @@ import { useData } from "../MapComponent";
import { config } from "@/config/config";
import { useMap } from "../MapComponent";
const backendUrl = config.backendUrl;
const Timeline: React.FC = () => {
interface TimelineProps {
schemeDate?: Date;
timeRange?: { start: Date; end: Date };
disableDateSelection?: boolean;
schemeName?: string;
}
const Timeline: React.FC<TimelineProps> = ({
schemeDate,
timeRange,
disableDateSelection = false,
schemeName = "",
}) => {
const data = useData();
if (!data) {
return <div>Loading...</div>; // 或其他占位符
@@ -55,8 +68,20 @@ const Timeline: React.FC = () => {
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟
const [calculatedInterval, setCalculatedInterval] = useState<number>(15); // 分钟
// 计算时间轴范围
const minTime = timeRange
? timeRange.start.getHours() * 60 + timeRange.start.getMinutes()
: 0;
const maxTime = timeRange
? timeRange.end.getHours() * 60 + timeRange.end.getMinutes()
: 1440;
useEffect(() => {
if (schemeDate) {
setSelectedDate(schemeDate);
}
}, [schemeDate]);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timelineRef = useRef<HTMLDivElement>(null);
// 添加缓存引用
@@ -82,9 +107,13 @@ const Timeline: React.FC = () => {
if (nodeCacheRef.current.has(nodeCacheKey)) {
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
} else {
nodePromise = fetch(
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
);
disableDateSelection && schemeName
? (nodePromise = fetch(
`${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}`
))
: (nodePromise = fetch(
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
));
requests.push(nodePromise);
}
}
@@ -95,9 +124,13 @@ const Timeline: React.FC = () => {
if (linkCacheRef.current.has(linkCacheKey)) {
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
} else {
linkPromise = fetch(
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
);
disableDateSelection && schemeName
? (linkPromise = fetch(
`${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}`
))
: (linkPromise = fetch(
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
));
requests.push(linkPromise);
}
}
@@ -192,6 +225,10 @@ const Timeline: React.FC = () => {
const handleSliderChange = useCallback(
(event: Event, newValue: number | number[]) => {
const value = Array.isArray(newValue) ? newValue[0] : newValue;
// 如果有时间范围限制,只允许在范围内拖动
if (timeRange && (value < minTime || value > maxTime)) {
return;
}
// 防抖设置currentTime避免频繁触发数据获取
if (debounceRef.current) {
clearTimeout(debounceRef.current);
@@ -200,7 +237,7 @@ const Timeline: React.FC = () => {
setCurrentTime(value);
}, 300); // 300ms 防抖延迟
},
[]
[timeRange, minTime, maxTime]
);
// 播放控制
@@ -217,7 +254,12 @@ const Timeline: React.FC = () => {
intervalRef.current = setInterval(() => {
setCurrentTime((prev) => {
const next = prev >= 1440 ? 0 : prev + 15; // 到达24:00后回到00:00
let next = prev + 15;
if (timeRange) {
if (next > maxTime) next = minTime;
} else {
if (next >= 1440) next = 0;
}
return next;
});
}, playInterval);
@@ -261,17 +303,27 @@ const Timeline: React.FC = () => {
}, []);
const handleStepBackward = useCallback(() => {
setCurrentTime((prev) => {
const next = prev <= 0 ? 1440 : prev - 15;
let next = prev - 15;
if (timeRange) {
if (next < minTime) next = maxTime;
} else {
if (next <= 0) next = 1440;
}
return next;
});
}, []);
}, [timeRange, minTime, maxTime]);
const handleStepForward = useCallback(() => {
setCurrentTime((prev) => {
const next = prev >= 1440 ? 0 : prev + 15;
let next = prev + 15;
if (timeRange) {
if (next > maxTime) next = minTime;
} else {
if (next >= 1440) next = 0;
}
return next;
});
}, []);
}, [timeRange, minTime, maxTime]);
// 日期选择处理
const handleDateChange = useCallback((newDate: Date | null) => {
@@ -291,7 +343,12 @@ const Timeline: React.FC = () => {
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setCurrentTime((prev) => {
const next = prev >= 1440 ? 0 : prev + 15;
let next = prev + 15;
if (timeRange) {
if (next > maxTime) next = minTime;
} else {
if (next >= 1440) next = 0;
}
return next;
});
}, newInterval);
@@ -333,8 +390,8 @@ const Timeline: React.FC = () => {
const currentTime = new Date();
const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
// 找到最近的前15分钟刻度
const roundedMinutes = Math.floor(minutes / 15) * 15;
setCurrentTime(roundedMinutes); // 组件卸载时重置时间
// const roundedMinutes = Math.floor(minutes / 15) * 15;
setCurrentTime(minutes); // 组件卸载时重置时间
return () => {
if (intervalRef.current) {
@@ -345,6 +402,13 @@ const Timeline: React.FC = () => {
}
};
}, []);
// 当 timeRange 改变时,设置 currentTime 到 minTime
useEffect(() => {
if (timeRange) {
setCurrentTime(minTime);
}
}, [timeRange, minTime]);
// 获取地图实例
const map = useMap();
// 这里防止地图缩放时,瓦片重新加载引起的属性更新出错
@@ -364,211 +428,260 @@ const Timeline: React.FC = () => {
}, [map, handlePause]);
return (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={zhCN}>
<Paper
elevation={3}
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 1000,
p: 2,
backgroundColor: "rgba(255, 255, 255, 0.95)",
backdropFilter: "blur(10px)",
}}
>
<Box sx={{ width: "100%" }}>
{/* 控制按钮栏 */}
<Stack
direction="row"
spacing={2}
alignItems="center"
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
>
<Tooltip title="后退一天">
<IconButton
color="primary"
onClick={handleDayStepBackward}
size="small"
>
<FiSkipBack />
</IconButton>
</Tooltip>
{/* 日期选择器 */}
<DatePicker
label="模拟数据日期选择"
value={selectedDate}
onChange={(newValue) =>
handleDateChange(
newValue && "toDate" in newValue
? newValue.toDate()
: (newValue as Date | null)
)
}
enableAccessibleFieldDOMStructure={false}
format="yyyy-MM-dd"
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
maxDate={new Date()} // 禁止选取未来的日期
/>
<Tooltip title="前进一天">
<IconButton
color="primary"
onClick={handleDayStepForward}
size="small"
disabled={
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={onRefresh}
>
</Button>
</Tooltip>
</Box>
{/* 当前时间显示 */}
<Typography
variant="h6"
sx={{
ml: "auto",
fontWeight: "bold",
color: "primary.main",
}}
<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">
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={zhCN}>
<Paper
elevation={3}
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 1000,
p: 2,
backgroundColor: "rgba(255, 255, 255, 0.95)",
backdropFilter: "blur(10px)",
}}
>
<Box sx={{ width: "100%" }}>
{/* 控制按钮栏 */}
<Stack
direction="row"
spacing={2}
alignItems="center"
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
>
{formatTime(currentTime)}
</Typography>
</Stack>
<Tooltip title="后退一天">
<IconButton
color="primary"
onClick={handleDayStepBackward}
size="small"
disabled={disableDateSelection}
>
<FiSkipBack />
</IconButton>
</Tooltip>
{/* 日期选择器 */}
<DatePicker
label="模拟数据日期选择"
value={selectedDate}
onChange={(newValue) =>
handleDateChange(
newValue && "toDate" in newValue
? newValue.toDate()
: (newValue as Date | null)
)
}
enableAccessibleFieldDOMStructure={false}
format="yyyy-MM-dd"
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
maxDate={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 }}>
<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={{
height: 8,
"& .MuiSlider-track": {
backgroundColor: "primary.main",
height: 6,
},
"& .MuiSlider-rail": {
backgroundColor: "grey.300",
height: 6,
},
"& .MuiSlider-thumb": {
height: 20,
width: 20,
backgroundColor: "primary.main",
border: "2px solid #fff",
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
"&:hover": {
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
<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={onRefresh}
>
</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,
},
},
"& .MuiSlider-mark": {
backgroundColor: "grey.400",
height: 4,
width: 2,
},
"& .MuiSlider-markActive": {
backgroundColor: "primary.main",
},
"& .MuiSlider-markLabel": {
fontSize: "0.75rem",
color: "grey.600",
},
}}
/>
"& .MuiSlider-rail": {
backgroundColor: "grey.300",
height: 6,
},
"& .MuiSlider-thumb": {
height: 20,
width: 20,
backgroundColor: "primary.main",
border: "2px solid #fff",
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
"&:hover": {
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
},
},
"& .MuiSlider-mark": {
backgroundColor: "grey.400",
height: 4,
width: 2,
},
"& .MuiSlider-markActive": {
backgroundColor: "primary.main",
},
"& .MuiSlider-markLabel": {
fontSize: "0.75rem",
color: "grey.600",
},
}}
/>
{/* 禁用区域遮罩 */}
{timeRange && (
<>
{/* 左侧禁用区域 */}
{minTime > 0 && (
<Box
sx={{
position: "absolute",
left: "14px",
top: "30%",
transform: "translateY(-50%)",
width: `${(minTime / 1440) * 856 + 2}px`,
height: "20px",
backgroundColor: "rgba(189, 189, 189, 0.4)",
pointerEvents: "none",
backdropFilter: "blur(1px)",
borderRadius: "2.5px",
rounded: "true",
}}
/>
)}
{/* 右侧禁用区域 */}
{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>
</Paper>
</LocalizationProvider>
</Paper>
</LocalizationProvider>
</div>
);
};