爆管分析页面,新增时间轴、工具栏,修改部分组件以满足页面功能需求
This commit is contained in:
1
public/icons/burst_pipe_icon.svg
Normal file
1
public/icons/burst_pipe_icon.svg
Normal 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 |
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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: 在地图上定位
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user