调整后端api,完善历史数据面板的交互
This commit is contained in:
@@ -45,8 +45,8 @@ export interface TimeSeriesPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SCADADataPanelProps {
|
export interface SCADADataPanelProps {
|
||||||
/** 选中的设备 ID 列表 */
|
/** 选中的要素信息列表,格式为 [[id, type], [id, type]] */
|
||||||
deviceIds: string[];
|
featureInfos: [string, string][];
|
||||||
/** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */
|
/** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */
|
||||||
type?: "realtime" | "scheme" | "none";
|
type?: "realtime" | "scheme" | "none";
|
||||||
/** 策略类型 */
|
/** 策略类型 */
|
||||||
@@ -55,7 +55,7 @@ export interface SCADADataPanelProps {
|
|||||||
scheme_name?: string;
|
scheme_name?: string;
|
||||||
/** 自定义数据获取器,默认使用后端 API */
|
/** 自定义数据获取器,默认使用后端 API */
|
||||||
fetchTimeSeriesData?: (
|
fetchTimeSeriesData?: (
|
||||||
deviceIds: string[],
|
featureInfos: [string, string][],
|
||||||
range: { from: Date; to: Date },
|
range: { from: Date; to: Date },
|
||||||
type?: "realtime" | "scheme" | "none",
|
type?: "realtime" | "scheme" | "none",
|
||||||
scheme_type?: string,
|
scheme_type?: string,
|
||||||
@@ -75,28 +75,36 @@ type LoadingState = "idle" | "loading" | "success" | "error";
|
|||||||
* 从后端 API 获取 SCADA 数据
|
* 从后端 API 获取 SCADA 数据
|
||||||
*/
|
*/
|
||||||
const fetchFromBackend = async (
|
const fetchFromBackend = async (
|
||||||
deviceIds: string[],
|
featureInfos: [string, string][],
|
||||||
range: { from: Date; to: Date },
|
range: { from: Date; to: Date },
|
||||||
type: "realtime" | "scheme" | "none" = "realtime",
|
type: "realtime" | "scheme" | "none" = "realtime",
|
||||||
scheme_type?: string,
|
scheme_type?: string,
|
||||||
scheme_name?: string
|
scheme_name?: string
|
||||||
): Promise<TimeSeriesPoint[]> => {
|
): Promise<TimeSeriesPoint[]> => {
|
||||||
if (deviceIds.length === 0) {
|
if (featureInfos.length === 0) {
|
||||||
return [];
|
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 start_time = dayjs(range.from).toISOString();
|
||||||
const end_time = dayjs(range.to).toISOString();
|
const end_time = dayjs(range.to).toISOString();
|
||||||
|
|
||||||
// 监测值数据接口
|
// 将 featureInfos 转换为后端期望的格式: id1:type1,id2:type2
|
||||||
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 feature_infos = featureInfos
|
||||||
// 清洗数据接口
|
.map(([id, type]) => `${id}:${type}`)
|
||||||
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}`;
|
.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 {
|
try {
|
||||||
if (type === "none") {
|
if (type === "none") {
|
||||||
@@ -110,13 +118,13 @@ const fetchFromBackend = async (
|
|||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cleanedData = transformBackendData(cleanedRes, deviceIds);
|
const cleanedData = transformBackendData(cleanedRes, featureIds);
|
||||||
const rawData = transformBackendData(rawRes, deviceIds);
|
const rawData = transformBackendData(rawRes, featureIds);
|
||||||
|
|
||||||
return mergeTimeSeriesData(
|
return mergeTimeSeriesData(
|
||||||
cleanedData,
|
cleanedData,
|
||||||
rawData,
|
rawData,
|
||||||
deviceIds,
|
featureIds,
|
||||||
"clean",
|
"clean",
|
||||||
"raw"
|
"raw"
|
||||||
);
|
);
|
||||||
@@ -134,9 +142,9 @@ const fetchFromBackend = async (
|
|||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cleanedData = transformBackendData(cleanedRes, deviceIds);
|
const cleanedData = transformBackendData(cleanedRes, featureIds);
|
||||||
const rawData = transformBackendData(rawRes, deviceIds);
|
const rawData = transformBackendData(rawRes, featureIds);
|
||||||
const schemeSimData = transformBackendData(schemeSimRes, deviceIds);
|
const schemeSimData = transformBackendData(schemeSimRes, featureIds);
|
||||||
|
|
||||||
// 合并三组数据
|
// 合并三组数据
|
||||||
const timeMap = new Map<string, Record<string, number | null>>();
|
const timeMap = new Map<string, Record<string, number | null>>();
|
||||||
@@ -148,7 +156,7 @@ const fetchFromBackend = async (
|
|||||||
timeMap.set(point.timestamp, {});
|
timeMap.set(point.timestamp, {});
|
||||||
}
|
}
|
||||||
const values = timeMap.get(point.timestamp)!;
|
const values = timeMap.get(point.timestamp)!;
|
||||||
deviceIds.forEach((deviceId) => {
|
featureIds.forEach((deviceId) => {
|
||||||
const value = point.values[deviceId];
|
const value = point.values[deviceId];
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
values[`${deviceId}_${suffix}`] = value;
|
values[`${deviceId}_${suffix}`] = value;
|
||||||
@@ -184,9 +192,9 @@ const fetchFromBackend = async (
|
|||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cleanedData = transformBackendData(cleanedRes, deviceIds);
|
const cleanedData = transformBackendData(cleanedRes, featureIds);
|
||||||
const rawData = transformBackendData(rawRes, deviceIds);
|
const rawData = transformBackendData(rawRes, featureIds);
|
||||||
const simulationData = transformBackendData(simulationRes, deviceIds);
|
const simulationData = transformBackendData(simulationRes, featureIds);
|
||||||
|
|
||||||
// 合并三组数据
|
// 合并三组数据
|
||||||
const timeMap = new Map<string, Record<string, number | null>>();
|
const timeMap = new Map<string, Record<string, number | null>>();
|
||||||
@@ -198,7 +206,7 @@ const fetchFromBackend = async (
|
|||||||
timeMap.set(point.timestamp, {});
|
timeMap.set(point.timestamp, {});
|
||||||
}
|
}
|
||||||
const values = timeMap.get(point.timestamp)!;
|
const values = timeMap.get(point.timestamp)!;
|
||||||
deviceIds.forEach((deviceId) => {
|
featureIds.forEach((deviceId) => {
|
||||||
const value = point.values[deviceId];
|
const value = point.values[deviceId];
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
values[`${deviceId}_${suffix}`] = value;
|
values[`${deviceId}_${suffix}`] = value;
|
||||||
@@ -384,7 +392,7 @@ const emptyStateMessages: Record<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
||||||
deviceIds,
|
featureInfos,
|
||||||
type = "realtime",
|
type = "realtime",
|
||||||
scheme_type,
|
scheme_type,
|
||||||
scheme_name,
|
scheme_name,
|
||||||
@@ -396,6 +404,12 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
return fetchTimeSeriesData;
|
return fetchTimeSeriesData;
|
||||||
}, [fetchTimeSeriesData]);
|
}, [fetchTimeSeriesData]);
|
||||||
|
|
||||||
|
// 从 featureInfos 中提取设备 ID 列表
|
||||||
|
const deviceIds = useMemo(
|
||||||
|
() => featureInfos.map(([id]) => id),
|
||||||
|
[featureInfos]
|
||||||
|
);
|
||||||
|
|
||||||
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
|
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
|
||||||
const [to, setTo] = useState<Dayjs>(() => dayjs());
|
const [to, setTo] = useState<Dayjs>(() => dayjs());
|
||||||
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
|
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
|
||||||
@@ -405,7 +419,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
|
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
|
||||||
const [selectedSource, setSelectedSource] = useState<
|
const [selectedSource, setSelectedSource] = useState<
|
||||||
"raw" | "clean" | "sim" | "all"
|
"raw" | "clean" | "sim" | "all"
|
||||||
>(() => (deviceIds.length === 1 ? "all" : "clean"));
|
>(() => (featureInfos.length === 1 ? "all" : "clean"));
|
||||||
const draggableRef = useRef<HTMLDivElement>(null);
|
const draggableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 获取 SCADA 设备信息,生成 deviceLabels
|
// 获取 SCADA 设备信息,生成 deviceLabels
|
||||||
@@ -466,7 +480,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
try {
|
try {
|
||||||
const { from: rangeFrom, to: rangeTo } = normalizedRange;
|
const { from: rangeFrom, to: rangeTo } = normalizedRange;
|
||||||
const result = await customFetcher(
|
const result = await customFetcher(
|
||||||
deviceIds,
|
featureInfos,
|
||||||
{
|
{
|
||||||
from: rangeFrom.toDate(),
|
from: rangeFrom.toDate(),
|
||||||
to: rangeTo.toDate(),
|
to: rangeTo.toDate(),
|
||||||
@@ -483,7 +497,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
deviceIds,
|
featureInfos,
|
||||||
customFetcher,
|
customFetcher,
|
||||||
hasDevices,
|
hasDevices,
|
||||||
normalizedRange,
|
normalizedRange,
|
||||||
@@ -500,14 +514,14 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setTimeSeries([]);
|
setTimeSeries([]);
|
||||||
}
|
}
|
||||||
}, [deviceIds.join(",")]);
|
}, [JSON.stringify(featureInfos)]);
|
||||||
|
|
||||||
// 当设备数量变化时,调整数据源选择
|
// 当设备数量变化时,调整数据源选择
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deviceIds.length > 1 && selectedSource === "all") {
|
if (featureInfos.length > 1 && selectedSource === "all") {
|
||||||
setSelectedSource("clean");
|
setSelectedSource("clean");
|
||||||
}
|
}
|
||||||
}, [deviceIds.length, selectedSource]);
|
}, [featureInfos.length, selectedSource]);
|
||||||
|
|
||||||
const columns: GridColDef[] = useMemo(() => {
|
const columns: GridColDef[] = useMemo(() => {
|
||||||
const base: GridColDef[] = [
|
const base: GridColDef[] = [
|
||||||
@@ -863,7 +877,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={`${deviceIds.length}`}
|
label={`${featureInfos.length}`}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "rgba(255,255,255,0.2)",
|
backgroundColor: "rgba(255,255,255,0.2)",
|
||||||
color: "primary.contrastText",
|
color: "primary.contrastText",
|
||||||
|
|||||||
@@ -15,14 +15,13 @@ import { Style, Stroke, Fill, Circle } from "ol/style";
|
|||||||
import { FeatureLike } from "ol/Feature";
|
import { FeatureLike } from "ol/Feature";
|
||||||
import Feature from "ol/Feature";
|
import Feature from "ol/Feature";
|
||||||
import StyleEditorPanel from "./StyleEditorPanel";
|
import StyleEditorPanel from "./StyleEditorPanel";
|
||||||
|
import { LayerStyleState } from "./StyleEditorPanel";
|
||||||
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
||||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||||
|
|
||||||
import { config } from "@/config/config";
|
import { config } from "@/config/config";
|
||||||
const backendUrl = config.BACKEND_URL;
|
const backendUrl = config.BACKEND_URL;
|
||||||
|
|
||||||
import { LayerStyleState } from "./StyleEditorPanel";
|
|
||||||
|
|
||||||
// 添加接口定义隐藏按钮的props
|
// 添加接口定义隐藏按钮的props
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
||||||
@@ -43,7 +42,6 @@ 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[]>([
|
||||||
@@ -198,19 +196,6 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
|||||||
}
|
}
|
||||||
}, [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) => {
|
||||||
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
|
// 样式工具的特殊处理 - 只有再次点击时才会取消激活和关闭
|
||||||
@@ -671,7 +656,36 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
|||||||
)}
|
)}
|
||||||
{showHistoryPanel && (
|
{showHistoryPanel && (
|
||||||
<HistoryDataPanel
|
<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_type="burst_Analysis"
|
||||||
scheme_name={schemeName}
|
scheme_name={schemeName}
|
||||||
type={queryType as "realtime" | "scheme" | "none"}
|
type={queryType as "realtime" | "scheme" | "none"}
|
||||||
|
|||||||
Reference in New Issue
Block a user