From 1e8af75b882f67b33b00eb0c751ae1b6d7592819 Mon Sep 17 00:00:00 2001 From: JIANG Date: Tue, 10 Feb 2026 16:13:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B9=E7=9B=AE=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=BC=B9=E7=AA=97=EF=BC=88=E9=A2=84=E8=AE=BE=E9=80=89?= =?UTF-8?q?=E9=A1=B9=EF=BC=89=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/OlMap/MapComponent.tsx | 9 +- src/app/_refine_context.tsx | 5 +- src/components/project/ProjectSelector.tsx | 146 +++++++++++++++++++++ src/config/config.ts | 11 +- src/contexts/ProjectContext.tsx | 64 +++++++++ src/utils/mapQueryService.ts | 10 +- 6 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 src/components/project/ProjectSelector.tsx create mode 100644 src/contexts/ProjectContext.tsx diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index c2e1068..86970ad 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -75,10 +75,6 @@ interface DataContextType { const MapContext = createContext(undefined); const DataContext = createContext(undefined); -const MAP_EXTENT = config.MAP_EXTENT as [number, number, number, number]; -const MAP_URL = config.MAP_URL; -const MAP_WORKSPACE = config.MAP_WORKSPACE; -const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key // 添加防抖函数 function debounce any>(func: F, waitFor: number) { let timeout: ReturnType | null = null; @@ -99,6 +95,11 @@ export const useData = () => { }; const MapComponent: React.FC = ({ children }) => { + const MAP_EXTENT = config.MAP_EXTENT as [number, number, number, number]; + const MAP_URL = config.MAP_URL; + const MAP_WORKSPACE = config.MAP_WORKSPACE; + const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key + const mapRef = useRef(null); const deckLayerRef = useRef(null); diff --git a/src/app/_refine_context.tsx b/src/app/_refine_context.tsx index bd0e26e..af98169 100644 --- a/src/app/_refine_context.tsx +++ b/src/app/_refine_context.tsx @@ -14,6 +14,7 @@ import routerProvider from "@refinedev/nextjs-router"; import { ColorModeContextProvider } from "@contexts/color-mode"; import { dataProvider } from "@providers/data-provider"; +import { ProjectProvider } from "@/contexts/ProjectContext"; import { LiaNetworkWiredSolid } from "react-icons/lia"; import { TbDatabaseEdit } from "react-icons/tb"; @@ -32,7 +33,9 @@ export const RefineContext = ( ) => { return ( - + + + ); }; diff --git a/src/components/project/ProjectSelector.tsx b/src/components/project/ProjectSelector.tsx new file mode 100644 index 0000000..7537cb2 --- /dev/null +++ b/src/components/project/ProjectSelector.tsx @@ -0,0 +1,146 @@ +import { Title } from "@components/title"; +import { + Dialog, + DialogContent, + DialogActions, + Button, + Select, + MenuItem, + FormControl, + InputLabel, + TextField, + Box, + Typography, + Fade, +} from "@mui/material"; +import { useState } from "react"; + +interface ProjectSelectorProps { + open: boolean; + onSelect: (workspace: string, networkName: string) => void; +} + +const PROJECTS = [ + { label: "TJWater (默认)", workspace: "TJWater", networkName: "tjwater" }, + { label: "苏州河", workspace: "szh", networkName: "szh" }, + { label: "测试项目", workspace: "test", networkName: "test" }, +]; + +export const ProjectSelector: React.FC = ({ + open, + onSelect, +}) => { + const [workspace, setWorkspace] = useState(PROJECTS[0].workspace); + const [networkName, setNetworkName] = useState(PROJECTS[0].networkName); + const [customMode, setCustomMode] = useState(false); + + const handleConfirm = () => { + onSelect(workspace, networkName); + }; + + return ( + + + + + </Box> + <Typography variant="subtitle1" color="text.secondary"> + 请选择项目环境 + </Typography> + </Box> + + <DialogContent sx={{ display: "flex", flexDirection: "column", gap: 3, pt: 1 }}> + {!customMode ? ( + <FormControl fullWidth variant="outlined"> + <InputLabel>项目</InputLabel> + <Select + value={workspace} + label="项目" + onChange={(e) => { + const val = e.target.value; + if (val === "custom") { + setCustomMode(true); + } else { + const p = PROJECTS.find((p) => p.workspace === val); + if (p) { + setWorkspace(p.workspace); + setNetworkName(p.networkName); + } + } + }} + > + {PROJECTS.map((p) => ( + <MenuItem key={p.workspace} value={p.workspace}> + <Box sx={{ display: "flex", flexDirection: "column" }}> + <Typography variant="body1">{p.label}</Typography> + <Typography variant="caption" color="text.secondary"> + 工作区: {p.workspace} | 管网: {p.networkName} + </Typography> + </Box> + </MenuItem> + ))} + <MenuItem value="custom"> + <Typography variant="body1">自定义配置...</Typography> + </MenuItem> + </Select> + </FormControl> + ) : ( + <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> + <TextField + label="Geoserver 工作区" + value={workspace} + onChange={(e) => setWorkspace(e.target.value)} + fullWidth + helperText="例如: TJWater" + /> + <TextField + label="管网名称" + value={networkName} + onChange={(e) => setNetworkName(e.target.value)} + fullWidth + helperText="例如: tjwater" + /> + <Button + onClick={() => setCustomMode(false)} + size="small" + sx={{ alignSelf: "flex-start" }} + > + 返回列表 + </Button> + </Box> + )} + </DialogContent> + <DialogActions sx={{ px: 3, pb: 2 }}> + <Button + onClick={handleConfirm} + variant="contained" + fullWidth + size="large" + sx={{ + textTransform: "none", + borderRadius: 2, + fontWeight: "bold" + }} + > + 进入系统 + </Button> + </DialogActions> + </Dialog> + ); +}; diff --git a/src/config/config.ts b/src/config/config.ts index be0afb4..71b03f3 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -27,7 +27,16 @@ export const config = { ) : ["junctions", "pipes", "valves", "reservoirs", "pumps", "tanks", "scada"], }; -export const NETWORK_NAME = process.env.NEXT_PUBLIC_NETWORK_NAME || "tjwater"; +export let NETWORK_NAME = process.env.NEXT_PUBLIC_NETWORK_NAME || "tjwater"; + +export const setNetworkName = (name: string) => { + NETWORK_NAME = name; +}; + +export const setMapWorkspace = (workspace: string) => { + config.MAP_WORKSPACE = workspace; +}; + export const MAPBOX_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || "pk.eyJ1IjoiemhpZnUiLCJhIjoiY205azNyNGY1MGkyZDJxcTJleDUwaHV1ZCJ9.wOmSdOnDDdre-mB1Lpy6Fg"; diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx new file mode 100644 index 0000000..19f4e06 --- /dev/null +++ b/src/contexts/ProjectContext.tsx @@ -0,0 +1,64 @@ +"use client"; +import React, { createContext, useContext, useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { config, NETWORK_NAME, setMapWorkspace, setNetworkName } from "@/config/config"; +import { ProjectSelector } from "@/components/project/ProjectSelector"; + +interface ProjectContextType { + workspace: string; + networkName: string; +} + +const ProjectContext = createContext<ProjectContextType | undefined>(undefined); + +export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { status } = useSession(); + const [isConfigured, setIsConfigured] = useState(false); + const [currentProject, setCurrentProject] = useState({ + workspace: config.MAP_WORKSPACE, + networkName: NETWORK_NAME || "tjwater", + }); + + useEffect(() => { + // Check localStorage + const savedWorkspace = localStorage.getItem("NEXT_PUBLIC_MAP_WORKSPACE"); + const savedNetwork = localStorage.getItem("NEXT_PUBLIC_NETWORK_NAME"); + + // If we have saved config, use it. + if (savedWorkspace && savedNetwork) { + applyConfig(savedWorkspace, savedNetwork); + } + }, []); + + const applyConfig = (ws: string, net: string) => { + setMapWorkspace(ws); + setNetworkName(net); + setCurrentProject({ workspace: ws, networkName: net }); + + // Save to localStorage + localStorage.setItem("NEXT_PUBLIC_MAP_WORKSPACE", ws); + localStorage.setItem("NEXT_PUBLIC_NETWORK_NAME", net); + + setIsConfigured(true); + }; + + // Only show selector if authenticated and not configured + if (status === "authenticated" && !isConfigured) { + return ( + <ProjectSelector + open={true} + onSelect={(ws, net) => applyConfig(ws, net)} + /> + ); + } + + return ( + <ProjectContext.Provider value={currentProject}> + {children} + </ProjectContext.Provider> + ); +}; + +export const useProject = () => useContext(ProjectContext); diff --git a/src/utils/mapQueryService.ts b/src/utils/mapQueryService.ts index 33ffff7..71db93b 100644 --- a/src/utils/mapQueryService.ts +++ b/src/utils/mapQueryService.ts @@ -40,15 +40,15 @@ interface MapClickEvent { // ========== 常量配置 ========== /** - * GeoServer 服务配置 + * GeoServer 服务配置获取函数 */ -const GEOSERVER_CONFIG = { +const getGeoserverConfig = () => ({ url: config.MAP_URL, workspace: config.MAP_WORKSPACE, layers: ["geo_pipes_mat", "geo_junctions_mat", "geo_valves"], wfsVersion: "1.0.0", outputFormat: "application/json", -} as const; +}); /** * 地图交互配置 @@ -176,7 +176,7 @@ const convertRenderFeatureToFeature = ( * @returns WFS 查询 URL */ const buildWfsUrl = (layer: string, orFilter: string): string => { - const { url, workspace, wfsVersion, outputFormat } = GEOSERVER_CONFIG; + const { url, workspace, wfsVersion, outputFormat } = getGeoserverConfig(); const params = new URLSearchParams({ service: "WFS", version: wfsVersion, @@ -233,7 +233,7 @@ const queryFeaturesByIds = async ( try { if (!layer) { // 查询所有配置的图层 - const promises = GEOSERVER_CONFIG.layers.map((layerName) => + const promises = getGeoserverConfig().layers.map((layerName) => fetchFeaturesFromLayer(layerName, orFilter) );