完成时间轴前后端数据连通
This commit is contained in:
@@ -300,7 +300,7 @@ const DrawPanel: React.FC = () => {
|
||||
const isSaveDisabled = drawnFeatures.length === 0;
|
||||
|
||||
return (
|
||||
<div className="absolute top-20 left-4 bg-white p-1 rounded-xl shadow-lg flex flex-col opacity-85 hover:opacity-100 transition-opacity">
|
||||
<div className="absolute top-20 left-4 bg-white p-1 rounded-xl shadow-lg flex flex-col opacity-85 hover:opacity-100 transition-opacity z-10">
|
||||
<div className="flex">
|
||||
<ToolbarButton
|
||||
icon={<BackHandOutlinedIcon />}
|
||||
|
||||
@@ -20,7 +20,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
}) => {
|
||||
if (!id) {
|
||||
return (
|
||||
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px]">
|
||||
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px] z-10">
|
||||
<h3 className="text-lg font-semibold mb-4">属性面板</h3>
|
||||
<p className="text-gray-500">请选择一个要素以查看其属性。</p>
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px]">
|
||||
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px] z-10">
|
||||
<h3 className="text-lg font-semibold mb-4">属性面板</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
|
||||
@@ -36,9 +36,9 @@ const Scale: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-0 right-0 flex col-auto px-2 bg-white bg-opacity-70 text-black rounded-tl shadow-md text-sm">
|
||||
<div className="px-1">Zoom Level: {zoomLevel.toFixed(1)}</div>
|
||||
<div className="px-1">缩放: {zoomLevel.toFixed(1)}</div>
|
||||
<div className="px-1">
|
||||
Coordinates: {coordinates[0]}, {coordinates[1]}
|
||||
坐标: {coordinates[0]}, {coordinates[1]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,11 +19,14 @@ import {
|
||||
|
||||
// 导入OpenLayers样式相关模块
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import { useMap } from "../MapComponent";
|
||||
import { useData, useMap } from "../MapComponent";
|
||||
|
||||
import StyleLegend, { LegendStyleConfig } from "./StyleLegend";
|
||||
import { FlatStyleLike } from "ol/style/flat";
|
||||
|
||||
import { calculateClassification } from "@utils/breaks_classification";
|
||||
import { parseColor } from "@utils/parseColor";
|
||||
|
||||
interface StyleConfig {
|
||||
property: string;
|
||||
classificationMethod: string; // 分类方法
|
||||
@@ -96,6 +99,22 @@ const CLASSIFICATION_METHODS = [
|
||||
|
||||
const StyleEditorPanel: React.FC = () => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
if (!data) {
|
||||
return <div>Loading...</div>; // 或其他占位符
|
||||
}
|
||||
const {
|
||||
junctionData,
|
||||
pipeData,
|
||||
setShowJunctionText,
|
||||
setShowPipeText,
|
||||
setJunctionText,
|
||||
setPipeText,
|
||||
} = data;
|
||||
|
||||
const [applyJunctionStyle, setApplyJunctionStyle] = useState(false);
|
||||
const [applyPipeStyle, setApplyPipeStyle] = useState(false);
|
||||
|
||||
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
|
||||
const [selectedRenderLayer, setSelectedRenderLayer] =
|
||||
useState<WebGLVectorTileLayer>();
|
||||
@@ -134,75 +153,10 @@ const StyleEditorPanel: React.FC = () => {
|
||||
breaks: [],
|
||||
}
|
||||
);
|
||||
|
||||
// 样式状态管理 - 存储多个图层的样式状态
|
||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
// 通用颜色解析函数
|
||||
const parseColor = useCallback((color: string) => {
|
||||
// 解析 rgba 格式的颜色
|
||||
const match = color.match(
|
||||
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
||||
);
|
||||
if (match) {
|
||||
return {
|
||||
r: parseInt(match[1], 10),
|
||||
g: parseInt(match[2], 10),
|
||||
b: parseInt(match[3], 10),
|
||||
// 如果没有 alpha 值,默认为 1
|
||||
a: match[4] ? parseFloat(match[4]) : 1,
|
||||
};
|
||||
}
|
||||
// 如果还是十六进制格式,保持原来的解析方式
|
||||
const hex = color.replace("#", "");
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16),
|
||||
g: parseInt(hex.slice(2, 4), 16),
|
||||
b: parseInt(hex.slice(4, 6), 16),
|
||||
};
|
||||
}, []);
|
||||
// 获取数据分段分类结果
|
||||
const fetchClassification = async (
|
||||
layer_name: string,
|
||||
prop: string,
|
||||
n_classes: number,
|
||||
algorithm: string
|
||||
) => {
|
||||
if (!algorithm) {
|
||||
algorithm = "pretty_breaks"; // 默认算法
|
||||
}
|
||||
const response = await fetch("http://localhost:8000/jenks-classification", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
layer_name: layer_name, // 图层名称
|
||||
prop: prop, // 属性名称
|
||||
n_classes: n_classes, // 分段数
|
||||
algorithm: algorithm,
|
||||
// algorithm: "pretty_breaks",
|
||||
// algorithm: "hybrid_jenks"
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("API 请求失败:", response.status, response.statusText);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const breaks = data.breaks; // 从响应对象中提取 breaks 数组
|
||||
// console.log(breaks);
|
||||
// 验证返回的数据
|
||||
if (!Array.isArray(breaks) || breaks.length === 0) {
|
||||
console.error("API 返回的 breaks 不是有效数组:", breaks);
|
||||
return false;
|
||||
}
|
||||
|
||||
return breaks;
|
||||
};
|
||||
// 颜色方案选择
|
||||
const [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
|
||||
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
|
||||
@@ -232,21 +186,45 @@ const StyleEditorPanel: React.FC = () => {
|
||||
[gradientPaletteIndex, parseColor]
|
||||
);
|
||||
// 应用分类样式
|
||||
const applyStyle = (breaks?: number[]) => {
|
||||
const setStyleState = (layer: any) => {
|
||||
if (
|
||||
layer.get("value") !== undefined &&
|
||||
styleConfig.property !== undefined
|
||||
) {
|
||||
// 更新文字标签设置
|
||||
if (layer.get("value") === "junctions") {
|
||||
if (setJunctionText && setShowJunctionText) {
|
||||
setJunctionText(styleConfig.property);
|
||||
setShowJunctionText(styleConfig.showLabels);
|
||||
setApplyJunctionStyle(true);
|
||||
}
|
||||
}
|
||||
if (layer.get("value") === "pipes") {
|
||||
if (setPipeText && setShowPipeText) {
|
||||
setPipeText(styleConfig.property);
|
||||
setShowPipeText(styleConfig.showLabels);
|
||||
setApplyPipeStyle(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const applyStyle = (layerId: string, breaks?: number[]) => {
|
||||
// 使用传入的 breaks 数据
|
||||
if (!breaks) {
|
||||
if (!breaks || breaks.length === 0) {
|
||||
console.warn("没有有效的 breaks 数据");
|
||||
return;
|
||||
}
|
||||
if (!selectedRenderLayer || !styleConfig.property) return;
|
||||
const styleConfig = layerStyleStates.find(
|
||||
(s) => s.layerId === layerId
|
||||
)?.styleConfig;
|
||||
const selectedRenderLayer = renderLayers.find(
|
||||
(l) => l.get("id") === layerId
|
||||
);
|
||||
if (!selectedRenderLayer || !styleConfig?.property) return;
|
||||
const layerType: string = selectedRenderLayer?.get("type");
|
||||
const source = selectedRenderLayer.getSource();
|
||||
if (!source) return;
|
||||
|
||||
if (breaks.length === 0) {
|
||||
console.log("没有有效的 breaks 数据,无法应用样式");
|
||||
return;
|
||||
}
|
||||
const breaksLength = breaks.length;
|
||||
// 根据 breaks 计算每个分段的颜色,线条粗细
|
||||
const colors: string[] =
|
||||
@@ -385,8 +363,45 @@ const StyleEditorPanel: React.FC = () => {
|
||||
setLayerStyleStates((prev) =>
|
||||
prev.filter((state) => state.layerId !== layerId)
|
||||
);
|
||||
// 重置样式应用状态
|
||||
if (layerId === "junctions") {
|
||||
setApplyJunctionStyle(false);
|
||||
} else if (layerId === "pipes") {
|
||||
setApplyPipeStyle(false);
|
||||
}
|
||||
}
|
||||
}, [selectedRenderLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (applyJunctionStyle && junctionData.length > 0) {
|
||||
// 应用节点样式
|
||||
const junctionStyleConfigState = layerStyleStates.find(
|
||||
(s) => s.layerId === "junctions"
|
||||
);
|
||||
if (!junctionStyleConfigState) return;
|
||||
const segments = junctionStyleConfigState?.styleConfig.segments;
|
||||
const breaks = calculateClassification(
|
||||
junctionData,
|
||||
segments,
|
||||
styleConfig.classificationMethod
|
||||
);
|
||||
applyStyle(junctionStyleConfigState.layerId, breaks);
|
||||
}
|
||||
if (applyPipeStyle && pipeData.length > 0) {
|
||||
// 应用管道样式
|
||||
const pipeStyleConfigState = layerStyleStates.find(
|
||||
(s) => s.layerId === "pipes"
|
||||
);
|
||||
if (!pipeStyleConfigState) return;
|
||||
const segments = pipeStyleConfigState?.styleConfig.segments;
|
||||
const breaks = calculateClassification(
|
||||
pipeData,
|
||||
segments,
|
||||
styleConfig.classificationMethod
|
||||
);
|
||||
applyStyle(pipeStyleConfigState.layerId, breaks);
|
||||
}
|
||||
}, [junctionData, pipeData, applyJunctionStyle, applyPipeStyle]);
|
||||
// 样式状态管理功能
|
||||
// 保存当前图层的样式状态
|
||||
const saveLayerStyle = useCallback(
|
||||
@@ -436,8 +451,9 @@ const StyleEditorPanel: React.FC = () => {
|
||||
const updateVisibleLayers = () => {
|
||||
const layers = map.getAllLayers();
|
||||
// 筛选矢量瓦片图层
|
||||
const webGLVectorTileLayers = layers.filter((layer) =>
|
||||
layer.get("value")
|
||||
const webGLVectorTileLayers = layers.filter(
|
||||
(layer) =>
|
||||
layer.get("value") === "junctions" || layer.get("value") === "pipes" // 暂时只处理这两个图层
|
||||
) as WebGLVectorTileLayer[];
|
||||
|
||||
setRenderLayers(webGLVectorTileLayers);
|
||||
@@ -505,6 +521,7 @@ const StyleEditorPanel: React.FC = () => {
|
||||
}));
|
||||
}
|
||||
}, [styleConfig.colorType]);
|
||||
|
||||
// 获取所有激活的图例配置
|
||||
const getActiveLegendConfigs = useCallback(() => {
|
||||
return layerStyleStates
|
||||
@@ -803,7 +820,7 @@ const StyleEditorPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-20 left-4 bg-white p-4 rounded-xl shadow-lg opacity-95 hover:opacity-100 transition-opacity w-80">
|
||||
<div className="absolute top-20 left-4 bg-white p-4 rounded-xl shadow-lg opacity-95 hover:opacity-100 transition-opacity w-80 z-10">
|
||||
{/* 图层选择 */}
|
||||
<FormControl variant="standard" fullWidth margin="dense">
|
||||
<InputLabel>选择图层</InputLabel>
|
||||
@@ -948,25 +965,8 @@ const StyleEditorPanel: React.FC = () => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
// 获取分段数据后应用样式
|
||||
if (
|
||||
selectedRenderLayer &&
|
||||
selectedRenderLayer.get("value") !== undefined &&
|
||||
styleConfig.property !== undefined &&
|
||||
styleConfig.segments !== undefined
|
||||
) {
|
||||
const newBreaks = await fetchClassification(
|
||||
selectedRenderLayer?.get("value"),
|
||||
styleConfig.property,
|
||||
styleConfig.segments,
|
||||
styleConfig.classificationMethod
|
||||
);
|
||||
if (newBreaks) {
|
||||
applyStyle(newBreaks);
|
||||
// setShowLegend(true); // 应用样式后显示图例
|
||||
}
|
||||
}
|
||||
onClick={() => {
|
||||
setStyleState(selectedRenderLayer);
|
||||
}}
|
||||
disabled={!selectedRenderLayer || !styleConfig.property}
|
||||
startIcon={<ApplyIcon />}
|
||||
@@ -989,7 +989,7 @@ const StyleEditorPanel: React.FC = () => {
|
||||
</div>
|
||||
{/* 显示多图层图例 */}
|
||||
{getActiveLegendConfigs().length > 0 && (
|
||||
<div className="fixed bottom-35 right-4 flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
||||
<div className=" absolute bottom-40 right-4 shadow-lg flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
||||
<div className="flex flex-row gap-3">
|
||||
{getActiveLegendConfigs().map((config, index) => (
|
||||
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
||||
|
||||
@@ -27,7 +27,7 @@ const StyleLegend: React.FC<LegendStyleConfig> = ({
|
||||
return (
|
||||
<Box
|
||||
key={layerId}
|
||||
className="bg-white p-3 rounded-xl shadow-lg max-w-xs opacity-95"
|
||||
className="bg-white p-3 rounded-xl shadow-lg max-w-xs opacity-95 transition-opacity duration-300 hover:opacity-100"
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
{layerName} - {property}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Slider,
|
||||
Typography,
|
||||
Paper,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
@@ -22,26 +21,18 @@ 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";
|
||||
import { useData } from "../MapComponent";
|
||||
import { config } from "@/config/config";
|
||||
|
||||
interface TimelineProps {
|
||||
onTimeChange?: (time: string) => void;
|
||||
onDateChange?: (date: Date) => void;
|
||||
onPlay?: () => void;
|
||||
onPause?: () => void;
|
||||
onStop?: () => void;
|
||||
onRefresh?: () => void;
|
||||
onFetch?: () => void;
|
||||
}
|
||||
const backendUrl = config.backendUrl;
|
||||
const Timeline: React.FC = () => {
|
||||
const data = useData();
|
||||
if (!data) {
|
||||
return <div>Loading...</div>; // 或其他占位符
|
||||
}
|
||||
const { setJunctionDataState, setPipeDataState, junctionText, pipeText } =
|
||||
data;
|
||||
|
||||
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);
|
||||
@@ -51,6 +42,114 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
// 添加缓存引用
|
||||
const cacheRef = useRef<
|
||||
Map<string, { nodeRecords: any[]; linkRecords: any[] }>
|
||||
>(new Map());
|
||||
// 添加防抖引用
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const fetchFrameData = async (queryTime: Date) => {
|
||||
const query_time = queryTime.toISOString();
|
||||
const cacheKey = query_time;
|
||||
|
||||
// 检查缓存
|
||||
if (cacheRef.current.has(cacheKey)) {
|
||||
const { nodeRecords, linkRecords } = cacheRef.current.get(cacheKey)!;
|
||||
// 使用缓存数据更新状态
|
||||
updateDataStates(nodeRecords, linkRecords);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 定义需要查询的属性
|
||||
const junctionProperties = junctionText;
|
||||
const pipeProperties = pipeText;
|
||||
if (
|
||||
!junctionProperties ||
|
||||
!pipeProperties ||
|
||||
junctionProperties === "" ||
|
||||
pipeProperties === ""
|
||||
) {
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
"Query Time:",
|
||||
queryTime.toLocaleDateString() + " " + queryTime.toLocaleTimeString()
|
||||
);
|
||||
// 同时查询节点和管道数据
|
||||
const [nodeResponse, linkResponse] = await Promise.all([
|
||||
fetch(
|
||||
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
|
||||
),
|
||||
fetch(
|
||||
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
|
||||
),
|
||||
]);
|
||||
|
||||
const nodeRecords = await nodeResponse.json();
|
||||
const linkRecords = await linkResponse.json();
|
||||
|
||||
// 缓存数据
|
||||
cacheRef.current.set(cacheKey, {
|
||||
nodeRecords: nodeRecords.results,
|
||||
linkRecords: linkRecords.results,
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
updateDataStates(nodeRecords.results, linkRecords.results);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 提取更新状态的逻辑
|
||||
const updateDataStates = (nodeResults: any[], linkResults: any[]) => {
|
||||
const junctionProperties = junctionText;
|
||||
const pipeProperties = pipeText;
|
||||
|
||||
// 将 nodeRecords 转换为 Map 以提高查找效率
|
||||
const nodeMap: Map<string, any> = new Map(
|
||||
nodeResults.map((r: any) => [r.ID, r])
|
||||
);
|
||||
// 将 linkRecords 转换为 Map 以提高查找效率
|
||||
const linkMap: Map<string, any> = new Map(
|
||||
linkResults.map((r: any) => [r.ID, r])
|
||||
);
|
||||
|
||||
// 更新junctionData
|
||||
setJunctionDataState((prev: any[]) =>
|
||||
prev.map((j) => {
|
||||
const record = nodeMap.get(j.id);
|
||||
if (record) {
|
||||
return {
|
||||
...j,
|
||||
[junctionProperties]: record.value,
|
||||
};
|
||||
}
|
||||
return j;
|
||||
})
|
||||
);
|
||||
|
||||
// 更新pipeData
|
||||
setPipeDataState((prev: any[]) =>
|
||||
prev.map((p) => {
|
||||
const record = linkMap.get(p.id);
|
||||
if (record) {
|
||||
return {
|
||||
...p,
|
||||
flowFlag: pipeProperties === "flow" && record.value < 0 ? -1 : 1,
|
||||
path:
|
||||
pipeProperties === "flow" && record.value < 0 && p.flowFlag > 0
|
||||
? [...p.path].reverse()
|
||||
: p.path,
|
||||
[pipeProperties]: record.value,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// 时间刻度数组 (每5分钟一个刻度)
|
||||
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
||||
@@ -67,14 +166,22 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function currentTimeToDate(selectedDate: Date, minutes: number): Date {
|
||||
const date = new Date(selectedDate);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
date.setHours(hours, mins, 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
// 播放时间间隔选项
|
||||
const intervalOptions = [
|
||||
{ value: 1000, label: "1秒" },
|
||||
// { 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 小时" },
|
||||
@@ -88,79 +195,73 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
(event: Event, newValue: number | number[]) => {
|
||||
const value = Array.isArray(newValue) ? newValue[0] : newValue;
|
||||
setSliderValue(value);
|
||||
setCurrentTime(value);
|
||||
onTimeChange?.(formatTime(value));
|
||||
// 防抖设置currentTime,避免频繁触发数据获取
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setCurrentTime(value);
|
||||
}, 300); // 300ms 防抖延迟
|
||||
},
|
||||
[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]);
|
||||
}, [isPlaying, playInterval]);
|
||||
|
||||
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 handleDateChange = useCallback((newDate: Date | null) => {
|
||||
if (newDate) {
|
||||
setSelectedDate(newDate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 播放间隔改变处理
|
||||
const handleIntervalChange = useCallback(
|
||||
@@ -175,25 +276,33 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
setCurrentTime((prev) => {
|
||||
const next = prev >= 1435 ? 0 : prev + 5;
|
||||
setSliderValue(next);
|
||||
onTimeChange?.(formatTime(next));
|
||||
return next;
|
||||
});
|
||||
}, newInterval);
|
||||
}
|
||||
},
|
||||
[isPlaying, onTimeChange]
|
||||
[isPlaying]
|
||||
);
|
||||
// 计算时间段改变处理
|
||||
const handleCalculatedIntervalChange = useCallback((event: any) => {
|
||||
const newInterval = event.target.value;
|
||||
setCalculatedInterval(newInterval);
|
||||
}, []);
|
||||
// 组件卸载时清理定时器
|
||||
|
||||
// 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据
|
||||
useEffect(() => {
|
||||
fetchFrameData(currentTimeToDate(selectedDate, currentTime));
|
||||
}, [currentTime, selectedDate]);
|
||||
|
||||
// 组件卸载时清理定时器和防抖
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -224,7 +333,13 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
<DatePicker
|
||||
label="模拟数据日期选择"
|
||||
value={selectedDate}
|
||||
onChange={(newValue) => handleDateChange(newValue)}
|
||||
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 } }}
|
||||
@@ -308,7 +423,7 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<Refresh />}
|
||||
onClick={onRefresh}
|
||||
// onClick={onRefresh}
|
||||
>
|
||||
强制计算
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user