"use client"; import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import { Alert, alpha, Box, Button, Chip, CircularProgress, Divider, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, IconButton, InputLabel, MenuItem, Paper, Select, Stack, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tabs, TextField, Tooltip, Typography, } from "@mui/material"; import { Add as AddIcon, AdminPanelSettings as AdminPanelSettingsIcon, Block as BlockIcon, CheckCircle as CheckCircleIcon, Close as CloseIcon, Dns as DnsIcon, Groups as GroupsIcon, People as PeopleIcon, Refresh as RefreshIcon, RemoveCircleOutline as RemoveCircleOutlineIcon, Save as SaveIcon, Security as SecurityIcon, Storage as StorageIcon, } from "@mui/icons-material"; import { config } from "@config/config"; import { apiFetch } from "@/lib/apiFetch"; import { useProjectStore } from "@/store/projectStore"; type MetadataUser = { id: string; keycloak_id: string; username: string; email: string; role: string; is_active: boolean; is_superuser: boolean; }; type ProjectMember = { id: string; user_id: string; project_id: string; project_role: string; username: string; email: string; is_active: boolean; }; type AdminProject = { project_id: string; name: string; code: string; description?: string | null; gs_workspace: string; map_extent?: Record | null; status: string; }; type ProjectDatabaseConfig = { id: string; project_id: string; db_role: string; db_type: string; pool_min_size: number; pool_max_size: number; has_dsn: boolean; }; type DatabaseHealth = { ok: boolean; detail: string; }; const businessRoleOptions = [ { value: "admin", label: "系统管理员" }, { value: "operator", label: "运行人员" }, { value: "user", label: "普通用户" }, { value: "viewer", label: "只读用户" }, ]; const projectRoleLabels: Record = { owner: "项目负责人", admin: "项目管理员", member: "项目成员", viewer: "只读成员", }; const projectRoleOptions = [ { value: "admin", label: projectRoleLabels.admin }, { value: "member", label: projectRoleLabels.member }, { value: "viewer", label: projectRoleLabels.viewer }, ]; const projectStatusOptions = [ { value: "active", label: "启用" }, { value: "inactive", label: "停用" }, { value: "archived", label: "归档" }, ]; const databaseRoleOptions = [ { value: "biz_data", label: "业务数据库", helper: "PostgreSQL / 管网业务数据" }, { value: "iot_data", label: "时序数据库", helper: "TimescaleDB / SCADA 与实时数据" }, ]; const defaultProjectForm = { name: "", code: "", description: "", gs_workspace: "", map_extent: "", status: "active", }; const defaultDatabaseForms = { biz_data: { dsn: "", pool_min_size: 2, pool_max_size: 10, }, iot_data: { dsn: "", pool_min_size: 1, pool_max_size: 10, }, }; const createDefaultDatabaseForms = () => ({ biz_data: { ...defaultDatabaseForms.biz_data }, iot_data: { ...defaultDatabaseForms.iot_data }, }); const getBusinessRoleLabel = (role: string) => businessRoleOptions.find((option) => option.value === role)?.label ?? role; const getProjectRoleLabel = (role: string) => projectRoleLabels[role] ?? role; const formatJsonField = (value?: Record | null) => value ? JSON.stringify(value, null, 2) : ""; const parseOptionalJsonObject = (value: string, label: string) => { const trimmed = value.trim(); if (!trimmed) return null; const parsed = JSON.parse(trimmed); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`${label} 必须是 JSON 对象`); } return parsed as Record; }; const readErrorText = async (response: Response) => { const text = await response.text(); return text || `HTTP ${response.status}`; }; const isFastApiRouteNotFound = (response: Response, text: string) => response.status === 404 && text.includes('"detail":"Not Found"'); const normalizeDatabaseHealthDetail = (detail: string, ok: boolean) => { if (ok) return detail || "连通性测试通过"; const lowerDetail = detail.toLowerCase(); if (lowerDetail.includes("password authentication failed")) { return "连通性测试失败:用户名或密码错误,请检查 DSN 中的账号密码。"; } if (lowerDetail.includes("connection refused")) { return "连通性测试失败:目标主机或端口拒绝连接,请检查地址、端口和服务状态。"; } if (lowerDetail.includes("timeout") || lowerDetail.includes("timed out")) { return "连通性测试失败:连接超时,请检查网络、防火墙和数据库服务状态。"; } if ( lowerDetail.includes("could not translate host name") || lowerDetail.includes("name or service not known") ) { return "连通性测试失败:数据库主机名无法解析,请检查 DSN 中的主机地址。"; } if (detail.startsWith("数据库连接失败:")) { return detail.replace("数据库连接失败:", "连通性测试失败:"); } return detail || "连通性测试失败"; }; const parseDatabaseHealthText = (text: string): DatabaseHealth | null => { try { const payload = JSON.parse(text); if (payload && typeof payload === "object" && "ok" in payload) { return { ok: Boolean(payload.ok), detail: String(payload.detail ?? ""), }; } } catch { return null; } return null; }; const cardSx = { borderRadius: 2, borderColor: "divider", boxShadow: "0 10px 30px rgba(15, 23, 42, 0.06)", }; const tableSx = { minWidth: 720, "& .MuiTableCell-head": { bgcolor: "action.hover", color: "text.secondary", fontSize: 12, fontWeight: 700, letterSpacing: 0, }, "& .MuiTableRow-root:last-child .MuiTableCell-body": { borderBottom: 0, }, }; type StatCardProps = { icon: React.ReactNode; label: string; value: string | number; tone?: "primary" | "success" | "warning" | "info"; }; const StatCard = ({ icon, label, value, tone = "primary" }: StatCardProps) => ( ({ ...cardSx, p: 2, flex: "1 1 180px", minWidth: 0, bgcolor: alpha(theme.palette[tone].main, 0.04), })} > ({ width: 40, height: 40, borderRadius: 1.5, display: "grid", placeItems: "center", color: `${tone}.main`, bgcolor: alpha(theme.palette[tone].main, 0.12), })} > {icon} {label} {value} ); const SectionHeader = ({ title, description, action, }: { title: string; description?: string; action?: React.ReactNode; }) => ( {title} {description && ( {description} )} {action} ); const StatusChip = ({ active }: { active: boolean }) => ( : } label={active ? "启用" : "禁用"} variant={active ? "filled" : "outlined"} sx={{ minWidth: 76, justifyContent: "flex-start" }} /> ); const EmptyRow = ({ colSpan, label }: { colSpan: number; label: string }) => ( {label} ); export const SystemAdminPanel = () => { const currentProjectId = useProjectStore((state) => state.currentProjectId); const [tab, setTab] = useState(0); const [users, setUsers] = useState([]); const [members, setMembers] = useState([]); const [projects, setProjects] = useState([]); const [databases, setDatabases] = useState([]); const [currentAdmin, setCurrentAdmin] = useState(null); const [adminChecked, setAdminChecked] = useState(false); const [isAuthorized, setIsAuthorized] = useState(false); const [metadataConfigAvailable, setMetadataConfigAvailable] = useState(true); const [projectId, setProjectId] = useState(currentProjectId ?? ""); const [hasLoadedCurrentProjectMembers, setHasLoadedCurrentProjectMembers] = useState(false); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const [memberForm, setMemberForm] = useState({ user_id: "", project_role: "viewer", }); const [projectForm, setProjectForm] = useState(defaultProjectForm); const [createProjectOpen, setCreateProjectOpen] = useState(false); const [createProjectForm, setCreateProjectForm] = useState(defaultProjectForm); const [databaseForms, setDatabaseForms] = useState(createDefaultDatabaseForms); const [databaseHealth, setDatabaseHealth] = useState< Record >({ biz_data: null, iot_data: null, }); const activeUsers = useMemo( () => users.filter((user) => user.is_active).length, [users], ); const systemAdminUsers = useMemo( () => users.filter((user) => user.role === "admin").length, [users], ); const superUsers = useMemo( () => users.filter((user) => user.is_superuser).length, [users], ); const memberUserIds = useMemo( () => new Set(members.map((member) => member.user_id)), [members], ); const availableMemberUsers = useMemo( () => users.filter( (user) => user.is_active && !user.is_superuser && user.id !== currentAdmin?.id && !memberUserIds.has(user.id), ), [currentAdmin?.id, memberUserIds, users], ); const selectedMemberUser = useMemo( () => users.find((user) => user.id === memberForm.user_id), [memberForm.user_id, users], ); const hasProjectId = Boolean(projectId.trim()); const selectedProject = useMemo( () => projects.find((project) => project.project_id === projectId.trim()), [projectId, projects], ); const databasesByRole = useMemo( () => new Map( databases.map((database) => [database.db_role, database] as const), ), [databases], ); const loadUsers = useCallback(async () => { const response = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/users`); if (!response.ok) { throw new Error(await readErrorText(response)); } setUsers(await response.json()); }, []); const applyProjectForm = useCallback((project: AdminProject | null) => { if (!project) { setProjectForm(defaultProjectForm); return; } setProjectForm({ name: project.name ?? "", code: project.code ?? "", description: project.description ?? "", gs_workspace: project.gs_workspace ?? "", map_extent: formatJsonField(project.map_extent), status: project.status || "active", }); }, []); const loadProjects = useCallback(async (preferredProjectId?: string) => { const response = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/projects`); if (!response.ok) { const errorText = await readErrorText(response); if (isFastApiRouteNotFound(response, errorText)) { setMetadataConfigAvailable(false); setProjects([]); applyProjectForm(null); return; } throw new Error(errorText); } setMetadataConfigAvailable(true); const payload = (await response.json()) as AdminProject[]; setProjects(payload); const activeProjectId = preferredProjectId || projectId.trim() || currentProjectId || payload[0]?.project_id || ""; if (activeProjectId && !projectId.trim()) { setProjectId(activeProjectId); } applyProjectForm( payload.find((project) => project.project_id === activeProjectId) ?? null, ); }, [applyProjectForm, currentProjectId, projectId]); const loadMembers = useCallback(async () => { if (!projectId.trim()) return; const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/members`, ); if (!response.ok) { throw new Error(await readErrorText(response)); } setMembers(await response.json()); }, [projectId]); const loadDatabases = useCallback(async () => { if (!projectId.trim()) return; setDatabaseHealth({ biz_data: null, iot_data: null }); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/databases`, ); if (!response.ok) { const errorText = await readErrorText(response); if (isFastApiRouteNotFound(response, errorText)) { setMetadataConfigAvailable(false); setDatabases([]); return; } throw new Error(errorText); } setMetadataConfigAvailable(true); const payload = (await response.json()) as ProjectDatabaseConfig[]; setDatabases(payload); setDatabaseForms((current) => { const next = createDefaultDatabaseForms(); for (const database of payload) { const role = database.db_role as keyof typeof defaultDatabaseForms; if (role in next) { next[role] = { ...current[role], dsn: "", pool_min_size: database.pool_min_size, pool_max_size: database.pool_max_size, }; } } return next; }); }, [projectId]); useEffect(() => { let cancelled = false; const loadInitialState = async () => { try { const adminResponse = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/me`, { projectHeaderMode: "omit", skipAuthRedirect: true, }); if (!adminResponse.ok) { if (!cancelled) { setIsAuthorized(false); setCurrentAdmin(null); setAdminChecked(true); } return; } const adminPayload = await adminResponse.json(); if (!cancelled) { setCurrentAdmin(adminPayload); setIsAuthorized(true); setAdminChecked(true); } const usersResponse = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/users`); if (!usersResponse.ok) { throw new Error(await readErrorText(usersResponse)); } const payload = await usersResponse.json(); if (!cancelled) setUsers(payload); const projectsResponse = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/projects`); if (!projectsResponse.ok) { const errorText = await readErrorText(projectsResponse); if (isFastApiRouteNotFound(projectsResponse, errorText)) { if (!cancelled) { setMetadataConfigAvailable(false); setProjects([]); applyProjectForm(null); } return; } throw new Error(errorText); } if (!cancelled) setMetadataConfigAvailable(true); const projectsPayload = (await projectsResponse.json()) as AdminProject[]; if (!cancelled) { setProjects(projectsPayload); const initialProjectId = currentProjectId || projectsPayload[0]?.project_id || ""; setProjectId((current) => current.trim() || !initialProjectId ? current : initialProjectId, ); applyProjectForm( projectsPayload.find( (project) => project.project_id === initialProjectId, ) ?? null, ); } } catch (err) { if (!cancelled) { setAdminChecked(true); setError(String(err)); } } }; void loadInitialState(); return () => { cancelled = true; }; }, [applyProjectForm, currentProjectId]); useEffect(() => { if (!currentProjectId || projectId.trim()) return; setProjectId(currentProjectId); }, [currentProjectId, projectId]); useEffect(() => { if (!isAuthorized || !hasProjectId || hasLoadedCurrentProjectMembers) return; setHasLoadedCurrentProjectMembers(true); loadMembers().catch((err) => setError(String(err))); }, [hasLoadedCurrentProjectMembers, hasProjectId, isAuthorized, loadMembers]); useEffect(() => { if (!selectedProject) return; applyProjectForm(selectedProject); }, [applyProjectForm, selectedProject]); useEffect(() => { if (!isAuthorized || !hasProjectId || !metadataConfigAvailable) return; setDatabaseHealth({ biz_data: null, iot_data: null }); loadDatabases().catch((err) => setError(String(err))); }, [ hasProjectId, isAuthorized, loadDatabases, metadataConfigAvailable, ]); useEffect(() => { if (!metadataConfigAvailable && tab !== 3) { setTab(3); } }, [metadataConfigAvailable, tab]); const updateUserActive = async (user: MetadataUser, isActive: boolean) => { setError(null); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/users/${user.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ is_active: isActive }), }, ); if (!response.ok) { setError(await readErrorText(response)); return; } await loadUsers(); }; const updateUserRole = async (user: MetadataUser, role: string) => { setError(null); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/users/${user.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role }), }, ); if (!response.ok) { setError(await readErrorText(response)); return; } await loadUsers(); }; const addMember = async (event: FormEvent) => { event.preventDefault(); setError(null); setMessage(null); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/members`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(memberForm), }, ); if (!response.ok) { setError(await readErrorText(response)); return; } setMessage("项目成员已添加"); setMemberForm((prev) => ({ ...prev, user_id: "" })); await loadMembers(); }; const updateMemberRole = async (member: ProjectMember, projectRole: string) => { setError(null); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${member.project_id}/members/${member.user_id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ project_role: projectRole }), }, ); if (!response.ok) { setError(await readErrorText(response)); return; } await loadMembers(); }; const removeMember = async (member: ProjectMember) => { setError(null); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${member.project_id}/members/${member.user_id}`, { method: "DELETE" }, ); if (!response.ok) { setError(await readErrorText(response)); return; } setMessage("项目成员已移除"); await loadMembers(); }; const startNewProject = () => { setCreateProjectForm(defaultProjectForm); setCreateProjectOpen(true); }; const selectProject = (value: string) => { setProjectId(value); setHasLoadedCurrentProjectMembers(false); setMembers([]); setDatabases([]); setDatabaseForms(createDefaultDatabaseForms()); setDatabaseHealth({ biz_data: null, iot_data: null }); }; const resetDatabaseHealth = (role: keyof typeof defaultDatabaseForms) => { setDatabaseHealth((current) => ({ ...current, [role]: null })); }; const buildProjectPayload = (form: typeof defaultProjectForm) => ({ name: form.name.trim(), code: form.code.trim(), description: form.description.trim() || null, gs_workspace: form.gs_workspace.trim(), map_extent: parseOptionalJsonObject(form.map_extent, "地图范围"), status: form.status, }); const saveProject = async (event: FormEvent) => { event.preventDefault(); if (!selectedProject?.project_id) { setError("请先选择要编辑的项目。"); return; } setError(null); setMessage(null); try { const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${selectedProject.project_id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildProjectPayload(projectForm)), }, ); if (!response.ok) { throw new Error(await readErrorText(response)); } const saved = (await response.json()) as AdminProject; setHasLoadedCurrentProjectMembers(false); setProjectId(saved.project_id); applyProjectForm(saved); await loadProjects(saved.project_id); setMessage("项目配置已更新"); } catch (err) { setError(String(err)); } }; const createProject = async (event: FormEvent) => { event.preventDefault(); setError(null); setMessage(null); try { const response = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/projects`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildProjectPayload(createProjectForm)), }); if (!response.ok) { throw new Error(await readErrorText(response)); } const saved = (await response.json()) as AdminProject; setCreateProjectOpen(false); setCreateProjectForm(defaultProjectForm); setHasLoadedCurrentProjectMembers(false); setProjectId(saved.project_id); applyProjectForm(saved); await loadProjects(saved.project_id); setMessage("项目已创建"); } catch (err) { setError(String(err)); } }; const saveDatabase = async (role: keyof typeof defaultDatabaseForms) => { if (!projectId.trim()) return; const form = databaseForms[role]; if (!form.dsn.trim()) { setError("请填写新的 DSN 并通过连通性测试后再保存。"); return; } if (!databaseHealth[role]?.ok) { setError("请先通过连通性测试再保存数据库配置。"); return; } setError(null); setMessage(null); const payload: Record = { db_role: role, pool_min_size: Number(form.pool_min_size), pool_max_size: Number(form.pool_max_size), }; if (form.dsn.trim()) { payload.dsn = form.dsn.trim(); } const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/databases`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, ); if (!response.ok) { const errorText = await readErrorText(response); if (isFastApiRouteNotFound(response, errorText)) { setMetadataConfigAvailable(false); return; } setError(errorText); return; } setDatabaseForms((current) => ({ ...current, [role]: { ...current[role], dsn: "" }, })); setMessage(`${databaseRoleOptions.find((item) => item.value === role)?.label}已保存`); await loadDatabases(); }; const checkDatabaseHealth = async (role: keyof typeof defaultDatabaseForms) => { if (!projectId.trim()) return; setError(null); setDatabaseHealth((current) => ({ ...current, [role]: null })); const form = databaseForms[role]; const body = form.dsn.trim() ? { dsn: form.dsn.trim() } : {}; const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/databases/${role}/health`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }, ); if (!response.ok) { const errorText = await readErrorText(response); if (isFastApiRouteNotFound(response, errorText)) { setMetadataConfigAvailable(false); return; } const healthPayload = parseDatabaseHealthText(errorText); if (healthPayload) { setDatabaseHealth((current) => ({ ...current, [role]: { ...healthPayload, detail: normalizeDatabaseHealthDetail( healthPayload.detail, healthPayload.ok, ), }, })); return; } setError(normalizeDatabaseHealthDetail(errorText, false)); return; } const payload = (await response.json()) as DatabaseHealth; setDatabaseHealth((current) => ({ ...current, [role]: { ...payload, detail: normalizeDatabaseHealthDetail(payload.detail, payload.ok), }, })); }; const deleteDatabase = async (role: keyof typeof defaultDatabaseForms) => { if (!projectId.trim()) return; setError(null); const response = await apiFetch( `${config.BACKEND_URL}/api/v1/admin/projects/${projectId.trim()}/databases/${role}`, { method: "DELETE" }, ); if (!response.ok) { const errorText = await readErrorText(response); if (isFastApiRouteNotFound(response, errorText)) { setMetadataConfigAvailable(false); return; } setError(errorText); return; } setMessage(`${databaseRoleOptions.find((item) => item.value === role)?.label}配置已删除`); await loadDatabases(); }; return ( ({ ...cardSx, p: { xs: 2, md: 3 }, bgcolor: theme.palette.mode === "dark" ? alpha(theme.palette.primary.main, 0.1) : alpha(theme.palette.primary.main, 0.04), })} > ({ width: 48, height: 48, borderRadius: 2, display: "grid", placeItems: "center", color: "primary.main", bgcolor: alpha(theme.palette.primary.main, 0.12), })} > 系统管理 Keycloak 负责登录身份,系统配置库负责系统角色、账号状态和项目权限 {adminChecked && isAuthorized && ( } label="管理员权限已验证" sx={{ alignSelf: { xs: "flex-start", md: "center" } }} /> )} {!adminChecked && ( 正在校验系统管理权限 )} {adminChecked && !isAuthorized && ( 无系统管理权限 )} {message && ( setMessage(null)} sx={{ borderRadius: 2 }}> {message} )} {error && ( setError(null)} sx={{ borderRadius: 2 }}> {error} )} {isAuthorized && !metadataConfigAvailable && ( 后端尚未启用项目配置接口,请重启或部署包含 /api/v1/admin/projects 的后端后再使用项目配置和数据库配置。 )} {isAuthorized && ( <> } label="系统用户" value={users.length} /> } label="启用用户" value={activeUsers} tone="success" /> } label="系统管理员角色" value={systemAdminUsers} tone="warning" /> } label="超级管理员" value={superUsers} tone="info" /> } label={hasProjectId ? "当前项目成员" : "项目成员未加载"} value={hasProjectId ? members.length : "-"} tone="info" /> } label="系统项目" value={projects.length} tone="primary" /> {metadataConfigAvailable && ( 当前管理项目 {selectedProject ? `${selectedProject.name} / ${selectedProject.code}` : "未选择项目"} 选择项目 )} setTab(value)} variant="scrollable" scrollButtons="auto" sx={{ px: 2, minHeight: 56, borderBottom: 1, borderColor: "divider", "& .MuiTab-root": { minHeight: 56, textTransform: "none", fontWeight: 700, }, }} > } iconPosition="start" label="项目成员" disabled={!metadataConfigAvailable} /> } iconPosition="start" label="项目配置" disabled={!metadataConfigAvailable} /> } iconPosition="start" label="数据库配置" disabled={!metadataConfigAvailable} /> } iconPosition="start" label="系统用户" /> {tab === 3 && ( } variant="outlined" onClick={loadUsers}> 刷新 } /> 用户身份 系统角色 超级权限 状态 操作 {users.length === 0 && } {users.map((user) => { const isSelf = user.id === currentAdmin?.id; const protectedUserReason = isSelf ? "不能操作当前登录用户" : user.is_superuser ? "超级管理员权限独立于系统角色,需后台设置" : ""; return ( {user.username} {isSelf && ( )} {user.email} {user.keycloak_id} {user.is_superuser ? ( } label="超级管理员" /> ) : ( )} ); })}
)} {tab === 0 && ( } onClick={loadMembers} disabled={!hasProjectId} > 刷新成员 } /> {!hasProjectId && ( 当前未选择管理项目。 )} {hasProjectId && ( ({ width: 40, height: 40, borderRadius: 1.5, display: "grid", placeItems: "center", color: "primary.main", bgcolor: alpha(theme.palette.primary.main, 0.12), flexShrink: 0, })} > 当前管理项目 {selectedProject?.name ?? "未命名项目"} {selectedProject ? `${selectedProject.code} · ${projectId}` : projectId} {selectedProject && ( option.value === selectedProject.status, )?.label ?? selectedProject.status } /> )} } label={`${members.length} 名成员`} /> )} 添加成员 选择用户 项目角色 用户 邮箱 项目角色 状态 操作 {members.length === 0 && ( )} {members.map((member) => { const isSelf = member.user_id === currentAdmin?.id; const isOwner = member.project_role === "owner"; return ( {member.username} {isSelf && ( )} {member.user_id} {member.email} {isOwner ? ( ) : ( )} ); })}
)} {tab === 1 && ( } /> {!selectedProject && ( 当前未选择管理项目。请在页面顶部选择项目,或使用“新建项目”创建项目。 )} setProjectForm({ ...projectForm, name: event.target.value }) } disabled={!selectedProject} required fullWidth /> setProjectForm({ ...projectForm, code: event.target.value }) } disabled={!selectedProject} required fullWidth /> setProjectForm({ ...projectForm, gs_workspace: event.target.value, }) } disabled={!selectedProject} required fullWidth /> 状态 setProjectForm({ ...projectForm, description: event.target.value, }) } disabled={!selectedProject} fullWidth multiline minRows={2} /> setProjectForm({ ...projectForm, map_extent: event.target.value, }) } disabled={!selectedProject} fullWidth multiline minRows={4} placeholder='{"bbox":[120.1,30.1,120.2,30.2]}' />
)} {tab === 2 && ( } variant="outlined" onClick={loadDatabases} disabled={!hasProjectId} > 刷新配置 } /> {!hasProjectId && ( 当前未选择管理项目。 )} {hasProjectId && databaseRoleOptions.map((roleOption) => { const role = roleOption.value as keyof typeof defaultDatabaseForms; const form = databaseForms[role]; const configRecord = databasesByRole.get(role); const health = databaseHealth[role]; return ( {roleOption.label} {roleOption.helper} { resetDatabaseHealth(role); setDatabaseForms((current) => ({ ...current, [role]: { ...current[role], dsn: event.target.value }, })); }} placeholder="postgresql://user:password@host:5432/db" helperText={ configRecord?.has_dsn ? "填写新 DSN 后测试新连接,通过后才能保存;留空只测试已保存 DSN。" : "首次保存前必须填写 DSN 并通过连通性测试。" } fullWidth /> { resetDatabaseHealth(role); setDatabaseForms((current) => ({ ...current, [role]: { ...current[role], pool_min_size: Number(event.target.value), }, })); }} sx={{ width: { xs: "100%", md: 120 } }} /> { resetDatabaseHealth(role); setDatabaseForms((current) => ({ ...current, [role]: { ...current[role], pool_max_size: Number(event.target.value), }, })); }} sx={{ width: { xs: "100%", md: 120 } }} /> {health && ( {health.detail} )} ); })} )}
setCreateProjectOpen(false)} fullWidth maxWidth="md" PaperProps={{ sx: { borderRadius: 2, overflow: "hidden", }, }} > ({ width: 40, height: 40, borderRadius: 1.5, display: "grid", placeItems: "center", color: "primary.main", bgcolor: alpha(theme.palette.primary.main, 0.12), flexShrink: 0, })} > 新建项目 系统项目基础配置 setCreateProjectOpen(false)} edge="end" > 基础信息 setCreateProjectForm({ ...createProjectForm, name: event.target.value, }) } required fullWidth /> setCreateProjectForm({ ...createProjectForm, code: event.target.value, }) } required fullWidth /> 服务配置 setCreateProjectForm({ ...createProjectForm, gs_workspace: event.target.value, }) } required fullWidth /> 状态 描述与范围 setCreateProjectForm({ ...createProjectForm, description: event.target.value, }) } fullWidth multiline minRows={2} /> setCreateProjectForm({ ...createProjectForm, map_extent: event.target.value, }) } fullWidth multiline minRows={4} placeholder='{"bbox":[120.1,30.1,120.2,30.2]}' /> )} ); };