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

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

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761204792054" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12884" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M64 416L160 512H256l64 64 96 32 64-32 96 32 96-32 32-64h160l32-32L960 384v384h-96v-64h-704v64H64V416z" fill="#2D8DFE" opacity=".1" p-id="12885"></path><path d="M960 800h-96L832 768v-32H192v32l-32 32H64L32 768V384l32-32h96L192 384v32h64l25.6 6.4 57.6 57.6 76.8 32 51.2-25.6h25.6L576 512l44.8-19.2 25.6-57.6 25.6-19.2H832V384l32-32H960l32 32v384l-32 32z m-64-64h32v-320H896V448l-32 32h-172.8l-25.6 44.8-12.8 12.8-64 32-25.6 6.4-83.2-32-51.2 25.6-25.6 6.4-96-32-12.8-6.4-51.2-57.6H160L128 448v-32h-32v320H128V704l32-32h704l32 32v32z" fill="#2D8DFE" p-id="12886"></path><path d="M736 320l-64-32-32 128L736 320zM544 256L448 288 544 384V256zM384 352L320 384l128 64-64-96z" fill="#2D8DFE" p-id="12887"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -8,7 +8,7 @@ export default function Home() {
return ( return (
<div className="relative w-full h-full overflow-hidden"> <div className="relative w-full h-full overflow-hidden">
<MapComponent> <MapComponent>
{/* <MapToolbar hiddenButtons={["style"]} /> */} <MapToolbar />
<BurstPipeAnalysisPanel /> <BurstPipeAnalysisPanel />
</MapComponent> </MapComponent>
</div> </div>

View File

@@ -78,9 +78,7 @@ export default function Home() {
<div className="relative w-full h-full overflow-hidden"> <div className="relative w-full h-full overflow-hidden">
<MapComponent> <MapComponent>
<MapToolbar /> <MapToolbar />
<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"> <Timeline />
<Timeline />
</div>
</MapComponent> </MapComponent>
<SCADADeviceList <SCADADeviceList
devices={devices} devices={devices}

View File

@@ -31,7 +31,7 @@ const LayerControl: React.FC = () => {
}; };
return ( 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"> <div className="ml-3 grid grid-cols-3">
{layers.map((layer, index) => ( {layers.map((layer, index) => (
<FormControlLabel <FormControlLabel

View File

@@ -31,7 +31,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"]; const isImportantKeys = ["ID", "类型", "Name", "面积", "长度"];
return ( 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 justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -56,10 +56,13 @@ interface LayerStyleState {
legendConfig: LegendStyleConfig; legendConfig: LegendStyleConfig;
isActive: boolean; isActive: boolean;
} }
// 持久化存储
const STORAGE_KEYS = { // StyleEditorPanel 组件 Props 接口
layerStyleStates: "styleEditor_layerStyleStates", interface StyleEditorPanelProps {
}; layerStyleStates: LayerStyleState[];
setLayerStyleStates: React.Dispatch<React.SetStateAction<LayerStyleState[]>>;
}
// 预设颜色方案 // 预设颜色方案
const SINGLE_COLOR_PALETTES = [ const SINGLE_COLOR_PALETTES = [
{ {
@@ -108,7 +111,10 @@ const CLASSIFICATION_METHODS = [
// { name: "自然间断", value: "jenks_optimized" }, // { name: "自然间断", value: "jenks_optimized" },
]; ];
const StyleEditorPanel: React.FC = () => { const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
layerStyleStates,
setLayerStyleStates,
}) => {
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
if (!data) { if (!data) {
@@ -123,7 +129,6 @@ const StyleEditorPanel: React.FC = () => {
setShowPipeText, setShowPipeText,
setJunctionText, setJunctionText,
setPipeText, setPipeText,
updateLegendConfigs,
} = data; } = data;
const { open, close } = useNotification(); const { open, close } = useNotification();
@@ -155,20 +160,7 @@ const StyleEditorPanel: React.FC = () => {
opacity: 0.9, opacity: 0.9,
adjustWidthByProperty: true, 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 [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0); const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
@@ -735,20 +727,6 @@ const StyleEditorPanel: React.FC = () => {
} }
}, [styleConfig.colorType]); }, [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 = () => { const getColorSetting = () => {
if (styleConfig.colorType === "single") { if (styleConfig.colorType === "single") {
return ( return (

View File

@@ -28,7 +28,20 @@ import { useData } from "../MapComponent";
import { config } from "@/config/config"; import { config } from "@/config/config";
import { useMap } from "../MapComponent"; import { useMap } from "../MapComponent";
const backendUrl = config.backendUrl; 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(); const data = useData();
if (!data) { if (!data) {
return <div>Loading...</div>; // 或其他占位符 return <div>Loading...</div>; // 或其他占位符
@@ -55,8 +68,20 @@ const Timeline: React.FC = () => {
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒 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 intervalRef = useRef<NodeJS.Timeout | null>(null);
const timelineRef = useRef<HTMLDivElement>(null); const timelineRef = useRef<HTMLDivElement>(null);
// 添加缓存引用 // 添加缓存引用
@@ -82,9 +107,13 @@ const Timeline: React.FC = () => {
if (nodeCacheRef.current.has(nodeCacheKey)) { if (nodeCacheRef.current.has(nodeCacheKey)) {
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
} else { } else {
nodePromise = fetch( disableDateSelection && schemeName
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` ? (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); requests.push(nodePromise);
} }
} }
@@ -95,9 +124,13 @@ const Timeline: React.FC = () => {
if (linkCacheRef.current.has(linkCacheKey)) { if (linkCacheRef.current.has(linkCacheKey)) {
linkRecords = linkCacheRef.current.get(linkCacheKey)!; linkRecords = linkCacheRef.current.get(linkCacheKey)!;
} else { } else {
linkPromise = fetch( disableDateSelection && schemeName
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` ? (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); requests.push(linkPromise);
} }
} }
@@ -192,6 +225,10 @@ const Timeline: React.FC = () => {
const handleSliderChange = useCallback( const handleSliderChange = useCallback(
(event: Event, newValue: number | number[]) => { (event: Event, newValue: number | number[]) => {
const value = Array.isArray(newValue) ? newValue[0] : newValue; const value = Array.isArray(newValue) ? newValue[0] : newValue;
// 如果有时间范围限制,只允许在范围内拖动
if (timeRange && (value < minTime || value > maxTime)) {
return;
}
// 防抖设置currentTime避免频繁触发数据获取 // 防抖设置currentTime避免频繁触发数据获取
if (debounceRef.current) { if (debounceRef.current) {
clearTimeout(debounceRef.current); clearTimeout(debounceRef.current);
@@ -200,7 +237,7 @@ const Timeline: React.FC = () => {
setCurrentTime(value); setCurrentTime(value);
}, 300); // 300ms 防抖延迟 }, 300); // 300ms 防抖延迟
}, },
[] [timeRange, minTime, maxTime]
); );
// 播放控制 // 播放控制
@@ -217,7 +254,12 @@ const Timeline: React.FC = () => {
intervalRef.current = setInterval(() => { intervalRef.current = setInterval(() => {
setCurrentTime((prev) => { 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; return next;
}); });
}, playInterval); }, playInterval);
@@ -261,17 +303,27 @@ const Timeline: React.FC = () => {
}, []); }, []);
const handleStepBackward = useCallback(() => { const handleStepBackward = useCallback(() => {
setCurrentTime((prev) => { 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; return next;
}); });
}, []); }, [timeRange, minTime, maxTime]);
const handleStepForward = useCallback(() => { const handleStepForward = useCallback(() => {
setCurrentTime((prev) => { 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; return next;
}); });
}, []); }, [timeRange, minTime, maxTime]);
// 日期选择处理 // 日期选择处理
const handleDateChange = useCallback((newDate: Date | null) => { const handleDateChange = useCallback((newDate: Date | null) => {
@@ -291,7 +343,12 @@ const Timeline: React.FC = () => {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => { intervalRef.current = setInterval(() => {
setCurrentTime((prev) => { 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; return next;
}); });
}, newInterval); }, newInterval);
@@ -333,8 +390,8 @@ const Timeline: React.FC = () => {
const currentTime = new Date(); const currentTime = new Date();
const minutes = currentTime.getHours() * 60 + currentTime.getMinutes(); const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
// 找到最近的前15分钟刻度 // 找到最近的前15分钟刻度
const roundedMinutes = Math.floor(minutes / 15) * 15; // const roundedMinutes = Math.floor(minutes / 15) * 15;
setCurrentTime(roundedMinutes); // 组件卸载时重置时间 setCurrentTime(minutes); // 组件卸载时重置时间
return () => { return () => {
if (intervalRef.current) { if (intervalRef.current) {
@@ -345,6 +402,13 @@ const Timeline: React.FC = () => {
} }
}; };
}, []); }, []);
// 当 timeRange 改变时,设置 currentTime 到 minTime
useEffect(() => {
if (timeRange) {
setCurrentTime(minTime);
}
}, [timeRange, minTime]);
// 获取地图实例 // 获取地图实例
const map = useMap(); const map = useMap();
// 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错
@@ -364,211 +428,260 @@ const Timeline: React.FC = () => {
}, [map, handlePause]); }, [map, handlePause]);
return ( return (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={zhCN}> <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">
<Paper <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={zhCN}>
elevation={3} <Paper
sx={{ elevation={3}
position: "absolute", sx={{
bottom: 0, position: "absolute",
left: 0, bottom: 0,
right: 0, left: 0,
zIndex: 1000, right: 0,
p: 2, zIndex: 1000,
backgroundColor: "rgba(255, 255, 255, 0.95)", p: 2,
backdropFilter: "blur(10px)", backgroundColor: "rgba(255, 255, 255, 0.95)",
}} backdropFilter: "blur(10px)",
> }}
<Box sx={{ width: "100%" }}> >
{/* 控制按钮栏 */} <Box sx={{ width: "100%" }}>
<Stack {/* 控制按钮栏 */}
direction="row" <Stack
spacing={2} direction="row"
alignItems="center" spacing={2}
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }} 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",
}}
> >
{formatTime(currentTime)} <Tooltip title="后退一天">
</Typography> <IconButton
</Stack> 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>
{/* 时间轴滑块 */} <Tooltip title="后退一步">
<Box ref={timelineRef} sx={{ px: 2 }}> <IconButton
<Slider color="primary"
value={currentTime} onClick={handleStepBackward}
min={0} size="small"
max={1440} // 24:00 = 1440分钟 >
step={15} // 每15分钟一个步进 <TbRewindBackward15 />
marks={timeMarks.filter((_, index) => index % 12 === 0)} // 每小时显示一个标记 </IconButton>
onChange={handleSliderChange} </Tooltip>
valueLabelDisplay="auto"
valueLabelFormat={formatTime} <Tooltip title={isPlaying ? "暂停" : "播放"}>
sx={{ <IconButton
height: 8, color="primary"
"& .MuiSlider-track": { onClick={isPlaying ? handlePause : handlePlay}
backgroundColor: "primary.main", size="small"
height: 6, >
}, {isPlaying ? <Pause /> : <PlayArrow />}
"& .MuiSlider-rail": { </IconButton>
backgroundColor: "grey.300", </Tooltip>
height: 6,
}, <Tooltip title="前进一步">
"& .MuiSlider-thumb": { <IconButton
height: 20, color="primary"
width: 20, onClick={handleStepForward}
backgroundColor: "primary.main", size="small"
border: "2px solid #fff", >
boxShadow: "0 2px 8px rgba(0,0,0,0.2)", <TbRewindForward15 />
"&:hover": { </IconButton>
boxShadow: "0 4px 12px rgba(0,0,0,0.3)", </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-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",
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>
</Box> </Paper>
</Paper> </LocalizationProvider>
</LocalizationProvider> </div>
); );
}; };

View File

@@ -13,12 +13,49 @@ import { Style, Stroke, Fill, Circle } from "ol/style";
import { FeatureLike } from "ol/Feature"; import { FeatureLike } from "ol/Feature";
import Feature from "ol/Feature"; import Feature from "ol/Feature";
import StyleEditorPanel from "./StyleEditorPanel"; import StyleEditorPanel from "./StyleEditorPanel";
import StyleLegend from "./StyleLegend"; // 引入图例组件
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import { config } from "@/config/config"; import { config } from "@/config/config";
const backendUrl = config.backendUrl; 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 // 添加接口定义隐藏按钮的props
interface ToolbarProps { interface ToolbarProps {
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style'] hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
@@ -38,6 +75,79 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons }) => {
const [highlightLayer, setHighlightLayer] = const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null); 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(() => { useEffect(() => {
if (!map) return; if (!map) return;
@@ -73,7 +183,9 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons }) => {
map.removeLayer(highLightLayer); map.removeLayer(highLightLayer);
}; };
}, [map]); }, [map]);
useEffect(() => {
console.log(layerStyleStates);
}, [layerStyleStates]);
// 高亮要素的函数 // 高亮要素的函数
useEffect(() => { useEffect(() => {
if (!highlightLayer) { if (!highlightLayer) {
@@ -348,7 +460,23 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons }) => {
</div> </div>
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />} {showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
{showDrawPanel && map && <DrawPanel />} {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>
)}
</> </>
); );
}; };

View File

@@ -30,7 +30,7 @@ const Zoom: React.FC = () => {
}; };
return ( 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-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"> <div className="w-8 h-8 bg-gray-50 flex items-center justify-center rounded-xl drop-shadow-xl shadow-black">
<button <button

View File

@@ -18,15 +18,12 @@ import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
import MVT from "ol/format/MVT"; import MVT from "ol/format/MVT";
import { FlatStyleLike } from "ol/style/flat"; import { FlatStyleLike } from "ol/style/flat";
import { toLonLat } from "ol/proj"; import { toLonLat } from "ol/proj";
import { center } from "@turf/center"; import { along, bearing, lineString, length } from "@turf/turf";
import { bearing } from "@turf/turf";
import { Deck } from "@deck.gl/core"; import { Deck } from "@deck.gl/core";
import { TextLayer } from "@deck.gl/layers"; import { TextLayer } from "@deck.gl/layers";
import { TripsLayer } from "@deck.gl/geo-layers"; import { TripsLayer } from "@deck.gl/geo-layers";
import { CollisionFilterExtension } from "@deck.gl/extensions"; import { CollisionFilterExtension } from "@deck.gl/extensions";
import StyleLegend from "./Controls/StyleLegend"; // 假设 StyleLegend 在同一目录
interface MapComponentProps { interface MapComponentProps {
children?: React.ReactNode; children?: React.ReactNode;
} }
@@ -47,7 +44,6 @@ interface DataContextType {
pipeText: string; pipeText: string;
setJunctionText?: React.Dispatch<React.SetStateAction<string>>; setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
setPipeText?: React.Dispatch<React.SetStateAction<string>>; setPipeText?: React.Dispatch<React.SetStateAction<string>>;
updateLegendConfigs?: (configs: any[]) => void;
} }
// 创建自定义Layer类来包装deck.gl // 创建自定义Layer类来包装deck.gl
@@ -128,12 +124,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const [pipeText, setPipeText] = useState(""); const [pipeText, setPipeText] = useState("");
const flowAnimation = useRef(false); // 添加动画控制标志 const flowAnimation = useRef(false); // 添加动画控制标志
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别 const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
// 图例配置
const [activeLegendConfigs, setActiveLegendConfigs] = useState<any[]>([]); // 存储图例配置
// 从 StyleEditorPanel 接收图例配置的回调
const updateLegendConfigs = (configs: any[]) => {
setActiveLegendConfigs(configs);
};
// 防抖更新函数 // 防抖更新函数
const debouncedUpdateData = useRef( const debouncedUpdateData = useRef(
debounce(() => { debounce(() => {
@@ -297,16 +288,19 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const [lon, lat] = toLonLat(coord); const [lon, lat] = toLonLat(coord);
return [lon, lat]; return [lon, lat];
}); });
// 添加验证:确保至少有 2 个坐标点
if (lineCoordsWGS84.length < 2) return; // 跳过此特征
// 计算中点 // 计算中点
const midPoint = center({ const lineStringFeature = lineString(lineCoordsWGS84);
type: "LineString", const lineLength = length(lineStringFeature);
coordinates: lineCoordsWGS84, const midPoint = along(lineStringFeature, lineLength / 2)
}).geometry.coordinates; .geometry.coordinates;
// 计算角度 // 计算角度
let lineAngle = bearing( const prevPoint = along(lineStringFeature, lineLength * 0.49)
lineCoordsWGS84[0], .geometry.coordinates;
lineCoordsWGS84[lineCoordsWGS84.length - 1] const nextPoint = along(lineStringFeature, lineLength * 0.51)
); .geometry.coordinates;
let lineAngle = bearing(prevPoint, nextPoint);
lineAngle = -lineAngle + 90; lineAngle = -lineAngle + 90;
if (lineAngle < -90 || lineAngle > 90) { if (lineAngle < -90 || lineAngle > 90) {
lineAngle += 180; lineAngle += 180;
@@ -611,28 +605,16 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
showPipeText, showPipeText,
junctionText, junctionText,
pipeText, pipeText,
updateLegendConfigs,
}} }}
> >
<MapContext.Provider value={map}> <MapContext.Provider value={map}>
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<div ref={mapRef} className="w-full h-full"></div> <div ref={mapRef} className="w-full h-full"></div>
<MapTools /> <MapTools />
{children} {children}
</div> </div>
<canvas id="deck-canvas" /> <canvas id="deck-canvas" />
</MapContext.Provider> </MapContext.Provider>
{/* 图例始终渲染 */}
{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>
)}
</DataContext.Provider> </DataContext.Provider>
</> </>
); );

View File

@@ -18,13 +18,15 @@ import "dayjs/locale/zh-cn";
import { useMap } from "@app/OlMap/MapComponent"; import { useMap } from "@app/OlMap/MapComponent";
import VectorLayer from "ol/layer/Vector"; import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector"; import VectorSource from "ol/source/Vector";
import Style from "ol/style/Style"; import { Style, Stroke, Icon } from "ol/style";
import Stroke from "ol/style/Stroke";
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import Feature from "ol/Feature"; import Feature, { FeatureLike } from "ol/Feature";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import axios from "axios"; import axios from "axios";
import { config, NETWORK_NAME } from "@/config/config"; import { config, NETWORK_NAME } from "@/config/config";
import { along, lineString, length, toMercator } from "@turf/turf";
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
interface PipePoint { interface PipePoint {
id: string; id: string;
@@ -54,34 +56,65 @@ const AnalysisParameters: React.FC = () => {
useEffect(() => { useEffect(() => {
if (!map) return; if (!map) return;
// 创建高亮图层 const burstPipeStyle = function (feature: FeatureLike) {
const highlightLayer = new VectorLayer({ const styles = [];
source: new VectorSource(), // 线条样式(底层发光,主线条,内层高亮线)
style: [ styles.push(
// 外层发光效果(底层)
new Style({ new Style({
stroke: new Stroke({ stroke: new Stroke({
color: "rgba(255, 0, 0, 0.3)", color: "rgba(255, 0, 0, 0.3)",
width: 12, width: 12,
}), }),
}), }),
// 主线条 - 使用虚线表示爆管
new Style({ new Style({
stroke: new Stroke({ stroke: new Stroke({
color: "#ff0000", color: "rgba(255, 0, 0, 1)",
width: 6, width: 6,
lineDash: [15, 10], // 虚线样式,表示管道损坏/爆管
}),
}),
// 内层高亮线
new Style({
stroke: new Stroke({
color: "#ff6666",
width: 3,
lineDash: [15, 10], lineDash: [15, 10],
}), }),
}), }),
], new Style({
stroke: new Stroke({
color: "rgba(255, 102, 102, 1)",
width: 3,
lineDash: [15, 10],
}),
})
);
const geometry = feature.getGeometry();
const lineCoords =
geometry?.getType() === "LineString"
? (geometry as any).getCoordinates()
: null;
if (geometry) {
const lineCoordsWGS84 = lineCoords.map((coord: []) => {
const [lon, lat] = toLonLat(coord);
return [lon, lat];
});
// 计算中点
const lineStringFeature = lineString(lineCoordsWGS84);
const lineLength = length(lineStringFeature);
const midPoint = along(lineStringFeature, lineLength / 2).geometry
.coordinates;
// 在中点添加 icon 样式
const midPointMercator = toMercator(midPoint);
styles.push(
new Style({
geometry: new Point(midPointMercator),
image: new Icon({
src: "/icons/burst_pipe_icon.svg",
scale: 0.2,
anchor: [0.5, 1],
}),
})
);
}
return styles;
};
// 创建高亮图层
const highlightLayer = new VectorLayer({
source: new VectorSource(),
style: burstPipeStyle,
properties: { properties: {
name: "高亮管道", name: "高亮管道",
value: "highlight_pipeline", value: "highlight_pipeline",
@@ -132,7 +165,6 @@ const AnalysisParameters: React.FC = () => {
) )
.map((feature) => { .map((feature) => {
const properties = feature.getProperties(); const properties = feature.getProperties();
console.log("管道属性:", feature, properties);
return { return {
id: properties.id, id: properties.id,
diameter: properties.diameter || 0, diameter: properties.diameter || 0,
@@ -210,6 +242,14 @@ const AnalysisParameters: React.FC = () => {
const handleAnalyze = async () => { const handleAnalyze = async () => {
setAnalyzing(true); setAnalyzing(true);
// 显示处理中的通知
open?.({
key: "burst-analysis",
type: "progress",
message: "方案提交分析中",
undoableTimeout: 3,
});
const burst_ID = pipePoints.map((pipe) => pipe.id); const burst_ID = pipePoints.map((pipe) => pipe.id);
const burst_size = pipePoints.map((pipe) => const burst_size = pipePoints.map((pipe) =>
parseInt(pipe.area.toString(), 10) parseInt(pipe.area.toString(), 10)
@@ -240,8 +280,8 @@ const AnalysisParameters: React.FC = () => {
open?.({ open?.({
key: "burst-analysis", key: "burst-analysis",
type: "success", type: "success",
message: "分析请求提交成功", message: "方案分析成功",
description: "方案已成功提交,正在进行分析", description: "方案分析完成,请在方案查询中查看结果。",
}); });
} catch (error) { } catch (error) {
console.error("分析请求失败:", error); console.error("分析请求失败:", error);
@@ -427,7 +467,7 @@ const AnalysisParameters: React.FC = () => {
disabled={analyzing} disabled={analyzing}
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
> >
{analyzing ? "方案提交中..." : "方案分析"} {analyzing ? "方案提交分析中..." : "方案分析"}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@@ -1,6 +1,8 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom"; // 添加这行
import { import {
Box, Box,
Button, Button,
@@ -35,9 +37,12 @@ import * as turf from "@turf/turf";
import { GeoJSON } from "ol/format"; import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector"; import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector"; import VectorSource from "ol/source/Vector";
import { Stroke, Style } from "ol/style"; import { Stroke, Style, Icon } from "ol/style";
import Feature from "ol/Feature"; import Feature, { FeatureLike } from "ol/Feature";
import { set } from "ol/transform"; import { along, lineString, length, toMercator } from "@turf/turf";
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
import Timeline from "@app/OlMap/Controls/Timeline";
interface SchemeDetail { interface SchemeDetail {
burst_ID: string[]; burst_ID: string[];
@@ -72,7 +77,6 @@ interface SchemaItem {
interface SchemeQueryProps { interface SchemeQueryProps {
schemes?: SchemeRecord[]; schemes?: SchemeRecord[];
onSchemesChange?: (schemes: SchemeRecord[]) => void; onSchemesChange?: (schemes: SchemeRecord[]) => void;
onViewDetails?: (id: number) => void;
onLocate?: (id: number) => void; onLocate?: (id: number) => void;
network?: string; network?: string;
} }
@@ -80,20 +84,29 @@ interface SchemeQueryProps {
const SchemeQuery: React.FC<SchemeQueryProps> = ({ const SchemeQuery: React.FC<SchemeQueryProps> = ({
schemes: externalSchemes, schemes: externalSchemes,
onSchemesChange, onSchemesChange,
onViewDetails,
onLocate, onLocate,
network = NETWORK_NAME, network = NETWORK_NAME,
}) => { }) => {
const [queryAll, setQueryAll] = useState<boolean>(true); const [queryAll, setQueryAll] = useState<boolean>(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date())); const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs(new Date()));
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const { open } = useNotification();
const [highlightLayer, setHighlightLayer] = const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null); useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]); const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
// 时间轴相关状态
const [showTimeline, setShowTimeline] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [timeRange, setTimeRange] = useState<
{ start: Date; end: Date } | undefined
>();
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [schemeName, setSchemeName] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
const { open } = useNotification();
const map = useMap(); const map = useMap();
// 使用外部提供的 schemes 或内部状态 // 使用外部提供的 schemes 或内部状态
@@ -181,38 +194,95 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}); });
} }
}; };
// 内部的方案查询函数
const handleViewDetails = (id: number) => {
setShowTimeline(true);
// 计算时间范围
const scheme = schemes.find((s) => s.id === id);
const burstPipeIds = scheme?.schemeDetail?.burst_ID || [];
const schemeDate = scheme?.startTime
? new Date(scheme.startTime)
: undefined;
if (scheme?.startTime && scheme.schemeDetail?.modify_total_duration) {
const start = new Date(scheme.startTime);
const end = new Date(
start.getTime() + scheme.schemeDetail.modify_total_duration * 1000
);
setSelectedDate(schemeDate);
setTimeRange({ start, end });
setSchemeName(scheme.schemeName);
handleLocatePipes(burstPipeIds);
}
};
// 初始化管道图层和高亮图层 // 初始化管道图层和高亮图层
useEffect(() => { useEffect(() => {
if (!map) return; if (!map) return;
// 获取地图的目标容器
// 创建高亮图层 - 爆管管段标识样式 const target = map.getTargetElement();
const highlightLayer = new VectorLayer({ if (target) {
source: new VectorSource(), setMapContainer(target);
style: [ }
// 外层发光效果(底层) const burstPipeStyle = function (feature: FeatureLike) {
const styles = [];
// 线条样式(底层发光,主线条,内层高亮线)
styles.push(
new Style({ new Style({
stroke: new Stroke({ stroke: new Stroke({
color: "rgba(255, 0, 0, 0.3)", color: "rgba(255, 0, 0, 0.3)",
width: 12, width: 12,
}), }),
}), }),
// 主线条 - 使用虚线表示爆管
new Style({ new Style({
stroke: new Stroke({ stroke: new Stroke({
color: "#ff0000", color: "rgba(255, 0, 0, 1)",
width: 6, width: 6,
lineDash: [15, 10], // 虚线样式,表示管道损坏/爆管
}),
}),
// 内层高亮线
new Style({
stroke: new Stroke({
color: "#ff6666",
width: 3,
lineDash: [15, 10], lineDash: [15, 10],
}), }),
}), }),
], new Style({
stroke: new Stroke({
color: "rgba(255, 102, 102, 1)",
width: 3,
lineDash: [15, 10],
}),
})
);
const geometry = feature.getGeometry();
const lineCoords =
geometry?.getType() === "LineString"
? (geometry as any).getCoordinates()
: null;
if (geometry) {
const lineCoordsWGS84 = lineCoords.map((coord: []) => {
const [lon, lat] = toLonLat(coord);
return [lon, lat];
});
// 计算中点
const lineStringFeature = lineString(lineCoordsWGS84);
const lineLength = length(lineStringFeature);
const midPoint = along(lineStringFeature, lineLength / 2).geometry
.coordinates;
// 在中点添加 icon 样式
const midPointMercator = toMercator(midPoint);
styles.push(
new Style({
geometry: new Point(midPointMercator),
image: new Icon({
src: "/icons/burst_pipe_icon.svg",
scale: 0.2,
anchor: [0.5, 1],
}),
})
);
}
return styles;
};
// 创建高亮图层 - 爆管管段标识样式
const highlightLayer = new VectorLayer({
source: new VectorSource(),
style: burstPipeStyle,
properties: { properties: {
name: "爆管管段高亮", name: "爆管管段高亮",
value: "burst_pipe_highlight", value: "burst_pipe_highlight",
@@ -247,348 +317,366 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
}, [highlightFeatures]); }, [highlightFeatures]);
return ( return (
<Box className="flex flex-col h-full"> <>
{/* 查询条件 - 单行布局 */} {/* 将时间轴渲染到地图容器中 */}
<Box className="mb-2 p-2 bg-gray-50 rounded"> {showTimeline &&
<Box className="flex items-center gap-2 justify-between"> mapContainer &&
<Box className="flex items-center gap-2"> ReactDOM.createPortal(
<FormControlLabel <Timeline
control={ schemeDate={selectedDate}
<Checkbox timeRange={timeRange}
checked={queryAll} disableDateSelection={!!timeRange}
onChange={(e) => setQueryAll(e.target.checked)} schemeName={schemeName}
size="small" />,
/> mapContainer // 渲染到地图容器中,而不是 body
} )}
label={<Typography variant="body2"></Typography>} <Box className="flex flex-col h-full">
className="m-0" {/* 查询条件 - 单行布局 */}
/> <Box className="mb-2 p-2 bg-gray-50 rounded">
<LocalizationProvider <Box className="flex items-center gap-2 justify-between">
dateAdapter={AdapterDayjs} <Box className="flex items-center gap-2">
adapterLocale="zh-cn" <FormControlLabel
> control={
<DatePicker <Checkbox
value={queryDate} checked={queryAll}
onChange={(value) => onChange={(e) => setQueryAll(e.target.checked)}
value && dayjs.isDayjs(value) && setQueryDate(value) size="small"
/>
} }
format="YYYY-MM-DD" label={<Typography variant="body2"></Typography>}
disabled={queryAll} className="m-0"
slotProps={{
textField: {
size: "small",
sx: { width: 200 },
},
}}
/> />
</LocalizationProvider> <LocalizationProvider
</Box> dateAdapter={AdapterDayjs}
<Button adapterLocale="zh-cn"
variant="contained"
onClick={handleQuery}
disabled={loading}
size="small"
className="bg-blue-600 hover:bg-blue-700"
sx={{ minWidth: 80 }}
>
{loading ? "查询中..." : "查询"}
</Button>
</Box>
</Box>
{/* 结果列表 */}
<Box className="flex-1 overflow-auto">
{schemes.length === 0 ? (
<Box className="flex flex-col items-center justify-center h-full text-gray-400">
<Box className="mb-4">
<svg
width="80"
height="80"
viewBox="0 0 80 80"
fill="none"
className="opacity-40"
> >
<rect <DatePicker
x="10" value={queryDate}
y="20" onChange={(value) =>
width="60" value && dayjs.isDayjs(value) && setQueryDate(value)
height="45" }
rx="2" format="YYYY-MM-DD"
stroke="currentColor" disabled={queryAll}
strokeWidth="2" slotProps={{
textField: {
size: "small",
sx: { width: 200 },
},
}}
/> />
<line </LocalizationProvider>
x1="10"
y1="30"
x2="70"
y2="30"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</Box> </Box>
<Typography variant="body2"> 0 </Typography> <Button
<Typography variant="body2" className="mt-1"> variant="contained"
No data onClick={handleQuery}
</Typography> disabled={loading}
size="small"
className="bg-blue-600 hover:bg-blue-700"
sx={{ minWidth: 80 }}
>
{loading ? "查询中..." : "查询"}
</Button>
</Box> </Box>
) : ( </Box>
<Box className="space-y-2 p-2">
<Typography variant="caption" className="text-gray-500 px-2">
{schemes.length}
</Typography>
{schemes.map((scheme) => (
<Card
key={scheme.id}
variant="outlined"
className="hover:shadow-md transition-shadow"
>
<CardContent className="p-3 pb-2 last:pb-3">
{/* 主要信息行 */}
<Box className="flex items-start justify-between mb-2">
<Box className="flex-1 min-w-0">
<Box className="flex items-center gap-2 mb-1">
<Typography
variant="body2"
className="font-medium truncate"
title={scheme.schemeName}
>
{scheme.schemeName}
</Typography>
<Chip
label={scheme.type}
size="small"
className="h-5"
color="primary"
variant="outlined"
/>
</Box>
<Typography
variant="caption"
className="text-gray-500 block"
>
ID: {scheme.id} · : {formatTime(scheme.create_time)}
</Typography>
</Box>
{/* 操作按钮 */}
<Box className="flex gap-1 ml-2">
<Tooltip
title={
expandedId === scheme.id ? "收起详情" : "查看详情"
}
>
<IconButton
size="small"
onClick={() =>
setExpandedId(
expandedId === scheme.id ? null : scheme.id
)
}
color="primary"
className="p-1"
>
<InfoIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="定位">
<IconButton
size="small"
onClick={() => onLocate?.(scheme.id)}
color="primary"
className="p-1"
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* 可折叠的详细信息 */} {/* 结果列表 */}
<Collapse in={expandedId === scheme.id}> <Box className="flex-1 overflow-auto">
<Box className="mt-2 pt-3 border-t border-gray-200"> {schemes.length === 0 ? (
{/* 信息网格布局 */} <Box className="flex flex-col items-center justify-center h-full text-gray-400">
<Box className="grid grid-cols-2 gap-x-4 gap-y-3 mb-3"> <Box className="mb-4">
{/* 爆管详情列 */} <svg
<Box className="space-y-2"> width="80"
<Box className="space-y-1.5 pl-2"> height="80"
<Box className="flex items-start gap-2"> viewBox="0 0 80 80"
<Typography fill="none"
variant="caption" className="opacity-40"
className="text-gray-600 min-w-[70px] mt-0.5" >
> <rect
ID: x="10"
</Typography> y="20"
<Box className="flex-1 flex flex-wrap gap-1"> width="60"
{scheme.schemeDetail?.burst_ID?.length ? ( height="45"
scheme.schemeDetail.burst_ID.map( rx="2"
(pipeId, index) => ( stroke="currentColor"
<Link strokeWidth="2"
key={index} />
component="button" <line
variant="caption" x1="10"
className="font-medium text-blue-600 hover:text-blue-800 underline cursor-pointer" y1="30"
onClick={(e) => { x2="70"
e.preventDefault(); y2="30"
handleLocatePipes?.([pipeId]); stroke="currentColor"
}} strokeWidth="2"
> />
{pipeId} </svg>
</Link> </Box>
<Typography variant="body2"> 0 </Typography>
<Typography variant="body2" className="mt-1">
No data
</Typography>
</Box>
) : (
<Box className="space-y-2 p-2">
<Typography variant="caption" className="text-gray-500 px-2">
{schemes.length}
</Typography>
{schemes.map((scheme) => (
<Card
key={scheme.id}
variant="outlined"
className="hover:shadow-md transition-shadow"
>
<CardContent className="p-3 pb-2 last:pb-3">
{/* 主要信息行 */}
<Box className="flex items-start justify-between mb-2">
<Box className="flex-1 min-w-0">
<Box className="flex items-center gap-2 mb-1">
<Typography
variant="body2"
className="font-medium truncate"
title={scheme.schemeName}
>
{scheme.schemeName}
</Typography>
<Chip
label={scheme.type}
size="small"
className="h-5"
color="primary"
variant="outlined"
/>
</Box>
<Typography
variant="caption"
className="text-gray-500 block"
>
ID: {scheme.id} · :{" "}
{formatTime(scheme.create_time)}
</Typography>
</Box>
{/* 操作按钮 */}
<Box className="flex gap-1 ml-2">
<Tooltip
title={
expandedId === scheme.id ? "收起详情" : "查看详情"
}
>
<IconButton
size="small"
onClick={() =>
setExpandedId(
expandedId === scheme.id ? null : scheme.id
)
}
color="primary"
className="p-1"
>
<InfoIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="定位">
<IconButton
size="small"
onClick={() => onLocate?.(scheme.id)}
color="primary"
className="p-1"
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* 可折叠的详细信息 */}
<Collapse in={expandedId === scheme.id}>
<Box className="mt-2 pt-3 border-t border-gray-200">
{/* 信息网格布局 */}
<Box className="grid grid-cols-2 gap-x-4 gap-y-3 mb-3">
{/* 爆管详情列 */}
<Box className="space-y-2">
<Box className="space-y-1.5 pl-2">
<Box className="flex items-start gap-2">
<Typography
variant="caption"
className="text-gray-600 min-w-[70px] mt-0.5"
>
ID:
</Typography>
<Box className="flex-1 flex flex-wrap gap-1">
{scheme.schemeDetail?.burst_ID?.length ? (
scheme.schemeDetail.burst_ID.map(
(pipeId, index) => (
<Link
key={index}
component="button"
variant="caption"
className="font-medium text-blue-600 hover:text-blue-800 underline cursor-pointer"
onClick={(e) => {
e.preventDefault();
handleLocatePipes?.([pipeId]);
}}
>
{pipeId}
</Link>
)
) )
) ) : (
) : ( <Typography
<Typography variant="caption"
variant="caption" className="font-medium text-gray-900"
className="font-medium text-gray-900" >
> N/A
N/A </Typography>
</Typography> )}
)} </Box>
</Box>
<Box className="flex items-center gap-2">
<Typography
variant="caption"
className="text-gray-600 min-w-[70px]"
>
:
</Typography>
<Typography
variant="caption"
className="font-medium text-gray-900"
>
560 mm
</Typography>
</Box>
<Box className="flex items-center gap-2">
<Typography
variant="caption"
className="text-gray-600 min-w-[70px]"
>
:
</Typography>
<Typography
variant="caption"
className="font-medium text-gray-900"
>
{scheme.schemeDetail?.burst_size?.[0] ||
"N/A"}{" "}
cm²
</Typography>
</Box>
<Box className="flex items-center gap-2">
<Typography
variant="caption"
className="text-gray-600 min-w-[70px]"
>
:
</Typography>
<Typography
variant="caption"
className="font-medium text-gray-900"
>
{scheme.schemeDetail?.modify_total_duration ||
"N/A"}{" "}
</Typography>
</Box> </Box>
</Box> </Box>
<Box className="flex items-center gap-2"> </Box>
<Typography
variant="caption" {/* 方案信息列 */}
className="text-gray-600 min-w-[70px]" <Box className="space-y-2">
> <Box className="space-y-1.5 pl-2">
: <Box className="flex items-center gap-2">
</Typography> <Typography
<Typography variant="caption"
variant="caption" className="text-gray-600 min-w-[70px]"
className="font-medium text-gray-900" >
> :
560 mm </Typography>
</Typography> <Typography
</Box> variant="caption"
<Box className="flex items-center gap-2"> className="font-medium text-gray-900"
<Typography >
variant="caption" {scheme.user}
className="text-gray-600 min-w-[70px]" </Typography>
> </Box>
: <Box className="flex items-center gap-2">
</Typography> <Typography
<Typography variant="caption"
variant="caption" className="text-gray-600 min-w-[70px]"
className="font-medium text-gray-900" >
> :
{scheme.schemeDetail?.burst_size?.[0] || "N/A"}{" "} </Typography>
cm² <Typography
</Typography> variant="caption"
</Box> className="font-medium text-gray-900"
<Box className="flex items-center gap-2"> >
<Typography {moment(scheme.create_time).format(
variant="caption" "YYYY-MM-DD HH:mm"
className="text-gray-600 min-w-[70px]" )}
> </Typography>
: </Box>
</Typography> <Box className="flex items-center gap-2">
<Typography <Typography
variant="caption" variant="caption"
className="font-medium text-gray-900" className="text-gray-600 min-w-[70px]"
> >
{scheme.schemeDetail?.modify_total_duration || :
"N/A"}{" "} </Typography>
<Typography
</Typography> variant="caption"
className="font-medium text-gray-900"
>
{moment(scheme.startTime).format(
"YYYY-MM-DD HH:mm"
)}
</Typography>
</Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
{/* 方案信息列 */} {/* 操作按钮区域 */}
<Box className="space-y-2"> <Box className="pt-2 border-t border-gray-100 flex gap-5">
<Box className="space-y-1.5 pl-2"> {scheme.schemeDetail?.burst_ID?.length ? (
<Box className="flex items-center gap-2"> <Button
<Typography variant="outlined"
variant="caption" fullWidth
className="text-gray-600 min-w-[70px]" size="small"
> className="border-blue-600 text-blue-600 hover:bg-blue-50"
: onClick={() =>
</Typography> handleLocatePipes?.(
<Typography scheme.schemeDetail!.burst_ID
variant="caption" )
className="font-medium text-gray-900" }
> sx={{
{scheme.user} textTransform: "none",
</Typography> fontWeight: 500,
</Box> }}
<Box className="flex items-center gap-2"> >
<Typography
variant="caption" </Button>
className="text-gray-600 min-w-[70px]" ) : null}
>
:
</Typography>
<Typography
variant="caption"
className="font-medium text-gray-900"
>
{moment(scheme.create_time).format(
"YYYY-MM-DD HH:mm"
)}
</Typography>
</Box>
<Box className="flex items-center gap-2">
<Typography
variant="caption"
className="text-gray-600 min-w-[70px]"
>
:
</Typography>
<Typography
variant="caption"
className="font-medium text-gray-900"
>
{moment(scheme.startTime).format(
"YYYY-MM-DD HH:mm"
)}
</Typography>
</Box>
</Box>
</Box>
</Box>
{/* 操作按钮区域 */}
<Box className="pt-2 border-t border-gray-100 flex gap-5">
{scheme.schemeDetail?.burst_ID?.length ? (
<Button <Button
variant="outlined" variant="contained"
fullWidth fullWidth
size="small" size="small"
className="border-blue-600 text-blue-600 hover:bg-blue-50" className="bg-blue-600 hover:bg-blue-700"
onClick={() => onClick={() => handleViewDetails(scheme.id)}
handleLocatePipes?.(scheme.schemeDetail!.burst_ID)
}
sx={{ sx={{
textTransform: "none", textTransform: "none",
fontWeight: 500, fontWeight: 500,
}} }}
> >
</Button> </Button>
) : null} </Box>
<Button
variant="contained"
fullWidth
size="small"
className="bg-blue-600 hover:bg-blue-700"
onClick={() => onViewDetails?.(scheme.id)}
sx={{
textTransform: "none",
fontWeight: 500,
}}
>
</Button>
</Box> </Box>
</Box> </Collapse>
</Collapse> </CardContent>
</CardContent> </Card>
</Card> ))}
))} </Box>
</Box> )}
)} </Box>
</Box> </Box>
</Box> </>
); );
}; };

View File

@@ -12,7 +12,6 @@ import {
import AnalysisParameters from "./BurstPipeAnalysis/AnalysisParameters"; import AnalysisParameters from "./BurstPipeAnalysis/AnalysisParameters";
import SchemeQuery from "./BurstPipeAnalysis/SchemeQuery"; import SchemeQuery from "./BurstPipeAnalysis/SchemeQuery";
import LocationResults from "./BurstPipeAnalysis/LocationResults"; import LocationResults from "./BurstPipeAnalysis/LocationResults";
interface SchemeDetail { interface SchemeDetail {
burst_ID: string[]; burst_ID: string[];
burst_size: number[]; burst_size: number[];
@@ -81,7 +80,6 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue); setCurrentTab(newValue);
}; };
const drawerWidth = 520; const drawerWidth = 520;
return ( return (
@@ -89,7 +87,7 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
{/* 收起时的触发按钮 */} {/* 收起时的触发按钮 */}
{!isOpen && ( {!isOpen && (
<Box <Box
className="absolute top-4 right-4 z-[1300] bg-white shadow-2xl rounded-lg cursor-pointer hover:shadow-xl transition-all duration-300 opacity-95 hover:opacity-100" className="absolute top-4 right-4 bg-white shadow-2xl rounded-lg cursor-pointer hover:shadow-xl transition-all duration-300 opacity-95 hover:opacity-100"
onClick={handleToggle} onClick={handleToggle}
> >
<Box className="flex flex-col items-center py-3 px-3 gap-1"> <Box className="flex flex-col items-center py-3 px-3 gap-1">
@@ -205,10 +203,6 @@ const BurstPipeAnalysisPanel: React.FC<BurstPipeAnalysisPanelProps> = ({
<SchemeQuery <SchemeQuery
schemes={schemes} schemes={schemes}
onSchemesChange={setSchemes} onSchemesChange={setSchemes}
onViewDetails={(id) => {
console.log("查看详情:", id);
// TODO: 显示方案详情
}}
onLocate={(id) => { onLocate={(id) => {
console.log("定位方案:", id); console.log("定位方案:", id);
// TODO: 在地图上定位 // TODO: 在地图上定位

View File

@@ -356,7 +356,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
<Paper <Paper
className={clsx( className={clsx(
"absolute right-4 top-20 w-4xl h-2xl bg-white rounded-xl shadow-lg overflow-hidden flex flex-col transition-opacity duration-300", "absolute right-4 top-20 w-4xl h-2xl bg-white rounded-xl shadow-lg overflow-hidden flex flex-col transition-opacity duration-300",
visible ? "opacity-95 hover:opacity-100" : "opacity-0 -z-10" visible ? "opacity-95 hover:opacity-100" : "opacity-0 z-10"
)} )}
> >
{/* Header */} {/* Header */}