From 9d06226cb4d6860c16f3ccd83206695f5e264002 Mon Sep 17 00:00:00 2001 From: JIANG Date: Wed, 11 Feb 2026 16:29:18 +0800 Subject: [PATCH] Implemented a Zustand-based project_id store, expanded project selection/switching to persist project_id, and centralized backend requests via api/apiFetch (including data provider updates) to inject X-Project-ID. --- package-lock.json | 32 ++++++++++++++++- package.json | 3 +- src/app/OlMap/Controls/HistoryDataPanel.tsx | 17 +++++----- src/app/OlMap/Controls/Timeline.tsx | 11 +++--- src/app/OlMap/Controls/Toolbar.tsx | 5 +-- src/components/header/index.tsx | 6 ++++ .../BurstPipeAnalysis/AnalysisParameters.tsx | 4 +-- .../BurstPipeAnalysisPanel.tsx | 4 +-- .../olmap/BurstPipeAnalysis/SchemeQuery.tsx | 4 +-- .../BurstPipeAnalysis/ValveIsolation.tsx | 4 +-- .../AnalysisParameters.tsx | 4 +-- .../ContaminantSimulation/SchemeQuery.tsx | 4 +-- .../FlushingAnalysis/AnalysisParameters.tsx | 4 +-- .../olmap/FlushingAnalysis/SchemeQuery.tsx | 4 +-- .../olmap/HealthRiskAnalysis/Timeline.tsx | 3 +- .../OptimizationParameters.tsx | 4 +-- .../SchemeQuery.tsx | 4 +-- src/components/olmap/SCADADataPanel.tsx | 17 +++++----- src/components/olmap/SCADADeviceList.tsx | 4 +-- src/components/project/ProjectSelector.tsx | 34 ++++++++++++++----- src/contexts/ProjectContext.tsx | 22 ++++++++++-- src/lib/api.ts | 18 ++++++++++ src/lib/apiFetch.ts | 10 ++++++ src/providers/data-provider/index.ts | 5 ++- src/store/projectStore.ts | 27 +++++++++++++++ 25 files changed, 192 insertions(+), 62 deletions(-) create mode 100644 src/lib/api.ts create mode 100644 src/lib/apiFetch.ts create mode 100644 src/store/projectStore.ts diff --git a/package-lock.json b/package-lock.json index 3eab56a..c8bfce4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,8 @@ "react-draggable": "^4.5.0", "react-icons": "^5.5.0", "react-window": "^1.8.10", - "tailwindcss": "^4.1.13" + "tailwindcss": "^4.1.13", + "zustand": "^5.0.11" }, "devDependencies": { "@svgr/webpack": "^8.1.0", @@ -22904,6 +22905,35 @@ "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==", "license": "MIT AND BSD-3-Clause" }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", diff --git a/package.json b/package.json index a3f6c50..a144984 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "react-draggable": "^4.5.0", "react-icons": "^5.5.0", "react-window": "^1.8.10", - "tailwindcss": "^4.1.13" + "tailwindcss": "^4.1.13", + "zustand": "^5.0.11" }, "overrides": { "fast-xml-parser": "5.3.4" diff --git a/src/app/OlMap/Controls/HistoryDataPanel.tsx b/src/app/OlMap/Controls/HistoryDataPanel.tsx index ae0b32e..bd4f8c5 100644 --- a/src/app/OlMap/Controls/HistoryDataPanel.tsx +++ b/src/app/OlMap/Controls/HistoryDataPanel.tsx @@ -34,6 +34,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales"; import config from "@/config/config"; +import { apiFetch } from "@/lib/apiFetch"; dayjs.extend(utc); dayjs.extend(timezone); @@ -103,10 +104,10 @@ const fetchFromBackend = async ( if (type === "none") { // 查询清洗值和监测值 const [cleanedRes, rawRes] = await Promise.all([ - fetch(cleanedDataUrl) + apiFetch(cleanedDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(rawDataUrl) + apiFetch(rawDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), ]); @@ -126,13 +127,13 @@ const fetchFromBackend = async ( } else if (type === "scheme") { // 查询策略模拟值、清洗值和监测值 const [cleanedRes, rawRes, schemeSimRes] = await Promise.all([ - fetch(cleanedDataUrl) + apiFetch(cleanedDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(rawDataUrl) + apiFetch(rawDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(schemeSimulationDataUrl) + apiFetch(schemeSimulationDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), ]); @@ -178,13 +179,13 @@ const fetchFromBackend = async ( } else { // realtime: 查询模拟值、清洗值和监测值 const [cleanedRes, rawRes, simulationRes] = await Promise.all([ - fetch(cleanedDataUrl) + apiFetch(cleanedDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(rawDataUrl) + apiFetch(rawDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(simulationDataUrl) + apiFetch(simulationDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), ]); diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index 0688edb..f7cc65f 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -28,6 +28,7 @@ import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { useData } from "../MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; +import { apiFetch } from "@/lib/apiFetch"; import { useMap } from "../MapComponent"; interface TimelineProps { @@ -117,11 +118,11 @@ const Timeline: React.FC = ({ nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; } else { disableDateSelection && schemeName - ? (nodePromise = fetch( + ? (nodePromise = apiFetch( // `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}` `${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`, )) - : (nodePromise = fetch( + : (nodePromise = apiFetch( // `${config.BACKEND_URL}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` `${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=node&property=${junctionProperties}`, )); @@ -138,11 +139,11 @@ const Timeline: React.FC = ({ linkRecords = linkCacheRef.current.get(linkCacheKey)!; } else { disableDateSelection && schemeName - ? (linkPromise = fetch( + ? (linkPromise = apiFetch( // `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}` `${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${pipeProperties}`, )) - : (linkPromise = fetch( + : (linkPromise = apiFetch( // `${config.BACKEND_URL}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` `${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=link&property=${pipeProperties}`, )); @@ -513,7 +514,7 @@ const Timeline: React.FC = ({ duration: calculatedInterval, }; - const response = await fetch( + const response = await apiFetch( `${config.BACKEND_URL}/api/v1/runsimulationmanuallybydate/`, { method: "POST", diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx index 8914b20..7a763e2 100644 --- a/src/app/OlMap/Controls/Toolbar.tsx +++ b/src/app/OlMap/Controls/Toolbar.tsx @@ -20,6 +20,7 @@ import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/ import { useNotification } from "@refinedev/core"; import { config } from "@/config/config"; +import { apiFetch } from "@/lib/apiFetch"; // 添加接口定义隐藏按钮的props interface ToolbarProps { @@ -388,12 +389,12 @@ const Toolbar: React.FC = ({ const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z" let response; if (queryType === "scheme") { - response = await fetch( + response = await apiFetch( // `${config.BACKEND_URL}/queryschemesimulationrecordsbyidtime/?scheme_name=${schemeName}&id=${id}&querytime=${querytime}&type=${type}` `${config.BACKEND_URL}/api/v1/scheme/query/by-id-time?scheme_type=${schemeType}&scheme_name=${schemeName}&id=${id}&type=${type}&query_time=${querytime}`, ); } else { - response = await fetch( + response = await apiFetch( // `${config.BACKEND_URL}/querysimulationrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}` `${config.BACKEND_URL}/api/v1/realtime/query/by-id-time?id=${id}&type=${type}&query_time=${querytime}`, ); diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index 47256da..ee70181 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -22,6 +22,7 @@ import { HamburgerMenu, RefineThemedLayoutHeaderProps } from "@refinedev/mui"; import React, { useContext, useState } from "react"; import { ProjectSelector } from "@components/project/ProjectSelector"; import { setMapExtent, setMapWorkspace, setNetworkName } from "@config/config"; +import { useProjectStore } from "@/store/projectStore"; type IUser = { id: number; @@ -37,6 +38,9 @@ export const Header: React.FC = ({ const [anchorEl, setAnchorEl] = useState(null); const [showProjectSelector, setShowProjectSelector] = useState(false); const open = Boolean(anchorEl); + const setCurrentProjectId = useProjectStore( + (state) => state.setCurrentProjectId, + ); const { data: user } = useGetIdentity(); @@ -54,6 +58,7 @@ export const Header: React.FC = ({ }; const handleProjectSelect = ( + projectId: string, workspace: string, networkName: string, extent: number[], @@ -65,6 +70,7 @@ export const Header: React.FC = ({ localStorage.setItem("NEXT_PUBLIC_NETWORK_NAME", networkName); localStorage.setItem("NEXT_PUBLIC_MAP_EXTENT", extent.join(",")); localStorage.removeItem(`${workspace}_map_view`); + setCurrentProjectId(projectId || networkName || workspace); setShowProjectSelector(false); window.location.reload(); }; diff --git a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx index 15b611f..7e242c0 100644 --- a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx +++ b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx @@ -23,7 +23,7 @@ import { Style, Stroke, Icon } from "ol/style"; import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import Feature, { FeatureLike } from "ol/Feature"; import { useNotification } from "@refinedev/core"; -import axios from "axios"; +import { api } from "@/lib/api"; import { config, NETWORK_NAME } from "@/config/config"; import { along, lineString, length, toMercator } from "@turf/turf"; import { Point } from "ol/geom"; @@ -283,7 +283,7 @@ const AnalysisParameters: React.FC = () => { }; try { - await axios.get(`${config.BACKEND_URL}/api/v1/burst_analysis/`, { + await api.get(`${config.BACKEND_URL}/api/v1/burst_analysis/`, { params, paramsSerializer: { indexes: null, // 移除数组索引,即由 burst_ID[] 变为 burst_ID diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx index 6d204cc..5f7302a 100644 --- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx @@ -22,7 +22,7 @@ import AnalysisParameters from "./AnalysisParameters"; import SchemeQuery from "./SchemeQuery"; import LocationResults from "./LocationResults"; import ValveIsolation from "./ValveIsolation"; -import axios from "axios"; +import { api } from "@/lib/api"; import { config } from "@config/config"; import { useNotification } from "@refinedev/core"; import { LocationResult, SchemeRecord, ValveIsolationResult } from "./types"; @@ -85,7 +85,7 @@ const BurstPipeAnalysisPanel: React.FC = ({ const handleLocateScheme = async (scheme: SchemeRecord) => { try { - const response = await axios.get( + const response = await api.get( `${config.BACKEND_URL}/api/v1/burst-locate-result/${scheme.schemeName}`, ); setLocationResults(response.data); diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index 34dfcf3..b1c5e6b 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -26,7 +26,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; // 引入中文包 import dayjs, { Dayjs } from "dayjs"; -import axios from "axios"; +import { api } from "@/lib/api"; import moment from "moment"; import { config, NETWORK_NAME } from "@config/config"; import { useNotification } from "@refinedev/core"; @@ -109,7 +109,7 @@ const SchemeQuery: React.FC = ({ setLoading(true); try { - const response = await axios.get( + const response = await api.get( `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`, ); let filteredResults = response.data; diff --git a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx index 765a8dd..7553bbc 100644 --- a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx +++ b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx @@ -27,7 +27,7 @@ import { CheckBox as CheckBoxIcon, CheckBoxOutlineBlank as CheckBoxOutlineBlankIcon, } from "@mui/icons-material"; -import axios from "axios"; +import { api } from "@/lib/api"; import { config, NETWORK_NAME } from "@config/config"; import { ValveIsolationResult } from "./types"; import { useNotification } from "@refinedev/core"; @@ -270,7 +270,7 @@ const ValveIsolation: React.FC = ({ if (disabled.length > 0) { params.disabled_valves = disabled; } - const response = await axios.get( + const response = await api.get( `${config.BACKEND_URL}/api/v1/valve_isolation_analysis/`, { params, diff --git a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx index 9bb134a..ee58be3 100644 --- a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx +++ b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx @@ -17,7 +17,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; import dayjs, { Dayjs } from "dayjs"; import { useNotification } from "@refinedev/core"; -import axios from "axios"; +import { api } from "@/lib/api"; import { config, NETWORK_NAME } from "@/config/config"; import { useMap } from "@app/OlMap/MapComponent"; import VectorLayer from "ol/layer/Vector"; @@ -189,7 +189,7 @@ const AnalysisParameters: React.FC = () => { scheme_name: schemeName, }; - await axios.get(`${config.BACKEND_URL}/api/v1/contaminant_simulation/`, { + await api.get(`${config.BACKEND_URL}/api/v1/contaminant_simulation/`, { params, }); diff --git a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx index a7db304..21d29fd 100644 --- a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx +++ b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx @@ -25,7 +25,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; import dayjs, { Dayjs } from "dayjs"; -import axios from "axios"; +import { api } from "@/lib/api"; import moment from "moment"; import { useNotification } from "@refinedev/core"; import { config, NETWORK_NAME } from "@config/config"; @@ -180,7 +180,7 @@ const SchemeQuery: React.FC = ({ if (!queryAll && !queryDate) return; setLoading(true); try { - const response = await axios.get( + const response = await api.get( `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`, ); let filteredResults = response.data; diff --git a/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx b/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx index f7f3fe8..241e666 100644 --- a/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx +++ b/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx @@ -25,7 +25,7 @@ import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style"; import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; import Feature, { FeatureLike } from "ol/Feature"; import { useNotification } from "@refinedev/core"; -import axios from "axios"; +import { api } from "@/lib/api"; import { config, NETWORK_NAME } from "@/config/config"; interface ValveItem { @@ -242,7 +242,7 @@ const AnalysisParameters: React.FC = () => { // but axios usually handles array as valves[]=1&valves[]=2 // FastAPI default expects repeated query params. - const response = await axios.get(`${config.BACKEND_URL}/flushing_analysis/`, { + const response = await api.get(`${config.BACKEND_URL}/flushing_analysis/`, { params, // Ensure arrays are sent as repeated keys: valves=1&valves=2 paramsSerializer: { diff --git a/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx b/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx index eb75457..ed927a6 100644 --- a/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx @@ -27,7 +27,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; import dayjs, { Dayjs } from "dayjs"; -import axios from "axios"; +import { api } from "@/lib/api"; import moment from "moment"; import { config, NETWORK_NAME } from "@config/config"; import { useNotification } from "@refinedev/core"; @@ -221,7 +221,7 @@ const SchemeQuery: React.FC = ({ setLoading(true); try { - const response = await axios.get( + const response = await api.get( `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`, ); diff --git a/src/components/olmap/HealthRiskAnalysis/Timeline.tsx b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx index 382a4c3..957a99b 100644 --- a/src/components/olmap/HealthRiskAnalysis/Timeline.tsx +++ b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx @@ -29,6 +29,7 @@ import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { useData } from "../../../app/OlMap/MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; +import { apiFetch } from "@/lib/apiFetch"; import { useMap } from "../../../app/OlMap/MapComponent"; import { useHealthRisk } from "./HealthRiskContext"; import { @@ -422,7 +423,7 @@ const Timeline: React.FC = ({ undoableTimeout: 3, }); try { - const response = await fetch( + const response = await apiFetch( `${config.BACKEND_URL}/api/v1/composite/pipeline-health-prediction?query_time=${query_time}&network_name=${NETWORK_NAME}`, ); diff --git a/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx b/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx index de01cf5..4c1cb67 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/OptimizationParameters.tsx @@ -12,7 +12,7 @@ import { import { PlayArrow as PlayArrowIcon } from "@mui/icons-material"; import { useNotification } from "@refinedev/core"; import { useGetIdentity } from "@refinedev/core"; -import axios from "axios"; +import { api } from "@/lib/api"; import { config, NETWORK_NAME } from "@/config/config"; type IUser = { @@ -93,7 +93,7 @@ const OptimizationParameters: React.FC = () => { try { // 发送优化请求 - const response = await axios.post( + const response = await api.post( `${config.BACKEND_URL}/api/v1/sensorplacementscheme/create`, null, { diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx index ad36ab6..25560d0 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx @@ -24,7 +24,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; // 引入中文包 import dayjs, { Dayjs } from "dayjs"; -import axios from "axios"; +import { api } from "@/lib/api"; import moment from "moment"; import { config, NETWORK_NAME } from "@config/config"; import { useNotification } from "@refinedev/core"; @@ -148,7 +148,7 @@ const SchemeQuery: React.FC = ({ setLoading(true); try { - const response = await axios.get( + const response = await api.get( `${config.BACKEND_URL}/api/v1/getallsensorplacements/?network=${network}`, ); diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index 833da86..a6c53dc 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -37,7 +37,8 @@ import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales"; import config from "@/config/config"; import { useGetIdentity } from "@refinedev/core"; import { useNotification } from "@refinedev/core"; -import axios from "axios"; +import { api } from "@/lib/api"; +import { apiFetch } from "@/lib/apiFetch"; dayjs.extend(utc); dayjs.extend(timezone); @@ -96,10 +97,10 @@ const fetchFromBackend = async ( try { // 优先查询清洗数据和模拟数据 const [cleaningRes, simulationRes] = await Promise.all([ - fetch(cleaningDataUrl) + apiFetch(cleaningDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(simulationDataUrl) + apiFetch(simulationDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), ]); @@ -118,7 +119,7 @@ const fetchFromBackend = async ( ); } else { // 如果清洗数据没有数据,查询原始数据,返回模拟和原始数据 - const rawRes = await fetch(rawDataUrl) + const rawRes = await apiFetch(rawDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null); const rawData = transformBackendData(rawRes, deviceIds); @@ -338,13 +339,13 @@ const SCADADataPanel: React.FC = ({ const simulationDataUrl = `${config.BACKEND_URL}/api/v1/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`; try { const [cleanRes, rawRes, simRes] = await Promise.all([ - fetch(cleaningDataUrl) + apiFetch(cleaningDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(rawDataUrl) + apiFetch(rawDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), - fetch(simulationDataUrl) + apiFetch(simulationDataUrl) .then((r) => (r.ok ? r.json() : null)) .catch(() => null), ]); @@ -474,7 +475,7 @@ const SCADADataPanel: React.FC = ({ const endTime = dayjs(rangeTo).toISOString(); // 调用后端清洗接口 - const response = await axios.post( + const response = await api.post( `${ config.BACKEND_URL }/api/v1/composite/clean-scada?device_ids=${deviceIds.join( diff --git a/src/components/olmap/SCADADeviceList.tsx b/src/components/olmap/SCADADeviceList.tsx index 5cf9dfa..2c6f200 100644 --- a/src/components/olmap/SCADADeviceList.tsx +++ b/src/components/olmap/SCADADeviceList.tsx @@ -48,7 +48,7 @@ import { } from "@mui/icons-material"; import { FixedSizeList } from "react-window"; import { useNotification } from "@refinedev/core"; -import axios from "axios"; +import { api } from "@/lib/api"; import { useGetIdentity } from "@refinedev/core"; import config from "@/config/config"; @@ -622,7 +622,7 @@ const SCADADeviceList: React.FC = ({ const endTime = dayjs(cleanEndTime).toISOString(); // 调用后端清洗接口 - const response = await axios.post( + const response = await api.post( `${config.BACKEND_URL}/api/v1/composite/clean-scada?device_ids=all&start_time=${startTime}&end_time=${endTime}`, ); diff --git a/src/components/project/ProjectSelector.tsx b/src/components/project/ProjectSelector.tsx index 5f8b6da..b5fe3ec 100644 --- a/src/components/project/ProjectSelector.tsx +++ b/src/components/project/ProjectSelector.tsx @@ -19,24 +19,31 @@ import { useState } from "react"; interface ProjectSelectorProps { open: boolean; - onSelect: (workspace: string, networkName: string, extent: number[]) => void; + onSelect: ( + projectId: string, + workspace: string, + networkName: string, + extent: number[], + ) => void; onClose?: () => void; } const PROJECTS = [ { + id: "tjwater", label: "默认", workspace: "tjwater", networkName: "tjwater", extent: [13508802, 3608164, 13555651, 3633686], }, + // { + // label: "苏州河", + // workspace: "szh", + // networkName: "szh", + // extent: [13490131, 3630016, 13525879, 3666969], + // }, { - label: "苏州河", - workspace: "szh", - networkName: "szh", - extent: [13490131, 3630016, 13525879, 3666969], - }, - { + id: "test", label: "测试项目", workspace: "test", networkName: "test", @@ -49,6 +56,7 @@ export const ProjectSelector: React.FC = ({ onSelect, onClose, }) => { + const [projectId, setProjectId] = useState(PROJECTS[0].id); const [workspace, setWorkspace] = useState(PROJECTS[0].workspace); const [networkName, setNetworkName] = useState(PROJECTS[0].networkName); const [extent, setExtent] = useState( @@ -57,7 +65,8 @@ export const ProjectSelector: React.FC = ({ const [customMode, setCustomMode] = useState(false); const handleConfirm = () => { - onSelect(workspace, networkName, extent); + const resolvedProjectId = projectId.trim() || workspace || networkName; + onSelect(resolvedProjectId, workspace, networkName, extent); }; return ( @@ -123,9 +132,11 @@ export const ProjectSelector: React.FC = ({ const val = e.target.value; if (val === "custom") { setCustomMode(true); + setProjectId(workspace); } else { const p = PROJECTS.find((p) => p.workspace === val); if (p) { + setProjectId(p.id); setWorkspace(p.workspace); setNetworkName(p.networkName); setExtent(p.extent); @@ -150,6 +161,13 @@ export const ProjectSelector: React.FC = ({ ) : ( + setProjectId(e.target.value)} + fullWidth + helperText="例如: tjwater" + /> = ({ }) => { const { status } = useSession(); const [isConfigured, setIsConfigured] = useState(false); + const setCurrentProjectId = useProjectStore( + (state) => state.setCurrentProjectId, + ); const [currentProject, setCurrentProject] = useState({ workspace: config.MAP_WORKSPACE, networkName: NETWORK_NAME || "tjwater", @@ -28,10 +33,12 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ const savedWorkspace = localStorage.getItem("NEXT_PUBLIC_MAP_WORKSPACE"); const savedNetwork = localStorage.getItem("NEXT_PUBLIC_NETWORK_NAME"); const savedExtent = localStorage.getItem("NEXT_PUBLIC_MAP_EXTENT"); + const savedProjectId = localStorage.getItem("active_project"); // If we have saved config, use it. if (savedWorkspace && savedNetwork) { applyConfig( + savedProjectId || savedNetwork || savedWorkspace, savedWorkspace, savedNetwork, savedExtent ? savedExtent.split(",").map(Number) : config.MAP_EXTENT, @@ -39,7 +46,13 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ } }, []); - const applyConfig = async (ws: string, net: string, extent: number[]) => { + const applyConfig = async ( + projectId: string, + ws: string, + net: string, + extent: number[], + ) => { + const resolvedProjectId = projectId || net || ws; setMapWorkspace(ws); setNetworkName(net); setMapExtent(extent); @@ -47,6 +60,7 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ // Reset extent cache localStorage.removeItem(`${ws}_map_view`); setCurrentProject({ workspace: ws, networkName: net, extent: extent }); + setCurrentProjectId(resolvedProjectId); // Save to localStorage localStorage.setItem("NEXT_PUBLIC_MAP_WORKSPACE", ws); @@ -55,7 +69,7 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ setIsConfigured(true); try { - await fetch(`${config.BACKEND_URL}/openproject/?network=${net}`, { + await apiFetch(`${config.BACKEND_URL}/openproject/?network=${net}`, { method: "POST", }); } catch (error) { @@ -68,7 +82,9 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ return ( applyConfig(ws, net, extent)} + onSelect={(projectId, ws, net, extent) => + applyConfig(projectId, ws, net, extent) + } /> ); } diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..cc7aea8 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,18 @@ +import axios from "axios"; +import { config } from "@config/config"; +import { useProjectStore } from "@/store/projectStore"; + +export const API_URL = process.env.NEXT_PUBLIC_API_URL || config.BACKEND_URL; + +export const api = axios.create({ + baseURL: API_URL, +}); + +api.interceptors.request.use((request) => { + const projectId = useProjectStore.getState().currentProjectId; + if (projectId) { + request.headers = request.headers ?? {}; + request.headers["X-Project-ID"] = projectId; + } + return request; +}); diff --git a/src/lib/apiFetch.ts b/src/lib/apiFetch.ts new file mode 100644 index 0000000..1c8c531 --- /dev/null +++ b/src/lib/apiFetch.ts @@ -0,0 +1,10 @@ +import { useProjectStore } from "@/store/projectStore"; + +export const apiFetch = (input: RequestInfo | URL, init: RequestInit = {}) => { + const projectId = useProjectStore.getState().currentProjectId; + const headers = new Headers(init.headers ?? {}); + if (projectId) { + headers.set("X-Project-ID", projectId); + } + return fetch(input, { ...init, headers }); +}; diff --git a/src/providers/data-provider/index.ts b/src/providers/data-provider/index.ts index a049c0e..fa69f05 100644 --- a/src/providers/data-provider/index.ts +++ b/src/providers/data-provider/index.ts @@ -1,7 +1,6 @@ "use client"; import dataProviderSimpleRest from "@refinedev/simple-rest"; +import { api, API_URL } from "@/lib/api"; -const API_URL = "https://api.fake-rest.refine.dev"; - -export const dataProvider = dataProviderSimpleRest(API_URL); +export const dataProvider = dataProviderSimpleRest(API_URL, api); diff --git a/src/store/projectStore.ts b/src/store/projectStore.ts new file mode 100644 index 0000000..5fda7d1 --- /dev/null +++ b/src/store/projectStore.ts @@ -0,0 +1,27 @@ +import { create } from "zustand"; + +interface ProjectState { + currentProjectId: string | null; + setCurrentProjectId: (id: string | null) => void; +} + +const getInitialProjectId = () => { + if (typeof window === "undefined") { + return null; + } + return localStorage.getItem("active_project"); +}; + +export const useProjectStore = create((set) => ({ + currentProjectId: getInitialProjectId(), + setCurrentProjectId: (id) => { + if (typeof window !== "undefined") { + if (id) { + localStorage.setItem("active_project", id); + } else { + localStorage.removeItem("active_project"); + } + } + set({ currentProjectId: id }); + }, +}));