爆管分析页面,新增时间轴、工具栏,修改部分组件以满足页面功能需求
This commit is contained in:
@@ -31,7 +31,7 @@ const LayerControl: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute left-4 bottom-4 bg-white rounded-md drop-shadow-lg opacity-85 hover:opacity-100 transition-opacity max-w-xs">
|
||||
<div className="absolute left-4 bottom-4 bg-white rounded-md drop-shadow-lg z-10 opacity-85 hover:opacity-100 transition-opacity max-w-xs">
|
||||
<div className="ml-3 grid grid-cols-3">
|
||||
{layers.map((layer, index) => (
|
||||
<FormControlLabel
|
||||
|
||||
@@ -31,7 +31,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"];
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-96 max-h-[850px] flex flex-col backdrop-blur-sm opacity-95 hover:opacity-100 transition-all duration-300">
|
||||
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-96 z-10 max-h-[850px] flex flex-col backdrop-blur-sm opacity-95 hover:opacity-100 transition-all duration-300">
|
||||
{/* 头部 */}
|
||||
<div className="flex justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -56,10 +56,13 @@ interface LayerStyleState {
|
||||
legendConfig: LegendStyleConfig;
|
||||
isActive: boolean;
|
||||
}
|
||||
// 持久化存储
|
||||
const STORAGE_KEYS = {
|
||||
layerStyleStates: "styleEditor_layerStyleStates",
|
||||
};
|
||||
|
||||
// StyleEditorPanel 组件 Props 接口
|
||||
interface StyleEditorPanelProps {
|
||||
layerStyleStates: LayerStyleState[];
|
||||
setLayerStyleStates: React.Dispatch<React.SetStateAction<LayerStyleState[]>>;
|
||||
}
|
||||
|
||||
// 预设颜色方案
|
||||
const SINGLE_COLOR_PALETTES = [
|
||||
{
|
||||
@@ -108,7 +111,10 @@ const CLASSIFICATION_METHODS = [
|
||||
// { name: "自然间断", value: "jenks_optimized" },
|
||||
];
|
||||
|
||||
const StyleEditorPanel: React.FC = () => {
|
||||
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
layerStyleStates,
|
||||
setLayerStyleStates,
|
||||
}) => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
if (!data) {
|
||||
@@ -123,7 +129,6 @@ const StyleEditorPanel: React.FC = () => {
|
||||
setShowPipeText,
|
||||
setJunctionText,
|
||||
setPipeText,
|
||||
updateLegendConfigs,
|
||||
} = data;
|
||||
|
||||
const { open, close } = useNotification();
|
||||
@@ -155,20 +160,7 @@ const StyleEditorPanel: React.FC = () => {
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
});
|
||||
// 样式状态管理 - 存储多个图层的样式状态
|
||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||
() => {
|
||||
const saved = sessionStorage.getItem(STORAGE_KEYS.layerStyleStates);
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
}
|
||||
);
|
||||
// 保存layerStyleStates到sessionStorage
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem(
|
||||
STORAGE_KEYS.layerStyleStates,
|
||||
JSON.stringify(layerStyleStates)
|
||||
);
|
||||
}, [layerStyleStates]);
|
||||
|
||||
// 颜色方案选择
|
||||
const [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
|
||||
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
|
||||
@@ -735,20 +727,6 @@ const StyleEditorPanel: React.FC = () => {
|
||||
}
|
||||
}, [styleConfig.colorType]);
|
||||
|
||||
// 获取所有激活的图例配置
|
||||
useEffect(() => {
|
||||
if (!updateLegendConfigs) return;
|
||||
updateLegendConfigs(
|
||||
layerStyleStates
|
||||
.filter((state) => state.isActive && state.legendConfig.property)
|
||||
.map((state) => ({
|
||||
...state.legendConfig,
|
||||
layerName: state.layerName,
|
||||
layerId: state.layerId,
|
||||
}))
|
||||
);
|
||||
}, [layerStyleStates]);
|
||||
|
||||
const getColorSetting = () => {
|
||||
if (styleConfig.colorType === "single") {
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,12 +13,49 @@ import { Style, Stroke, Fill, Circle } from "ol/style";
|
||||
import { FeatureLike } from "ol/Feature";
|
||||
import Feature from "ol/Feature";
|
||||
import StyleEditorPanel from "./StyleEditorPanel";
|
||||
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
||||
|
||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||
|
||||
import { config } from "@/config/config";
|
||||
const backendUrl = config.backendUrl;
|
||||
|
||||
// 图层样式状态接口
|
||||
interface StyleConfig {
|
||||
property: string;
|
||||
classificationMethod: string;
|
||||
segments: number;
|
||||
minSize: number;
|
||||
maxSize: number;
|
||||
minStrokeWidth: number;
|
||||
maxStrokeWidth: number;
|
||||
fixedStrokeWidth: number;
|
||||
colorType: string;
|
||||
startColor: string;
|
||||
endColor: string;
|
||||
showLabels: boolean;
|
||||
opacity: number;
|
||||
adjustWidthByProperty: boolean;
|
||||
}
|
||||
|
||||
interface LegendStyleConfig {
|
||||
layerId: string;
|
||||
layerName: string;
|
||||
property: string;
|
||||
colors: string[];
|
||||
type: string;
|
||||
dimensions: number[];
|
||||
breaks: number[];
|
||||
}
|
||||
|
||||
interface LayerStyleState {
|
||||
layerId: string;
|
||||
layerName: string;
|
||||
styleConfig: StyleConfig;
|
||||
legendConfig: LegendStyleConfig;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
// 添加接口定义隐藏按钮的props
|
||||
interface ToolbarProps {
|
||||
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
||||
@@ -38,6 +75,79 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons }) => {
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
|
||||
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
|
||||
{
|
||||
isActive: false, // 默认不激活,不显示图例
|
||||
layerId: "junctions",
|
||||
layerName: "节点图层",
|
||||
styleConfig: {
|
||||
property: "pressure",
|
||||
classificationMethod: "pretty_breaks",
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "gradient",
|
||||
startColor: "rgba(51, 153, 204, 0.9)",
|
||||
endColor: "rgba(204, 51, 51, 0.9)",
|
||||
showLabels: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "junctions",
|
||||
layerName: "节点图层",
|
||||
property: "压力", // 暂时为空,等计算后更新
|
||||
colors: [],
|
||||
type: "point",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
isActive: false, // 默认不激活,不显示图例
|
||||
layerId: "pipes",
|
||||
layerName: "管道图层",
|
||||
styleConfig: {
|
||||
property: "flow",
|
||||
classificationMethod: "pretty_breaks",
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "gradient",
|
||||
startColor: "rgba(51, 153, 204, 0.9)",
|
||||
endColor: "rgba(204, 51, 51, 0.9)",
|
||||
showLabels: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "pipes",
|
||||
layerName: "管道图层",
|
||||
property: "流量", // 暂时为空,等计算后更新
|
||||
colors: [],
|
||||
type: "linestring",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 计算激活的图例配置
|
||||
const activeLegendConfigs = layerStyleStates
|
||||
.filter((state) => state.isActive && state.legendConfig.property)
|
||||
.map((state) => ({
|
||||
...state.legendConfig,
|
||||
layerName: state.layerName,
|
||||
layerId: state.layerId,
|
||||
}));
|
||||
|
||||
// 创建高亮图层
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
@@ -73,7 +183,9 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons }) => {
|
||||
map.removeLayer(highLightLayer);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(layerStyleStates);
|
||||
}, [layerStyleStates]);
|
||||
// 高亮要素的函数
|
||||
useEffect(() => {
|
||||
if (!highlightLayer) {
|
||||
@@ -348,7 +460,23 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons }) => {
|
||||
</div>
|
||||
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
|
||||
{showDrawPanel && map && <DrawPanel />}
|
||||
{showStyleEditor && <StyleEditorPanel />}
|
||||
{showStyleEditor && (
|
||||
<StyleEditorPanel
|
||||
layerStyleStates={layerStyleStates}
|
||||
setLayerStyleStates={setLayerStyleStates}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 图例显示 */}
|
||||
{activeLegendConfigs.length > 0 && (
|
||||
<div className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
||||
<div className="flex flex-row gap-3">
|
||||
{activeLegendConfigs.map((config, index) => (
|
||||
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ const Zoom: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute right-4 bottom-8">
|
||||
<div className="absolute right-4 bottom-8 z-10">
|
||||
<div className="w-8 h-26 flex flex-col gap-2 items-center">
|
||||
<div className="w-8 h-8 bg-gray-50 flex items-center justify-center rounded-xl drop-shadow-xl shadow-black">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user