import { HeartHandshake, Sparkles, UserPen, UserRoundCheck, UserRoundPlus } from "lucide-react"; import { type FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { UserCreateRequest, UserResponse, UserStatus } from "../api/types"; import { ConfirmDialog, ManagementDialog } from "../components/OperationControls"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import { PageFrame } from "../components/PageFrame"; import type { PageComponentProps } from "../contracts/page"; import { type UserEditFormState, type UserListSource, type UserRemovalConfirmationState, userEditFormFromResponse } from "../contracts/users"; import { isPlatformAdmin } from "../contracts/workspace"; import { userCreateRequestFromDraft, userDeactivateRequest, userUpdateRequestFromEditForm } from "../schemas/users"; import { cx } from "../utils/classes"; const roleOptions = [ { value: "platform-admin", label: "平台管理员" }, { value: "server-owner", label: "服主" }, { value: "server-admin", label: "服务器管理员" } ]; const statusOptions: Array<{ value: UserStatus; label: string }> = [ { value: "active", label: "启用" }, { value: "pending", label: "待审核" }, { value: "disabled", label: "停用" } ]; interface UsersPageInitialState { users?: UserResponse[]; loading?: boolean; source?: UserListSource; loadError?: string; } interface UsersPageProps extends PageComponentProps { initialState?: UsersPageInitialState; } export function UsersPage({ session, operations, initialState }: UsersPageProps) { const [users, setUsers] = useState(initialState?.users ?? []); const [loading, setLoading] = useState(initialState?.loading ?? true); const [source, setSource] = useState(initialState?.source ?? "api"); const [loadError, setLoadError] = useState(initialState?.loadError ?? ""); const [result, setResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string }>(); const [createOpen, setCreateOpen] = useState(false); const [editingUserId, setEditingUserId] = useState(null); const [editDraft, setEditDraft] = useState(null); const [confirmRemoval, setConfirmRemoval] = useState(null); const [confirmBusy, setConfirmBusy] = useState(false); const [statusDrafts, setStatusDrafts] = useState>({}); const [keyword, setKeyword] = useState(""); const [roleFilter, setRoleFilter] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [draft, setDraft] = useState({ displayName: "", email: "", roles: ["server-admin"], status: "pending", profile: { phone: "", qq: "", contactNote: "" } }); const loadUsers = useCallback(async () => { if (initialState?.users) { setUsers(initialState.users); setLoading(initialState.loading ?? false); setSource(initialState.source ?? "api"); setLoadError(initialState.loadError ?? ""); return; } setLoading(true); setLoadError(""); try { const response = await platformApiClient.listUsers(); setUsers(response.items); setSource("api"); } catch (error) { setUsers([]); setSource("api"); setLoadError(error instanceof Error ? error.message : "账号 API 加载失败,未显示本地样例数据。"); } finally { setLoading(false); } }, [initialState]); useEffect(() => { void loadUsers(); }, [loadUsers]); useEffect(() => { setStatusDrafts(Object.fromEntries(users.map((user) => [user.id, user.status]))); }, [users]); const counts = useMemo( () => ({ active: users.filter((user) => user.status === "active").length, pending: users.filter((user) => user.status === "pending").length, roleCount: new Set(users.flatMap((user) => user.roles)).size }), [users] ); const persistenceDisabled = source === "local-development"; const filteredUsers = useMemo(() => { const query = keyword.trim().toLowerCase(); return users.filter((user) => { if (query && !`${user.displayName} ${user.email ?? ""} ${user.id}`.toLowerCase().includes(query)) { return false; } if (roleFilter && !user.roles.some((role) => role === roleFilter || role.replace("-", "") === roleFilter.replace("-", ""))) { return false; } return statusFilter === "all" || user.status === statusFilter; }); }, [keyword, roleFilter, statusFilter, users]); const hasActiveFilters = Boolean(keyword.trim() || roleFilter || statusFilter !== "all"); const mutationDisabled = persistenceDisabled || loading || Boolean(loadError); const createPending = operations.isPending("users", "邀请用户"); const editPending = editingUserId ? operations.isPending(editingUserId, "编辑用户") : false; const summaryAvailable = !loading && !loadError; async function createUser(event: FormEvent) { event.preventDefault(); const request = userCreateRequestFromDraft(draft); const operationId = operations.begin({ intent: "邀请用户", targetKind: "platform", targetId: "users", requester: session.displayName }); setResult({ status: "pending", label: `正在发送邀请 ${operationId}` }); try { const created = await platformApiClient.createUser(request); setUsers((current) => [created, ...current.filter((user) => user.id !== created.id)]); operations.succeed(operationId, `用户邀请已创建:${created.id}`); setSource("api"); setLoadError(""); setResult({ status: "succeeded", label: `已邀请 ${created.displayName}` }); setCreateOpen(false); setDraft({ displayName: "", email: "", roles: ["server-admin"], status: "pending", profile: { phone: "", qq: "", contactNote: "" } }); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "用户创建失败"); setResult({ status: "failed", label: "用户创建失败,未写入数据库" }); } } function startEdit(user: UserResponse) { setEditingUserId(user.id); setEditDraft(userEditFormFromResponse(user)); setResult(undefined); } function closeEdit() { setEditingUserId(null); setEditDraft(null); } function updateEdit(key: K, value: UserEditFormState[K]) { setEditDraft((current) => (current ? { ...current, [key]: value } : current)); } function toggleEditRole(role: string) { setEditDraft((current) => { if (!current) { return current; } const roles = current.roles.includes(role) ? current.roles.filter((item) => item !== role) : [...current.roles, role]; return { ...current, roles }; }); } async function saveUserEdit(event: FormEvent) { event.preventDefault(); if (!editingUserId || !editDraft) { return; } const operationId = operations.begin({ intent: "编辑用户", targetKind: "platform", targetId: editingUserId, requester: session.displayName }); setResult({ status: "pending", label: "正在保存用户" }); try { const updated = await platformApiClient.updateUser(editingUserId, userUpdateRequestFromEditForm(editDraft)); setUsers((current) => current.map((item) => (item.id === updated.id ? updated : item))); setEditingUserId(null); setEditDraft(null); setSource("api"); setLoadError(""); operations.succeed(operationId, `用户已更新:${updated.id}`); setResult({ status: "succeeded", label: `已更新 ${updated.displayName}` }); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "用户更新失败"); setResult({ status: "failed", label: error instanceof Error ? error.message : "用户更新失败,未写入数据库" }); } } async function deactivateUser() { if (!confirmRemoval) { return; } setConfirmBusy(true); const user = confirmRemoval.user; const operationId = operations.begin({ intent: "停用用户", targetKind: "platform", targetId: user.id, requester: session.displayName }); try { const updated = await platformApiClient.updateUser(user.id, userDeactivateRequest()); setUsers((current) => current.map((item) => (item.id === updated.id ? updated : item))); operations.succeed(operationId, `用户已停用:${updated.id}`); setSource("api"); setLoadError(""); setResult({ status: "succeeded", label: `${updated.displayName} 已停用` }); setConfirmRemoval(null); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "用户停用失败"); setResult({ status: "failed", label: error instanceof Error ? error.message : "停用失败,平台拒绝变更" }); } finally { setConfirmBusy(false); } } async function setUserStatus(user: UserResponse, status: UserStatus) { const operationId = operations.begin({ intent: "更新用户状态", targetKind: "platform", targetId: user.id, requester: session.displayName }); setResult({ status: "pending", label: `正在更新 ${user.displayName}` }); try { const updated = await platformApiClient.updateUser(user.id, { status }); setUsers((current) => current.map((item) => (item.id === updated.id ? updated : item))); operations.succeed(operationId, `用户状态已更新:${updated.id}`); setSource("api"); setLoadError(""); setResult({ status: "succeeded", label: `${updated.displayName} 已${statusLabel(updated.status)}` }); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "用户状态更新失败"); setResult({ status: "failed", label: "状态更新失败,未写入数据库" }); } } function toggleRole(role: string) { setDraft((current) => { const nextRoles = current.roles.includes(role) ? current.roles.filter((item) => item !== role) : [...current.roles, role]; return { ...current, roles: nextRoles }; }); } function applyUserStatus(user: UserResponse) { const nextStatus = statusDrafts[user.id] ?? user.status; if (nextStatus === user.status) { return; } if (nextStatus === "disabled") { setConfirmRemoval({ action: "deactivate", user }); return; } void setUserStatus(user, nextStatus); } if (!isPlatformAdmin(session)) { return (
当前账号不能管理用户

服务器用户不会看到用户管理导航;直接访问该页时应返回角色默认工作区。

); } return (
{loading && } {loadError && void loadUsers()} compact />}

访问列表

{loading ? : result && } {source === "local-development" ? "本地开发样例 / 仅查看" : loadError ? "API 不可用" : "平台数据"}
setKeyword(event.target.value)} /> {hasActiveFilters && }
{!loading && !loadError && users.length === 0 ? ( setCreateOpen(true)} /> ) : !loading && !loadError && filteredUsers.length === 0 ? ( { setKeyword(""); setRoleFilter(""); setStatusFilter("all"); }} /> ) : (
{filteredUsers.map((user) => (
{user.displayName} {user.email ?? user.id}
{statusLabel(user.status)} {user.roles.map(roleLabel).join(" / ")} {profileSummary(user)}
))}
)}
{ if (!createPending) setCreateOpen(false); }}>
邀请与审核流程 建议先以“待审核”创建账号;平台管理员确认身份后再启用。服务器范围在服务器详情的“管理成员”里绑定,避免给错全局权限。
{roleOptions.map((role) => ( ))}
{ if (!editPending) closeEdit(); }}> {editDraft && (
void saveUserEdit(event)}>
{roleOptions.map((role) => ( ))}
)}
{ if (confirmRemoval) { setStatusDrafts((current) => ({ ...current, [confirmRemoval.user.id]: confirmRemoval.user.status })); } setConfirmRemoval(null); }} onConfirm={() => void deactivateUser()} />
); } function AccessFlowGuide() { return (
邀请用户 默认待审核,先收集联系方式和备注。
审核申请 确认身份后启用;离职或拒绝统一走停用确认。
绑定服务器范围 服主/管理员的具体服务器范围在服务器详情中维护。
); } function RoleImpactGuide() { return (
角色影响 平台管理员:可管理用户、插件、AI 提供商和维护页。 服主:管理自己名下服务器,并可邀请或移除服务器管理员。 服务器管理员:只进入服务器工作区,适合日常值班。
); } function statusLabel(status: UserStatus): string { return status === "active" ? "启用" : status === "pending" ? "待审核" : "停用"; } function statusClass(status: UserStatus): string { return status === "active" ? "status-active" : status === "pending" ? "status-pending" : "status-disabled"; } function roleLabel(role: string): string { const matched = roleOptions.find((option) => option.value === role || option.value.replace("-", "") === role.toLowerCase()); return matched?.label ?? role; } function profileSummary(user: UserResponse): string { const parts = [user.profile?.phone, user.profile?.qq ? `QQ ${user.profile.qq}` : undefined, user.profile?.contactNote].filter(Boolean); return parts.length > 0 ? parts.join(" · ") : "暂无联系方式"; }