更新历史数据组件传参,新增获取策略模拟值
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user