完成管网在线模拟页面组件基本样式和布局
This commit is contained in:
@@ -1,11 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import MapComponent from "@app/OlMap/MapComponent";
|
||||
import Timeline from "@app/OlMap/Controls/Timeline";
|
||||
import SCADADeviceList from "@components/olmap/SCADADeviceList";
|
||||
import SCADADataPanel from "@components/olmap/SCADADataPanel";
|
||||
|
||||
const mockDevices = [
|
||||
{
|
||||
id: "SCADA-001",
|
||||
name: "进水口压力",
|
||||
type: "pressure",
|
||||
coordinates: [121.4737, 31.2304] as [number, number],
|
||||
status: "online" as const,
|
||||
},
|
||||
{
|
||||
id: "SCADA-002",
|
||||
name: "二泵站流量",
|
||||
type: "flow",
|
||||
coordinates: [121.4807, 31.2204] as [number, number],
|
||||
status: "warning" as const,
|
||||
},
|
||||
{
|
||||
id: "SCADA-003",
|
||||
name: "管网节点 A",
|
||||
type: "pressure",
|
||||
coordinates: [121.4607, 31.2354] as [number, number],
|
||||
status: "offline" as const,
|
||||
},
|
||||
{
|
||||
id: "SCADA-004",
|
||||
name: "管网节点 B",
|
||||
type: "demand",
|
||||
coordinates: [121.4457, 31.2104] as [number, number],
|
||||
status: "online" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([]);
|
||||
const [panelVisible, setPanelVisible] = useState<boolean>(false);
|
||||
|
||||
const devices = useMemo(() => mockDevices, []);
|
||||
|
||||
const deviceLabels = useMemo(
|
||||
() =>
|
||||
devices.reduce<Record<string, string>>((acc, device) => {
|
||||
acc[device.id] = device.name;
|
||||
return acc;
|
||||
}, {}),
|
||||
[devices]
|
||||
);
|
||||
|
||||
const handleSelectionChange = useCallback((ids: string[]) => {
|
||||
setSelectedDeviceIds(ids);
|
||||
setPanelVisible(ids.length > 0);
|
||||
}, []);
|
||||
|
||||
const handleDeviceClick = useCallback(() => {
|
||||
setPanelVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleClosePanel = useCallback(() => {
|
||||
setPanelVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<MapComponent />
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[800px] opacity-90 hover:opacity-100 transition-opacity duration-300">
|
||||
<Timeline />
|
||||
</div>
|
||||
<SCADADeviceList
|
||||
devices={devices}
|
||||
onDeviceClick={handleDeviceClick}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
selectedDeviceIds={selectedDeviceIds}
|
||||
/>
|
||||
<SCADADataPanel
|
||||
deviceIds={selectedDeviceIds}
|
||||
deviceLabels={deviceLabels}
|
||||
visible={panelVisible}
|
||||
onClose={handleClosePanel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
60
src/app/OlMap/Controls/LayerControl.tsx
Normal file
60
src/app/OlMap/Controls/LayerControl.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useMap } from "../MapComponent";
|
||||
import { Layer } from "ol/layer";
|
||||
import { Checkbox, FormControlLabel } from "@mui/material";
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
|
||||
const LayerControl: React.FC = () => {
|
||||
const map = useMap();
|
||||
const [layers, setLayers] = useState<Layer[]>([]);
|
||||
const [layerVisibilities, setLayerVisibilities] = useState<
|
||||
Map<Layer, boolean>
|
||||
>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
const mapLayers = map
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.filter((layer) => layer instanceof WebGLVectorTileLayer) as Layer[];
|
||||
setLayers(mapLayers);
|
||||
const visible = new Map<Layer, boolean>();
|
||||
mapLayers.forEach((layer) => {
|
||||
visible.set(layer, layer.getVisible());
|
||||
});
|
||||
setLayerVisibilities(visible);
|
||||
}, [map]);
|
||||
|
||||
const handleVisibilityChange = (layer: Layer, visible: boolean) => {
|
||||
layer.setVisible(visible);
|
||||
setLayerVisibilities((prev) => new Map(prev).set(layer, visible));
|
||||
};
|
||||
|
||||
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="ml-3 grid grid-cols-3">
|
||||
{layers.map((layer, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={layerVisibilities.get(layer) ?? false}
|
||||
onChange={(e) =>
|
||||
handleVisibilityChange(layer, e.target.checked)
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={layer.get("name") || `Layer ${index + 1}`}
|
||||
sx={{
|
||||
fontSize: "0.7rem",
|
||||
"& .MuiFormControlLabel-label": { fontSize: "0.7rem" },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerControl;
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from "@mui/material";
|
||||
|
||||
// 导入OpenLayers样式相关模块
|
||||
import VectorTileLayer from "ol/layer/VectorTile";
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import { useMap } from "../MapComponent";
|
||||
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Slider,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
IconButton,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
||||
import { zhCN } from "date-fns/locale";
|
||||
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
|
||||
import { TbRewindBackward5, TbRewindForward5 } from "react-icons/tb";
|
||||
|
||||
interface TimelineProps {
|
||||
onTimeChange?: (time: string) => void;
|
||||
onDateChange?: (date: Date) => void;
|
||||
onPlay?: () => void;
|
||||
onPause?: () => void;
|
||||
onStop?: () => void;
|
||||
onRefresh?: () => void;
|
||||
onFetch?: () => void;
|
||||
}
|
||||
|
||||
const Timeline: React.FC<TimelineProps> = ({
|
||||
onTimeChange,
|
||||
onDateChange,
|
||||
onPlay,
|
||||
onPause,
|
||||
onStop,
|
||||
onRefresh,
|
||||
onFetch,
|
||||
}) => {
|
||||
const [currentTime, setCurrentTime] = useState<number>(0); // 分钟数 (0-1439)
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
|
||||
const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟
|
||||
const [sliderValue, setSliderValue] = useState<number>(0);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 时间刻度数组 (每5分钟一个刻度)
|
||||
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
||||
value: i * 5,
|
||||
label: i % 24 === 0 ? formatTime(i * 5) : "",
|
||||
}));
|
||||
|
||||
// 格式化时间显示
|
||||
function formatTime(minutes: number): string {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
return `${hours.toString().padStart(2, "0")}:${mins
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// 播放时间间隔选项
|
||||
const intervalOptions = [
|
||||
{ value: 1000, label: "1秒" },
|
||||
{ value: 2000, label: "2秒" },
|
||||
{ value: 5000, label: "5秒" },
|
||||
{ value: 10000, label: "10秒" },
|
||||
];
|
||||
// 播放时间间隔选项
|
||||
const calculatedIntervalOptions = [
|
||||
{ value: 1440, label: "1 天" },
|
||||
{ value: 60, label: "1 小时" },
|
||||
{ value: 30, label: "30 分钟" },
|
||||
{ value: 15, label: "15 分钟" },
|
||||
{ value: 5, label: "5 分钟" },
|
||||
];
|
||||
|
||||
// 处理时间轴滑动
|
||||
const handleSliderChange = useCallback(
|
||||
(event: Event, newValue: number | number[]) => {
|
||||
const value = Array.isArray(newValue) ? newValue[0] : newValue;
|
||||
setSliderValue(value);
|
||||
setCurrentTime(value);
|
||||
onTimeChange?.(formatTime(value));
|
||||
},
|
||||
[onTimeChange]
|
||||
);
|
||||
|
||||
// 播放控制
|
||||
const handlePlay = useCallback(() => {
|
||||
if (!isPlaying) {
|
||||
setIsPlaying(true);
|
||||
onPlay?.();
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentTime((prev) => {
|
||||
const next = prev >= 1435 ? 0 : prev + 5; // 到达23:55后回到00:00
|
||||
setSliderValue(next);
|
||||
onTimeChange?.(formatTime(next));
|
||||
return next;
|
||||
});
|
||||
}, playInterval);
|
||||
}
|
||||
}, [isPlaying, playInterval, onPlay, onTimeChange]);
|
||||
|
||||
const handlePause = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
onPause?.();
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [onPause]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
setSliderValue(0);
|
||||
onStop?.();
|
||||
onTimeChange?.(formatTime(0));
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [onStop, onTimeChange]);
|
||||
|
||||
// 步进控制
|
||||
const handleStepBackward = useCallback(() => {
|
||||
setCurrentTime((prev) => {
|
||||
const next = prev <= 0 ? 1435 : prev - 5;
|
||||
setSliderValue(next);
|
||||
onTimeChange?.(formatTime(next));
|
||||
return next;
|
||||
});
|
||||
}, [onTimeChange]);
|
||||
|
||||
const handleStepForward = useCallback(() => {
|
||||
setCurrentTime((prev) => {
|
||||
const next = prev >= 1435 ? 0 : prev + 5;
|
||||
setSliderValue(next);
|
||||
onTimeChange?.(formatTime(next));
|
||||
return next;
|
||||
});
|
||||
}, [onTimeChange]);
|
||||
|
||||
// 日期选择处理
|
||||
const handleDateChange = useCallback(
|
||||
(newDate: Date | null) => {
|
||||
if (newDate) {
|
||||
setSelectedDate(newDate);
|
||||
onDateChange?.(newDate);
|
||||
}
|
||||
},
|
||||
[onDateChange]
|
||||
);
|
||||
|
||||
// 播放间隔改变处理
|
||||
const handleIntervalChange = useCallback(
|
||||
(event: any) => {
|
||||
const newInterval = event.target.value;
|
||||
setPlayInterval(newInterval);
|
||||
|
||||
// 如果正在播放,重新启动定时器
|
||||
if (isPlaying && intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentTime((prev) => {
|
||||
const next = prev >= 1435 ? 0 : prev + 5;
|
||||
setSliderValue(next);
|
||||
onTimeChange?.(formatTime(next));
|
||||
return next;
|
||||
});
|
||||
}, newInterval);
|
||||
}
|
||||
},
|
||||
[isPlaying, onTimeChange]
|
||||
);
|
||||
// 计算时间段改变处理
|
||||
const handleCalculatedIntervalChange = useCallback((event: any) => {
|
||||
const newInterval = event.target.value;
|
||||
setCalculatedInterval(newInterval);
|
||||
}, []);
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={zhCN}>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
p: 2,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
||||
backdropFilter: "blur(10px)",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{/* 控制按钮栏 */}
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
|
||||
>
|
||||
{/* 日期选择器 */}
|
||||
<DatePicker
|
||||
label="模拟数据日期选择"
|
||||
value={selectedDate}
|
||||
onChange={(newValue) => handleDateChange(newValue)}
|
||||
enableAccessibleFieldDOMStructure={false}
|
||||
format="yyyy-MM-dd"
|
||||
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
|
||||
maxDate={new Date()} // 禁止选取未来的日期
|
||||
/>
|
||||
|
||||
{/* 播放控制按钮 */}
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
{/* 播放间隔选择 */}
|
||||
<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"
|
||||
>
|
||||
<TbRewindBackward5 />
|
||||
</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"
|
||||
>
|
||||
<TbRewindForward5 />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="停止">
|
||||
<IconButton color="secondary" onClick={handleStop} size="small">
|
||||
<Stop />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
{/* 强制计算时间段 */}
|
||||
<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 }}>
|
||||
<Slider
|
||||
value={sliderValue}
|
||||
min={0}
|
||||
max={1435} // 23:55 = 1435分钟
|
||||
step={5}
|
||||
marks={timeMarks.filter((_, index) => index % 12 === 0)} // 每小时显示一个标记
|
||||
onChange={handleSliderChange}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={formatTime}
|
||||
sx={{
|
||||
height: 8,
|
||||
"& .MuiSlider-track": {
|
||||
backgroundColor: "primary.main",
|
||||
height: 6,
|
||||
},
|
||||
"& .MuiSlider-rail": {
|
||||
backgroundColor: "grey.300",
|
||||
height: 6,
|
||||
},
|
||||
"& .MuiSlider-thumb": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
backgroundColor: "primary.main",
|
||||
border: "2px solid #fff",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
||||
"&:hover": {
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
||||
},
|
||||
},
|
||||
"& .MuiSlider-mark": {
|
||||
backgroundColor: "grey.400",
|
||||
height: 4,
|
||||
width: 2,
|
||||
},
|
||||
"& .MuiSlider-markActive": {
|
||||
backgroundColor: "primary.main",
|
||||
},
|
||||
"& .MuiSlider-markLabel": {
|
||||
fontSize: "0.75rem",
|
||||
color: "grey.600",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { useMap } from "../MapComponent";
|
||||
import Geolocation from "ol/Geolocation";
|
||||
import AddRoundedIcon from "@mui/icons-material/AddRounded";
|
||||
import RemoveRoundedIcon from "@mui/icons-material/RemoveRounded";
|
||||
import GpsFixedRoundedIcon from "@mui/icons-material/GpsFixedRounded";
|
||||
import clsx from "clsx";
|
||||
|
||||
const INITIAL_ZOOM = 14; // 默认缩放级别
|
||||
import FitScreenIcon from "@mui/icons-material/FitScreen";
|
||||
import { config } from "@config/config";
|
||||
|
||||
const Zoom: React.FC = () => {
|
||||
const map = useMap();
|
||||
const [locateDisabled, setLocateDisabled] = useState(false);
|
||||
|
||||
// 放大函数
|
||||
const handleZoomIn = () => {
|
||||
@@ -26,57 +22,22 @@ const Zoom: React.FC = () => {
|
||||
view.animate({ zoom: (view.getZoom() ?? 0) - 1, duration: 200 });
|
||||
};
|
||||
|
||||
// 定位功能
|
||||
const handleLocate = () => {
|
||||
// 缩放到全局 Extent
|
||||
const handleFitScreen = () => {
|
||||
if (!map) return;
|
||||
|
||||
const geolocation = new Geolocation({
|
||||
trackingOptions: { enableHighAccuracy: true },
|
||||
projection: map.getView().getProjection(),
|
||||
});
|
||||
|
||||
geolocation.once("change:position", () => {
|
||||
const coords = geolocation.getPosition();
|
||||
if (coords) {
|
||||
map
|
||||
.getView()
|
||||
.animate({ center: coords, zoom: INITIAL_ZOOM, duration: 500 });
|
||||
}
|
||||
geolocation.setTracking(false);
|
||||
});
|
||||
|
||||
geolocation.setTracking(true);
|
||||
};
|
||||
|
||||
// 包装 handleLocate,点击后禁用按钮一段时间
|
||||
const onLocateClick = () => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
() => {
|
||||
handleLocate();
|
||||
},
|
||||
(error) => {
|
||||
console.log(error.message);
|
||||
setLocateDisabled(true); // 定位失败后禁用按钮
|
||||
// alert("定位失败,将使用默认位置。");
|
||||
}
|
||||
);
|
||||
const view = map.getView();
|
||||
view.fit(config.mapExtent, { duration: 500 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute right-4 bottom-8">
|
||||
<div className="w-8 h-26 flex flex-col gap-2 items-center">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-8 h-8 bg-gray-50 flex items-center justify-center rounded-xl drop-shadow-xl shadow-black",
|
||||
locateDisabled && "text-gray-300"
|
||||
)}
|
||||
>
|
||||
<div className="w-8 h-8 bg-gray-50 flex items-center justify-center rounded-xl drop-shadow-xl shadow-black">
|
||||
<button
|
||||
className="w-6 h-6 flex items-center justify-center rounded-xl hover:bg-white transition-all duration-100"
|
||||
onClick={onLocateClick}
|
||||
disabled={locateDisabled}
|
||||
className="w-6 h-6 flex items-center justify-center rounded-xl hover:bg-white transition-all duration-100"
|
||||
onClick={handleFitScreen}
|
||||
>
|
||||
<GpsFixedRoundedIcon fontSize="small" />
|
||||
<FitScreenIcon fontSize="small" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-8 h-16 flex flex-col items-center justify-center bg-gray-50 rounded-xl drop-shadow-xl shadow-black">
|
||||
|
||||
@@ -24,6 +24,7 @@ import { bearing } from "@turf/turf";
|
||||
import { Deck } from "@deck.gl/core";
|
||||
import { TextLayer } from "@deck.gl/layers";
|
||||
import { TripsLayer } from "@deck.gl/geo-layers";
|
||||
import { tr } from "date-fns/locale";
|
||||
|
||||
// 创建自定义Layer类来包装deck.gl
|
||||
class DeckLayer extends Layer {
|
||||
@@ -89,8 +90,9 @@ const MapComponent: React.FC = () => {
|
||||
let showPipeText = true; // 控制管道文本显示
|
||||
let junctionText = "pressure";
|
||||
let pipeText = "flow";
|
||||
let animate = false; // 控制是否动画
|
||||
const isAnimating = useRef(false); // 添加动画控制标志
|
||||
|
||||
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
|
||||
// 防抖更新函数
|
||||
const debouncedUpdateData = useRef(
|
||||
debounce(() => {
|
||||
@@ -194,7 +196,7 @@ const MapComponent: React.FC = () => {
|
||||
})
|
||||
);
|
||||
// 属性为 flow 时启动动画
|
||||
if (pipeProperties === "flow") {
|
||||
if (pipeProperties === "flow" && animate) {
|
||||
isAnimating.current = true;
|
||||
} else {
|
||||
isAnimating.current = false;
|
||||
@@ -397,6 +399,13 @@ const MapComponent: React.FC = () => {
|
||||
padding: [50, 50, 50, 50], // 添加一些内边距
|
||||
duration: 1000, // 动画持续时间
|
||||
});
|
||||
// 监听缩放变化
|
||||
map.getView().on("change", () => {
|
||||
setTimeout(() => {
|
||||
const zoom = map.getView().getZoom() || 0;
|
||||
setCurrentZoom(zoom);
|
||||
}, 0);
|
||||
});
|
||||
// 初始化 deck.gl
|
||||
const deck = new Deck({
|
||||
initialViewState: {
|
||||
@@ -439,6 +448,7 @@ const MapComponent: React.FC = () => {
|
||||
getTextAnchor: "middle",
|
||||
getAlignmentBaseline: "center",
|
||||
getPixelOffset: [0, -10],
|
||||
visible: currentZoom >= 15 && currentZoom <= 24,
|
||||
// --- 修改以下属性 ---
|
||||
// characterSet: "auto",
|
||||
// outlineWidth: 4,
|
||||
@@ -458,6 +468,7 @@ const MapComponent: React.FC = () => {
|
||||
getPixelOffset: [0, -8],
|
||||
getTextAnchor: "middle",
|
||||
getAlignmentBaseline: "bottom",
|
||||
visible: currentZoom >= 15 && currentZoom <= 24,
|
||||
// --- 修改以下属性 ---
|
||||
// characterSet: "auto",
|
||||
// outlineWidth: 5,
|
||||
|
||||
@@ -3,6 +3,7 @@ import Zoom from "./Controls/Zoom";
|
||||
import BaseLayers from "./Controls/BaseLayers";
|
||||
import MapToolbar from "./Controls/Toolbar";
|
||||
import ScaleLine from "./Controls/ScaleLine";
|
||||
import LayerControl from "./Controls/LayerControl";
|
||||
|
||||
const MapTools = () => {
|
||||
return (
|
||||
@@ -11,6 +12,7 @@ const MapTools = () => {
|
||||
<ScaleLine />
|
||||
<BaseLayers />
|
||||
<MapToolbar />
|
||||
<LayerControl />
|
||||
{/* 继续添加其他自定义控件 */}
|
||||
</>
|
||||
);
|
||||
|
||||
BIN
src/app/icon.ico
BIN
src/app/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
Reference in New Issue
Block a user