调整后端api,完善历史数据面板的交互

This commit is contained in:
JIANG
2025-12-17 11:13:09 +08:00
parent 3a97e01dda
commit 6d672800f9
2 changed files with 76 additions and 48 deletions

View File

@@ -45,8 +45,8 @@ export interface TimeSeriesPoint {
}
export interface SCADADataPanelProps {
/** 选中的设备 ID 列表 */
deviceIds: string[];
/** 选中的要素信息列表,格式为 [[id, type], [id, type]] */
featureInfos: [string, string][];
/** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */
type?: "realtime" | "scheme" | "none";
/** 策略类型 */
@@ -55,7 +55,7 @@ export interface SCADADataPanelProps {
scheme_name?: string;
/** 自定义数据获取器,默认使用后端 API */
fetchTimeSeriesData?: (
deviceIds: string[],
featureInfos: [string, string][],
range: { from: Date; to: Date },
type?: "realtime" | "scheme" | "none",
scheme_type?: string,
@@ -75,28 +75,36 @@ type LoadingState = "idle" | "loading" | "success" | "error";
* 从后端 API 获取 SCADA 数据
*/
const fetchFromBackend = async (
deviceIds: string[],
featureInfos: [string, string][],
range: { from: Date; to: Date },
type: "realtime" | "scheme" | "none" = "realtime",
scheme_type?: string,
scheme_name?: string
): Promise<TimeSeriesPoint[]> => {
if (deviceIds.length === 0) {
if (featureInfos.length === 0) {
return [];
}
const device_ids = deviceIds.join(",");
// 提取设备 ID 列表
const featureIds = featureInfos.map(([id]) => id);
const feature_ids = featureIds.join(",");
const start_time = dayjs(range.from).toISOString();
const end_time = dayjs(range.to).toISOString();
// 监测值数据接口
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}`;
// 将 featureInfos 转换为后端期望的格式: id1:type1,id2:type2
const feature_infos = featureInfos
.map(([id, type]) => `${id}:${type}`)
.join(",");
// 监测值数据接口use_cleaned=false
const rawDataUrl = `${config.BACKEND_URL}/timescaledb/composite/element-scada?element_id=${feature_ids}&start_time=${start_time}&end_time=${end_time}&use_cleaned=false`;
// 清洗数据接口use_cleaned=true
const cleanedDataUrl = `${config.BACKEND_URL}/timescaledb/composite/element-scada?element_id=${feature_ids}&start_time=${start_time}&end_time=${end_time}&use_cleaned=true`;
// 模拟数据接口
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/element-simulation?feature_infos=${feature_infos}&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}`;
const schemeSimulationDataUrl = `${config.BACKEND_URL}/timescaledb/composite/element-simulation?feature_infos=${feature_infos}&start_time=${start_time}&end_time=${end_time}&scheme_type=${scheme_type}&scheme_name=${scheme_name}`;
try {
if (type === "none") {
@@ -110,13 +118,13 @@ const fetchFromBackend = async (
.catch(() => null),
]);
const cleanedData = transformBackendData(cleanedRes, deviceIds);
const rawData = transformBackendData(rawRes, deviceIds);
const cleanedData = transformBackendData(cleanedRes, featureIds);
const rawData = transformBackendData(rawRes, featureIds);
return mergeTimeSeriesData(
cleanedData,
rawData,
deviceIds,
featureIds,
"clean",
"raw"
);
@@ -134,9 +142,9 @@ const fetchFromBackend = async (
.catch(() => null),
]);
const cleanedData = transformBackendData(cleanedRes, deviceIds);
const rawData = transformBackendData(rawRes, deviceIds);
const schemeSimData = transformBackendData(schemeSimRes, deviceIds);
const cleanedData = transformBackendData(cleanedRes, featureIds);
const rawData = transformBackendData(rawRes, featureIds);
const schemeSimData = transformBackendData(schemeSimRes, featureIds);
// 合并三组数据
const timeMap = new Map<string, Record<string, number | null>>();
@@ -148,7 +156,7 @@ const fetchFromBackend = async (
timeMap.set(point.timestamp, {});
}
const values = timeMap.get(point.timestamp)!;
deviceIds.forEach((deviceId) => {
featureIds.forEach((deviceId) => {
const value = point.values[deviceId];
if (value !== undefined) {
values[`${deviceId}_${suffix}`] = value;
@@ -184,9 +192,9 @@ const fetchFromBackend = async (
.catch(() => null),
]);
const cleanedData = transformBackendData(cleanedRes, deviceIds);
const rawData = transformBackendData(rawRes, deviceIds);
const simulationData = transformBackendData(simulationRes, deviceIds);
const cleanedData = transformBackendData(cleanedRes, featureIds);
const rawData = transformBackendData(rawRes, featureIds);
const simulationData = transformBackendData(simulationRes, featureIds);
// 合并三组数据
const timeMap = new Map<string, Record<string, number | null>>();
@@ -198,7 +206,7 @@ const fetchFromBackend = async (
timeMap.set(point.timestamp, {});
}
const values = timeMap.get(point.timestamp)!;
deviceIds.forEach((deviceId) => {
featureIds.forEach((deviceId) => {
const value = point.values[deviceId];
if (value !== undefined) {
values[`${deviceId}_${suffix}`] = value;
@@ -384,7 +392,7 @@ const emptyStateMessages: Record<
};
const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
deviceIds,
featureInfos,
type = "realtime",
scheme_type,
scheme_name,
@@ -396,6 +404,12 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
return fetchTimeSeriesData;
}, [fetchTimeSeriesData]);
// 从 featureInfos 中提取设备 ID 列表
const deviceIds = useMemo(
() => featureInfos.map(([id]) => id),
[featureInfos]
);
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
const [to, setTo] = useState<Dayjs>(() => dayjs());
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
@@ -405,7 +419,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
const [selectedSource, setSelectedSource] = useState<
"raw" | "clean" | "sim" | "all"
>(() => (deviceIds.length === 1 ? "all" : "clean"));
>(() => (featureInfos.length === 1 ? "all" : "clean"));
const draggableRef = useRef<HTMLDivElement>(null);
// 获取 SCADA 设备信息,生成 deviceLabels
@@ -466,7 +480,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
try {
const { from: rangeFrom, to: rangeTo } = normalizedRange;
const result = await customFetcher(
deviceIds,
featureInfos,
{
from: rangeFrom.toDate(),
to: rangeTo.toDate(),
@@ -483,7 +497,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
}
},
[
deviceIds,
featureInfos,
customFetcher,
hasDevices,
normalizedRange,
@@ -500,14 +514,14 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
} else {
setTimeSeries([]);
}
}, [deviceIds.join(",")]);
}, [JSON.stringify(featureInfos)]);
// 当设备数量变化时,调整数据源选择
useEffect(() => {
if (deviceIds.length > 1 && selectedSource === "all") {
if (featureInfos.length > 1 && selectedSource === "all") {
setSelectedSource("clean");
}
}, [deviceIds.length, selectedSource]);
}, [featureInfos.length, selectedSource]);
const columns: GridColDef[] = useMemo(() => {
const base: GridColDef[] = [
@@ -863,7 +877,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
</Typography>
<Chip
size="small"
label={`${deviceIds.length}`}
label={`${featureInfos.length}`}
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "primary.contrastText",

View File

@@ -15,14 +15,13 @@ import { Style, Stroke, Fill, Circle } from "ol/style";
import { FeatureLike } from "ol/Feature";
import Feature from "ol/Feature";
import StyleEditorPanel from "./StyleEditorPanel";
import { LayerStyleState } from "./StyleEditorPanel";
import StyleLegend from "./StyleLegend"; // 引入图例组件
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
import { config } from "@/config/config";
const backendUrl = config.BACKEND_URL;
import { LayerStyleState } from "./StyleEditorPanel";
// 添加接口定义隐藏按钮的props
interface ToolbarProps {
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
@@ -43,7 +42,6 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
const [showHistoryPanel, setShowHistoryPanel] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [featureId, setFeatureId] = useState<string>("");
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
@@ -198,19 +196,6 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
}
}, [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) => {
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
@@ -671,7 +656,36 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
)}
{showHistoryPanel && (
<HistoryDataPanel
deviceIds={featureId ? [featureId] : []}
featureInfos={(() => {
if (!highlightFeature || !showHistoryPanel) return [];
const properties = highlightFeature.getProperties();
const id = properties.id;
if (!id) return [];
// 从图层名称推断类型
const layerId =
highlightFeature.getId()?.toString().split(".")[0] || "";
let type = "unknown";
if (layerId.includes("pipe")) {
type = "pipe";
} else if (layerId.includes("junction")) {
type = "junction";
} else if (layerId.includes("tank")) {
type = "tank";
} else if (layerId.includes("reservoir")) {
type = "reservoir";
} else if (layerId.includes("pump")) {
type = "pump";
} else if (layerId.includes("valve")) {
type = "valve";
}
// 仅处理 type 为 pipe 或 junction 的情况
if (type !== "pipe" && type !== "junction") {
return [];
}
return [[id, type]];
})()}
scheme_type="burst_Analysis"
scheme_name={schemeName}
type={queryType as "realtime" | "scheme" | "none"}