更新历史数据组件传参,新增获取策略模拟值

This commit is contained in:
JIANG
2025-12-16 17:43:18 +08:00
parent 15f48d0496
commit 05f8e500d6

View File

@@ -47,10 +47,19 @@ export interface TimeSeriesPoint {
export interface SCADADataPanelProps { export interface SCADADataPanelProps {
/** 选中的设备 ID 列表 */ /** 选中的设备 ID 列表 */
deviceIds: string[]; deviceIds: string[];
/** 自定义数据获取器,默认使用后端 API */ /** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */
type?: "realtime" | "scheme" | "none";
/** 策略类型 */
scheme_type?: string;
/** 策略名称 */
scheme_name?: string;
/** 自定义数据获取器,默认使用后端 API */
fetchTimeSeriesData?: ( fetchTimeSeriesData?: (
deviceIds: string[], deviceIds: string[],
range: { from: Date; to: Date } range: { from: Date; to: Date },
type?: "realtime" | "scheme" | "none",
scheme_type?: string,
scheme_name?: string
) => Promise<TimeSeriesPoint[]>; ) => Promise<TimeSeriesPoint[]>;
/** 默认展示的选项卡 */ /** 默认展示的选项卡 */
defaultTab?: "chart" | "table"; defaultTab?: "chart" | "table";
@@ -67,7 +76,10 @@ type LoadingState = "idle" | "loading" | "success" | "error";
*/ */
const fetchFromBackend = async ( const fetchFromBackend = async (
deviceIds: string[], deviceIds: string[],
range: { from: Date; to: Date } range: { from: Date; to: Date },
type: "realtime" | "scheme" | "none" = "realtime",
scheme_type?: string,
scheme_name?: string
): Promise<TimeSeriesPoint[]> => { ): Promise<TimeSeriesPoint[]> => {
if (deviceIds.length === 0) { if (deviceIds.length === 0) {
return []; return [];
@@ -76,49 +88,138 @@ const fetchFromBackend = async (
const device_ids = deviceIds.join(","); const device_ids = deviceIds.join(",");
const start_time = dayjs(range.from).toISOString(); const start_time = dayjs(range.from).toISOString();
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 cleaningDataUrl = `${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}`;
// 原始数据
const rawDataUrl = `${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 simulationDataUrl = `${config.BACKEND_URL}/timescaledb/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`; const simulationDataUrl = `${config.BACKEND_URL}/timescaledb/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`;
// 策略模拟数据接口
const schemeSimulationDataUrl = `${config.BACKEND_URL}/timescaledb/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}&scheme_type=${scheme_type}&scheme_name=${scheme_name}`;
try { try {
// 优先查询清洗数据和模拟数据 if (type === "none") {
const [cleaningRes, simulationRes] = await Promise.all([ // 查询清洗值和监测值
fetch(cleaningDataUrl) const [cleanedRes, monitoredRes] = await Promise.all([
.then((r) => (r.ok ? r.json() : null)) fetch(cleanedDataUrl)
.catch(() => null), .then((r) => (r.ok ? r.json() : null))
fetch(simulationDataUrl) .catch(() => null),
.then((r) => (r.ok ? r.json() : null)) fetch(monitoredDataUrl)
.catch(() => null), .then((r) => (r.ok ? r.json() : null))
]); .catch(() => null),
]);
const cleaningData = transformBackendData(cleaningRes, deviceIds); const cleanedData = transformBackendData(cleanedRes, deviceIds);
const simulationData = transformBackendData(simulationRes, deviceIds); const monitoredData = transformBackendData(monitoredRes, deviceIds);
// 如果清洗数据有数据,返回清洗和模拟数据
if (cleaningData.length > 0) {
return mergeTimeSeriesData( return mergeTimeSeriesData(
cleaningData, cleanedData,
simulationData, monitoredData,
deviceIds, deviceIds,
"clean", "clean",
"sim" "monitored"
); );
} else if (type === "scheme") {
// 查询策略模拟值、清洗值和监测值
const [cleanedRes, monitoredRes, schemeSimRes] = await Promise.all([
fetch(cleanedDataUrl)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
fetch(monitoredDataUrl)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
fetch(schemeSimulationDataUrl)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
]);
const cleanedData = transformBackendData(cleanedRes, deviceIds);
const monitoredData = transformBackendData(monitoredRes, deviceIds);
const schemeSimData = transformBackendData(schemeSimRes, deviceIds);
// 合并三组数据
const timeMap = new Map<string, Record<string, number | null>>();
[cleanedData, monitoredData, schemeSimData].forEach((data, index) => {
const suffix = ["clean", "monitored", "scheme_sim"][index];
data.forEach((point) => {
if (!timeMap.has(point.timestamp)) {
timeMap.set(point.timestamp, {});
}
const values = timeMap.get(point.timestamp)!;
deviceIds.forEach((deviceId) => {
const value = point.values[deviceId];
if (value !== undefined) {
values[`${deviceId}_${suffix}`] = value;
}
});
});
});
const result = Array.from(timeMap.entries()).map(
([timestamp, values]) => ({
timestamp,
values,
})
);
result.sort(
(a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
return result;
} else { } else {
// 如果清洗数据没有数据,查询原始数据,返回模拟和原始数据 // realtime: 查询模拟值、清洗值和监测值
const rawRes = await fetch(rawDataUrl) const [cleanedRes, monitoredRes, simulationRes] = await Promise.all([
.then((r) => (r.ok ? r.json() : null)) fetch(cleanedDataUrl)
.catch(() => null); .then((r) => (r.ok ? r.json() : null))
const rawData = transformBackendData(rawRes, deviceIds); .catch(() => null),
return mergeTimeSeriesData( fetch(monitoredDataUrl)
simulationData, .then((r) => (r.ok ? r.json() : null))
rawData, .catch(() => null),
deviceIds, fetch(simulationDataUrl)
"sim", .then((r) => (r.ok ? r.json() : null))
"raw" .catch(() => null),
]);
const cleanedData = transformBackendData(cleanedRes, deviceIds);
const monitoredData = transformBackendData(monitoredRes, deviceIds);
const simulationData = transformBackendData(simulationRes, deviceIds);
// 合并三组数据
const timeMap = new Map<string, Record<string, number | null>>();
[cleanedData, monitoredData, simulationData].forEach((data, index) => {
const suffix = ["clean", "monitored", "sim"][index];
data.forEach((point) => {
if (!timeMap.has(point.timestamp)) {
timeMap.set(point.timestamp, {});
}
const values = timeMap.get(point.timestamp)!;
deviceIds.forEach((deviceId) => {
const value = point.values[deviceId];
if (value !== undefined) {
values[`${deviceId}_${suffix}`] = value;
}
});
});
});
const result = Array.from(timeMap.entries()).map(
([timestamp, values]) => ({
timestamp,
values,
})
); );
result.sort(
(a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
return result;
} }
} catch (error) { } catch (error) {
console.error("[SCADADataPanel] 从后端获取数据失败:", error); console.error("[SCADADataPanel] 从后端获取数据失败:", error);
@@ -250,7 +351,7 @@ const buildDataset = (
}; };
deviceIds.forEach((id) => { deviceIds.forEach((id) => {
["raw", "clean", "sim"].forEach((suffix) => { ["clean", "monitored", "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) {
@@ -284,6 +385,9 @@ const emptyStateMessages: Record<
const SCADADataPanel: React.FC<SCADADataPanelProps> = ({ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
deviceIds, deviceIds,
type = "realtime",
scheme_type,
scheme_name,
fetchTimeSeriesData = defaultFetcher, fetchTimeSeriesData = defaultFetcher,
defaultTab = "chart", defaultTab = "chart",
fractionDigits = 2, fractionDigits = 2,
@@ -361,10 +465,16 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setError(null); setError(null);
try { try {
const { from: rangeFrom, to: rangeTo } = normalizedRange; const { from: rangeFrom, to: rangeTo } = normalizedRange;
const result = await customFetcher(deviceIds, { const result = await customFetcher(
from: rangeFrom.toDate(), deviceIds,
to: rangeTo.toDate(), {
}); from: rangeFrom.toDate(),
to: rangeTo.toDate(),
},
type,
scheme_type,
scheme_name
);
setTimeSeries(result); setTimeSeries(result);
setLoadingState("success"); setLoadingState("success");
} catch (err) { } catch (err) {
@@ -372,7 +482,15 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setLoadingState("error"); setLoadingState("error");
} }
}, },
[deviceIds, customFetcher, hasDevices, normalizedRange] [
deviceIds,
customFetcher,
hasDevices,
normalizedRange,
type,
scheme_type,
scheme_name,
]
); );
// 设备变化时自动查询 // 设备变化时自动查询
@@ -479,40 +597,49 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const getSeries = () => { const getSeries = () => {
return deviceIds.flatMap((id, index) => { return deviceIds.flatMap((id, index) => {
const series = []; const series = [];
["raw", "clean", "sim"].forEach((suffix, sIndex) => { ["clean", "monitored", "sim", "scheme_sim"].forEach(
const key = `${id}_${suffix}`; (suffix, sIndex) => {
const hasData = dataset.some( const key = `${id}_${suffix}`;
(item) => item[key] !== null && item[key] !== undefined const hasData = dataset.some(
); (item) => item[key] !== null && item[key] !== undefined
if (hasData) { );
series.push({ if (hasData) {
name: `${deviceLabels?.[id] ?? id} (${ const displayName =
suffix === "raw" ? "原始" : suffix === "clean" ? "清洗" : "模拟" suffix === "clean"
})`, ? "清洗值"
type: "line", : suffix === "monitored"
symbol: "none", ? "监测值"
sampling: "lttb", : suffix === "sim"
itemStyle: { ? "模拟"
color: colors[(index * 3 + sIndex) % colors.length], : "策略模拟";
},
data: dataset.map((item) => item[key]), series.push({
areaStyle: { name: `${deviceLabels?.[id] ?? id} (${displayName})`,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ type: "line",
{ symbol: "none",
offset: 0, sampling: "lttb",
color: colors[(index * 3 + sIndex) % colors.length], itemStyle: {
}, color: colors[(index * 4 + sIndex) % colors.length],
{ },
offset: 1, data: dataset.map((item) => item[key]),
color: "rgba(255, 255, 255, 0)", areaStyle: {
}, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
]), {
opacity: 0.3, offset: 0,
}, color: colors[(index * 4 + sIndex) % colors.length],
}); },
{
offset: 1,
color: "rgba(255, 255, 255, 0)",
},
]),
opacity: 0.3,
},
});
}
} }
}); );
// 如果没有clean/raw/sim数据则使用fallback // 如果没有任何数据则使用fallback
if (series.length === 0) { if (series.length === 0) {
series.push({ series.push({
name: deviceLabels?.[id] ?? id, name: deviceLabels?.[id] ?? id,