完善Toolbar和HistoryDataPanel的交互函数

This commit is contained in:
JIANG
2025-12-16 18:30:47 +08:00
parent 05f8e500d6
commit 3a97e01dda
3 changed files with 164 additions and 90 deletions

View File

@@ -90,7 +90,7 @@ const fetchFromBackend = async (
const end_time = dayjs(range.to).toISOString(); const end_time = dayjs(range.to).toISOString();
// 监测值数据接口 // 监测值数据接口
const monitoredDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`; const rawDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=raw_value&start_time=${start_time}&end_time=${end_time}`;
// 清洗数据接口 // 清洗数据接口
const cleanedDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`; const cleanedDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`;
// 模拟数据接口 // 模拟数据接口
@@ -101,32 +101,32 @@ const fetchFromBackend = async (
try { try {
if (type === "none") { if (type === "none") {
// 查询清洗值和监测值 // 查询清洗值和监测值
const [cleanedRes, monitoredRes] = await Promise.all([ const [cleanedRes, rawRes] = await Promise.all([
fetch(cleanedDataUrl) fetch(cleanedDataUrl)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.catch(() => null), .catch(() => null),
fetch(monitoredDataUrl) fetch(rawDataUrl)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.catch(() => null), .catch(() => null),
]); ]);
const cleanedData = transformBackendData(cleanedRes, deviceIds); const cleanedData = transformBackendData(cleanedRes, deviceIds);
const monitoredData = transformBackendData(monitoredRes, deviceIds); const rawData = transformBackendData(rawRes, deviceIds);
return mergeTimeSeriesData( return mergeTimeSeriesData(
cleanedData, cleanedData,
monitoredData, rawData,
deviceIds, deviceIds,
"clean", "clean",
"monitored" "raw"
); );
} else if (type === "scheme") { } else if (type === "scheme") {
// 查询策略模拟值、清洗值和监测值 // 查询策略模拟值、清洗值和监测值
const [cleanedRes, monitoredRes, schemeSimRes] = await Promise.all([ const [cleanedRes, rawRes, schemeSimRes] = await Promise.all([
fetch(cleanedDataUrl) fetch(cleanedDataUrl)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.catch(() => null), .catch(() => null),
fetch(monitoredDataUrl) fetch(rawDataUrl)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.catch(() => null), .catch(() => null),
fetch(schemeSimulationDataUrl) fetch(schemeSimulationDataUrl)
@@ -135,14 +135,14 @@ const fetchFromBackend = async (
]); ]);
const cleanedData = transformBackendData(cleanedRes, deviceIds); const cleanedData = transformBackendData(cleanedRes, deviceIds);
const monitoredData = transformBackendData(monitoredRes, deviceIds); const rawData = transformBackendData(rawRes, deviceIds);
const schemeSimData = transformBackendData(schemeSimRes, deviceIds); const schemeSimData = transformBackendData(schemeSimRes, deviceIds);
// 合并三组数据 // 合并三组数据
const timeMap = new Map<string, Record<string, number | null>>(); const timeMap = new Map<string, Record<string, number | null>>();
[cleanedData, monitoredData, schemeSimData].forEach((data, index) => { [cleanedData, rawData, schemeSimData].forEach((data, index) => {
const suffix = ["clean", "monitored", "scheme_sim"][index]; const suffix = ["clean", "raw", "scheme_sim"][index];
data.forEach((point) => { data.forEach((point) => {
if (!timeMap.has(point.timestamp)) { if (!timeMap.has(point.timestamp)) {
timeMap.set(point.timestamp, {}); timeMap.set(point.timestamp, {});
@@ -172,11 +172,11 @@ const fetchFromBackend = async (
return result; return result;
} else { } else {
// realtime: 查询模拟值、清洗值和监测值 // realtime: 查询模拟值、清洗值和监测值
const [cleanedRes, monitoredRes, simulationRes] = await Promise.all([ const [cleanedRes, rawRes, simulationRes] = await Promise.all([
fetch(cleanedDataUrl) fetch(cleanedDataUrl)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.catch(() => null), .catch(() => null),
fetch(monitoredDataUrl) fetch(rawDataUrl)
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.catch(() => null), .catch(() => null),
fetch(simulationDataUrl) fetch(simulationDataUrl)
@@ -185,14 +185,14 @@ const fetchFromBackend = async (
]); ]);
const cleanedData = transformBackendData(cleanedRes, deviceIds); const cleanedData = transformBackendData(cleanedRes, deviceIds);
const monitoredData = transformBackendData(monitoredRes, deviceIds); const rawData = transformBackendData(rawRes, deviceIds);
const simulationData = transformBackendData(simulationRes, deviceIds); const simulationData = transformBackendData(simulationRes, deviceIds);
// 合并三组数据 // 合并三组数据
const timeMap = new Map<string, Record<string, number | null>>(); const timeMap = new Map<string, Record<string, number | null>>();
[cleanedData, monitoredData, simulationData].forEach((data, index) => { [cleanedData, rawData, simulationData].forEach((data, index) => {
const suffix = ["clean", "monitored", "sim"][index]; const suffix = ["clean", "raw", "sim"][index];
data.forEach((point) => { data.forEach((point) => {
if (!timeMap.has(point.timestamp)) { if (!timeMap.has(point.timestamp)) {
timeMap.set(point.timestamp, {}); timeMap.set(point.timestamp, {});
@@ -351,7 +351,7 @@ const buildDataset = (
}; };
deviceIds.forEach((id) => { deviceIds.forEach((id) => {
["clean", "monitored", "sim", "scheme_sim"].forEach((suffix) => { ["clean", "raw", "sim", "scheme_sim"].forEach((suffix) => {
const key = `${id}_${suffix}`; const key = `${id}_${suffix}`;
const value = point.values[key]; const value = point.values[key];
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
@@ -520,23 +520,49 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
]; ];
const dynamic = (() => { const dynamic = (() => {
return deviceIds.map<GridColDef>((id) => ({ const cols: GridColDef[] = [];
field: id,
headerName: deviceLabels?.[id] ?? id, deviceIds.forEach((id) => {
minWidth: 140, const deviceName = deviceLabels?.[id] ?? id;
flex: 1,
valueFormatter: (value: any) => { // 为每个设备的每种数据类型创建列
if (value === null || value === undefined) return "--"; const suffixes = [
if (Number.isFinite(Number(value))) { { key: "clean", name: "清洗值" },
return Number(value).toFixed(fractionDigits); { key: "raw", name: "监测值" },
{ key: "sim", name: "模拟值" },
{ key: "scheme_sim", name: "策略模拟值" },
];
suffixes.forEach(({ key, name }) => {
const fieldKey = `${id}_${key}`;
// 检查是否有该字段的数据
const hasData = dataset.some(
(item) => item[fieldKey] !== null && item[fieldKey] !== undefined
);
if (hasData) {
cols.push({
field: fieldKey,
headerName: `${deviceName} (${name})`,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
});
} }
return String(value); });
}, });
}));
return cols;
})(); })();
return [...base, ...dynamic]; return [...base, ...dynamic];
}, [deviceIds, deviceLabels, fractionDigits, selectedSource]); }, [deviceIds, deviceLabels, fractionDigits, dataset]);
const rows = useMemo( const rows = useMemo(
() => () =>
@@ -597,48 +623,46 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const getSeries = () => { const getSeries = () => {
return deviceIds.flatMap((id, index) => { return deviceIds.flatMap((id, index) => {
const series = []; const series = [];
["clean", "monitored", "sim", "scheme_sim"].forEach( ["clean", "raw", "sim", "scheme_sim"].forEach((suffix, sIndex) => {
(suffix, sIndex) => { const key = `${id}_${suffix}`;
const key = `${id}_${suffix}`; const hasData = dataset.some(
const hasData = dataset.some( (item) => item[key] !== null && item[key] !== undefined
(item) => item[key] !== null && item[key] !== undefined );
); if (hasData) {
if (hasData) { const displayName =
const displayName = suffix === "clean"
suffix === "clean" ? "清洗值"
? "清洗值" : suffix === "raw"
: suffix === "monitored" ? "监测值"
? "监测值" : suffix === "sim"
: suffix === "sim" ? "模拟"
? "模拟" : "策略模拟";
: "策略模拟";
series.push({ series.push({
name: `${deviceLabels?.[id] ?? id} (${displayName})`, name: `${deviceLabels?.[id] ?? id} (${displayName})`,
type: "line", type: "line",
symbol: "none", symbol: "none",
sampling: "lttb", sampling: "lttb",
itemStyle: { itemStyle: {
color: colors[(index * 4 + sIndex) % colors.length], color: colors[(index * 4 + sIndex) % colors.length],
}, },
data: dataset.map((item) => item[key]), data: dataset.map((item) => item[key]),
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, offset: 0,
color: colors[(index * 4 + sIndex) % colors.length], color: colors[(index * 4 + sIndex) % colors.length],
}, },
{ {
offset: 1, offset: 1,
color: "rgba(255, 255, 255, 0)", color: "rgba(255, 255, 255, 0)",
}, },
]), ]),
opacity: 0.3, opacity: 0.3,
}, },
}); });
}
} }
); });
// 如果没有任何数据则使用fallback // 如果没有任何数据则使用fallback
if (series.length === 0) { if (series.length === 0) {
series.push({ series.push({
@@ -955,7 +979,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
color="warning.main" color="warning.main"
sx={{ mt: 1, display: "block" }} sx={{ mt: 1, display: "block" }}
> >
</Typography> </Typography>
)} )}
{error && ( {error && (

View File

@@ -43,6 +43,7 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
const [showHistoryPanel, setShowHistoryPanel] = useState<boolean>(false); const [showHistoryPanel, setShowHistoryPanel] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] = const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null); useState<VectorLayer<VectorSource> | null>(null);
const [featureId, setFeatureId] = useState<string>("");
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式 // 样式状态管理 - 在 Toolbar 中管理,带有默认样式
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([ const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
@@ -186,14 +187,30 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
); );
// 添加矢量属性查询事件监听器 // 添加矢量属性查询事件监听器
useEffect(() => { useEffect(() => {
if (!activeTools.includes("info") || !map) return; if (!map) return;
map.on("click", handleMapClickSelectFeatures); // 监听 info 或 history 工具激活时添加
if (activeTools.includes("info") || activeTools.includes("history")) {
map.on("click", handleMapClickSelectFeatures);
return () => { return () => {
map.un("click", handleMapClickSelectFeatures); map.un("click", handleMapClickSelectFeatures);
}; };
}
}, [activeTools, map, handleMapClickSelectFeatures]); }, [activeTools, map, handleMapClickSelectFeatures]);
// 监听 highlightFeature 变化,更新 featureId
useEffect(() => {
if (highlightFeature) {
const id = highlightFeature.getProperties().id;
if (id) {
setFeatureId(id);
}
console.log("高亮要素 ID:", id);
} else {
setFeatureId("");
}
}, [highlightFeature]);
// 处理工具栏按钮点击事件 // 处理工具栏按钮点击事件
const handleToolClick = (tool: string) => { const handleToolClick = (tool: string) => {
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭 // 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
@@ -652,7 +669,14 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
setLayerStyleStates={setLayerStyleStates} setLayerStyleStates={setLayerStyleStates}
/> />
)} )}
{showHistoryPanel && <HistoryDataPanel deviceIds={[]} />} {showHistoryPanel && (
<HistoryDataPanel
deviceIds={featureId ? [featureId] : []}
scheme_type="burst_Analysis"
scheme_name={schemeName}
type={queryType as "realtime" | "scheme" | "none"}
/>
)}
{/* 图例显示 */} {/* 图例显示 */}
{activeLegendConfigs.length > 0 && ( {activeLegendConfigs.length > 0 && (

View File

@@ -648,24 +648,50 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
})); }));
} }
} else { } else {
return deviceIds.map<GridColDef>((id) => ({ // 非清洗模式:显示所有设备的所有有数据的列
field: id, const cols: GridColDef[] = [];
headerName: deviceLabels?.[id] ?? id,
minWidth: 140, deviceIds.forEach((id) => {
flex: 1, const deviceName = deviceLabels?.[id] ?? id;
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--"; // 为每个设备的每种数据类型创建列
if (Number.isFinite(Number(value))) { const suffixes = [
return Number(value).toFixed(fractionDigits); { key: 'raw', name: '原始值' },
{ key: 'clean', name: '清洗值' },
{ key: 'sim', name: '模拟值' }
];
suffixes.forEach(({ key, name }) => {
const fieldKey = `${id}_${key}`;
// 检查是否有该字段的数据
const hasData = dataset.some(
(item) => item[fieldKey] !== null && item[fieldKey] !== undefined
);
if (hasData) {
cols.push({
field: fieldKey,
headerName: `${deviceName} (${name})`,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
});
} }
return String(value); });
}, });
}));
return cols;
} }
})(); })();
return [...base, ...dynamic]; return [...base, ...dynamic];
}, [deviceIds, deviceLabels, fractionDigits, showCleaning, selectedSource]); }, [deviceIds, deviceLabels, fractionDigits, showCleaning, selectedSource, dataset]);
const rows = useMemo( const rows = useMemo(
() => () =>