import { Title } from "@components/title"; import { Dialog, DialogContent, DialogActions, Button, Select, MenuItem, FormControl, InputLabel, TextField, Box, Typography, Fade, IconButton, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import { useEffect, useState } from "react"; import { apiFetch } from "@/lib/apiFetch"; import { config, NETWORK_NAME } from "@/config/config"; interface ProjectSelectorProps { open: boolean; onSelect: ( projectId: string, workspace: string, networkName: string, extent: number[], ) => void; onClose?: () => void; } type ProjectOption = { id: string; label: string; workspace: string; networkName: string; extent: number[]; description?: string | null; status?: string | null; projectRole?: string | null; }; export const ProjectSelector: React.FC = ({ open, onSelect, onClose, }) => { const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(false); const [loadError, setLoadError] = useState(null); const [projectId, setProjectId] = useState(""); const [projectIdError, setProjectIdError] = useState(null); const [workspace, setWorkspace] = useState(config.MAP_WORKSPACE); const [networkName, setNetworkName] = useState(NETWORK_NAME || "tjwater"); const [extent, setExtent] = useState(config.MAP_EXTENT); const [customMode, setCustomMode] = useState(false); useEffect(() => { const fetchProjects = async () => { setIsLoading(true); setLoadError(null); try { const response = await apiFetch( `${config.BACKEND_URL}/api/v1/meta/projects`, ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const mapped: ProjectOption[] = Array.isArray(data) ? data.map((item) => { const bbox = Array.isArray(item.map_extent?.bbox) ? item.map_extent.bbox.map((value: number) => Number(value)) : null; return { id: item.project_id, label: item.name || item.code || item.project_id, workspace: item.gs_workspace || config.MAP_WORKSPACE, networkName: item.code || NETWORK_NAME || config.MAP_WORKSPACE, extent: bbox && bbox.length === 4 ? bbox : config.MAP_EXTENT, description: item.description, status: item.status, projectRole: item.project_role, }; }) : []; setProjects(mapped); const savedProjectId = localStorage.getItem("active_project"); const initial = (savedProjectId && mapped.find((project) => project.id === savedProjectId)) || mapped[0]; if (initial) { setProjectId(initial.id); setWorkspace(initial.workspace); setNetworkName(initial.networkName); setExtent(initial.extent); setCustomMode(false); } else { setCustomMode(true); } } catch (error) { console.error("Failed to load projects:", error); setLoadError("项目列表加载失败,请使用自定义配置"); setCustomMode(true); } finally { setIsLoading(false); } }; fetchProjects(); }, []); const handleConfirm = () => { if (!projectId.trim()) { setProjectIdError("项目 ID 不能为空"); return; } setProjectIdError(null); onSelect(projectId.trim(), workspace, networkName, extent); }; return ( {onClose && ( theme.palette.grey[500], }} > )} </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={projectId} label="项目" onChange={(e) => { const val = e.target.value; if (val === "custom") { setCustomMode(true); setProjectIdError(null); } else { const p = projects.find((p) => p.id === val); if (p) { setProjectId(p.id); setWorkspace(p.workspace); setNetworkName(p.networkName); setExtent(p.extent); setProjectIdError(null); } } }} > {projects.length === 0 && ( <MenuItem value="" disabled> <Typography variant="body2" color="text.secondary"> {isLoading ? "正在加载项目..." : "暂无可用项目"} </Typography> </MenuItem> )} {projects.map((p) => ( <MenuItem key={p.id} value={p.id}> <Box sx={{ display: "flex", flexDirection: "column" }}> <Typography variant="body1">{p.label}</Typography> <Typography variant="caption" color="text.secondary"> 工作区: {p.workspace} | 管网: {p.networkName} </Typography> {(p.status || p.projectRole) && ( <Typography variant="caption" color="text.secondary"> {p.status ? `状态: ${p.status}` : ""} {p.status && p.projectRole ? " | " : ""} {p.projectRole ? `角色: ${p.projectRole}` : ""} </Typography> )} </Box> </MenuItem> ))} <MenuItem value="custom"> <Typography variant="body1">自定义配置...</Typography> </MenuItem> </Select> {loadError && ( <Typography variant="caption" color="error"> {loadError} </Typography> )} </FormControl> ) : ( <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> <TextField label="项目 ID" value={projectId} onChange={(e) => { setProjectId(e.target.value); setProjectIdError(null); }} fullWidth helperText={ projectIdError || "例如: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } error={Boolean(projectIdError)} /> <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> ); };