543 lines
25 KiB
TypeScript
543 lines
25 KiB
TypeScript
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<UserResponse[]>(initialState?.users ?? []);
|
|
const [loading, setLoading] = useState(initialState?.loading ?? true);
|
|
const [source, setSource] = useState<UserListSource>(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<string | null>(null);
|
|
const [editDraft, setEditDraft] = useState<UserEditFormState | null>(null);
|
|
const [confirmRemoval, setConfirmRemoval] = useState<UserRemovalConfirmationState | null>(null);
|
|
const [confirmBusy, setConfirmBusy] = useState(false);
|
|
const [statusDrafts, setStatusDrafts] = useState<Record<string, UserStatus>>({});
|
|
const [keyword, setKeyword] = useState("");
|
|
const [roleFilter, setRoleFilter] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState<UserStatus | "all">("all");
|
|
const [draft, setDraft] = useState<UserCreateRequest>({
|
|
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<HTMLFormElement>) {
|
|
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<K extends keyof UserEditFormState>(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<HTMLFormElement>) {
|
|
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 (
|
|
<div className="console-page">
|
|
<PageFrame kicker="身份" title="用户管理" status="无访问权限" metrics={[]} />
|
|
<section className="console-panel">
|
|
<div className="state-view state-error" role="alert">
|
|
<HeartHandshake size={24} />
|
|
<strong>当前账号不能管理用户</strong>
|
|
<p>服务器用户不会看到用户管理导航;直接访问该页时应返回角色默认工作区。</p>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="console-page">
|
|
<PageFrame
|
|
kicker="身份"
|
|
title="用户管理"
|
|
status={loadError ? "账号 API 不可用" : source === "api" ? "账号 API 已连接" : "本地开发数据"}
|
|
metrics={[
|
|
{ label: "用户", value: summaryAvailable ? `${users.length}` : "--", tone: "success" },
|
|
{ label: "角色", value: summaryAvailable ? `${counts.roleCount}` : "--", tone: "neutral" },
|
|
{ label: "待审核", value: summaryAvailable ? `${counts.pending}` : "--", tone: "warning" }
|
|
]}
|
|
/>
|
|
|
|
{loading && <LoadingState label="正在加载用户列表…" />}
|
|
{loadError && <ErrorState title="用户 API 暂不可用" reason={loadError} diagnosticId="user-management:list" onRetry={() => void loadUsers()} compact />}
|
|
|
|
<section className="console-panel">
|
|
<div className="panel-header">
|
|
<h2>访问列表</h2>
|
|
{loading ? <ResultBadge status="pending" label="加载用户…" /> : result && <ResultBadge status={result.status} label={result.label} />}
|
|
<span className="page-status">{source === "local-development" ? "本地开发样例 / 仅查看" : loadError ? "API 不可用" : "平台数据"}</span>
|
|
<button type="button" className="primary-command" disabled={mutationDisabled} onClick={() => setCreateOpen(true)}>
|
|
<UserRoundPlus size={14} />
|
|
<span>{mutationDisabled ? "等待 API" : "邀请用户"}</span>
|
|
</button>
|
|
</div>
|
|
<AccessFlowGuide />
|
|
<RoleImpactGuide />
|
|
<div className="resource-filter-bar" role="search" aria-label="用户筛选">
|
|
<input type="search" value={keyword} placeholder="搜索名称、邮箱或用户 ID" aria-label="搜索用户" onChange={(event) => setKeyword(event.target.value)} />
|
|
<select value={roleFilter} aria-label="按角色筛选" onChange={(event) => setRoleFilter(event.target.value)}>
|
|
<option value="">全部角色</option>
|
|
{roleOptions.map((role) => <option key={role.value} value={role.value}>{role.label}</option>)}
|
|
</select>
|
|
<select value={statusFilter} aria-label="按状态筛选" onChange={(event) => setStatusFilter(event.target.value as UserStatus | "all")}>
|
|
<option value="all">全部状态</option>
|
|
{statusOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
|
</select>
|
|
{hasActiveFilters && <button type="button" className="inline-link-command" onClick={() => { setKeyword(""); setRoleFilter(""); setStatusFilter("all"); }}>清除筛选</button>}
|
|
</div>
|
|
{!loading && !loadError && users.length === 0 ? (
|
|
<EmptyState title="暂无用户" description="平台暂未返回可管理账号。邀请用户后会在这里显示 API 连接结果。" actionLabel="邀请用户" onAction={() => setCreateOpen(true)} />
|
|
) : !loading && !loadError && filteredUsers.length === 0 ? (
|
|
<EmptyState title="没有匹配用户" description="调整名称、角色或状态筛选后再试。" actionLabel="清除筛选" onAction={() => { setKeyword(""); setRoleFilter(""); setStatusFilter("all"); }} />
|
|
) : (
|
|
<div className="resource-list user-management-list">
|
|
{filteredUsers.map((user) => (
|
|
<article key={user.id} className="resource-list-item user-management-item">
|
|
<div>
|
|
<strong>{user.displayName}</strong>
|
|
<span className="provider-id">{user.email ?? user.id}</span>
|
|
</div>
|
|
<span className={cx("status-pill", statusClass(user.status))}>
|
|
<UserRoundCheck size={13} />
|
|
{statusLabel(user.status)}
|
|
</span>
|
|
<span>{user.roles.map(roleLabel).join(" / ")}</span>
|
|
<span>{profileSummary(user)}</span>
|
|
<div className="user-actions" aria-label={`${user.displayName} 状态操作`}>
|
|
<button type="button" className="theme-upload" disabled={mutationDisabled || operations.isPending(user.id, "编辑用户")} aria-label={`编辑 ${user.displayName}`} onClick={() => startEdit(user)}>
|
|
<UserPen size={13} />
|
|
编辑
|
|
</button>
|
|
<label className="status-update-control">
|
|
<span>状态</span>
|
|
<select
|
|
value={statusDrafts[user.id] ?? user.status}
|
|
disabled={mutationDisabled || operations.isPending(user.id, "更新用户状态")}
|
|
aria-label={`选择 ${user.displayName} 状态`}
|
|
onChange={(event) => setStatusDrafts((current) => ({ ...current, [user.id]: event.target.value as UserStatus }))}
|
|
>
|
|
{statusOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="theme-upload"
|
|
disabled={mutationDisabled || operations.isPending(user.id, "更新用户状态") || (statusDrafts[user.id] ?? user.status) === user.status}
|
|
aria-label={`应用 ${user.displayName} 状态变更`}
|
|
onClick={() => applyUserStatus(user)}
|
|
>
|
|
<UserRoundCheck size={13} />
|
|
应用状态
|
|
</button>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<ManagementDialog open={createOpen} title="邀请用户" wide onClose={() => { if (!createPending) setCreateOpen(false); }}>
|
|
<form className="management-form dialog-form" onSubmit={createUser}>
|
|
<div className="form-guidance management-form-wide">
|
|
<strong>邀请与审核流程</strong>
|
|
<span>建议先以“待审核”创建账号;平台管理员确认身份后再启用。服务器范围在服务器详情的“管理成员”里绑定,避免给错全局权限。</span>
|
|
</div>
|
|
<label>
|
|
<span>显示名称</span>
|
|
<input value={draft.displayName} required onChange={(event) => setDraft((current) => ({ ...current, displayName: event.target.value }))} />
|
|
</label>
|
|
<label>
|
|
<span>邮箱</span>
|
|
<input value={draft.email} type="email" onChange={(event) => setDraft((current) => ({ ...current, email: event.target.value }))} />
|
|
</label>
|
|
<label>
|
|
<span>手机号</span>
|
|
<input
|
|
value={draft.profile?.phone ?? ""}
|
|
inputMode="tel"
|
|
onChange={(event) => setDraft((current) => ({ ...current, profile: { ...current.profile, phone: event.target.value } }))}
|
|
/>
|
|
</label>
|
|
<label>
|
|
<span>QQ</span>
|
|
<input
|
|
value={draft.profile?.qq ?? ""}
|
|
inputMode="numeric"
|
|
onChange={(event) => setDraft((current) => ({ ...current, profile: { ...current.profile, qq: event.target.value } }))}
|
|
/>
|
|
</label>
|
|
<label>
|
|
<span>备注</span>
|
|
<input
|
|
value={draft.profile?.contactNote ?? ""}
|
|
onChange={(event) => setDraft((current) => ({ ...current, profile: { ...current.profile, contactNote: event.target.value } }))}
|
|
/>
|
|
</label>
|
|
<label>
|
|
<span>状态</span>
|
|
<select value={draft.status} onChange={(event) => setDraft((current) => ({ ...current, status: event.target.value as UserStatus }))}>
|
|
{statusOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<div className="role-selector" aria-label="角色">
|
|
{roleOptions.map((role) => (
|
|
<button key={role.value} type="button" className={cx("role-chip", draft.roles.includes(role.value) && "role-chip-active")} onClick={() => toggleRole(role.value)}>
|
|
<Sparkles size={13} />
|
|
<span>{role.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<RoleImpactGuide />
|
|
<div className="confirm-actions">
|
|
<button type="button" disabled={createPending} onClick={() => setCreateOpen(false)}>
|
|
取消
|
|
</button>
|
|
<button type="submit" className="confirm-primary" disabled={mutationDisabled || createPending}>
|
|
<UserRoundPlus size={14} />
|
|
<span>{mutationDisabled ? "等待 API" : createPending ? "发送中…" : "发送邀请"}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</ManagementDialog>
|
|
|
|
<ManagementDialog open={editDraft !== null} title="编辑用户" wide onClose={() => { if (!editPending) closeEdit(); }}>
|
|
{editDraft && (
|
|
<form className="management-form dialog-form" onSubmit={(event) => void saveUserEdit(event)}>
|
|
<label>
|
|
<span>显示名称</span>
|
|
<input value={editDraft.displayName} required onChange={(event) => updateEdit("displayName", event.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>邮箱</span>
|
|
<input value={editDraft.email} type="email" onChange={(event) => updateEdit("email", event.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>手机号</span>
|
|
<input value={editDraft.phone} inputMode="tel" onChange={(event) => updateEdit("phone", event.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>QQ</span>
|
|
<input value={editDraft.qq} inputMode="numeric" onChange={(event) => updateEdit("qq", event.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>备注</span>
|
|
<input value={editDraft.contactNote} onChange={(event) => updateEdit("contactNote", event.target.value)} />
|
|
</label>
|
|
<label>
|
|
<span>状态</span>
|
|
<select value={editDraft.status} onChange={(event) => updateEdit("status", event.target.value as UserStatus)}>
|
|
{statusOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<div className="role-selector" aria-label="编辑角色">
|
|
{roleOptions.map((role) => (
|
|
<button key={role.value} type="button" className={cx("role-chip", editDraft.roles.includes(role.value) && "role-chip-active")} onClick={() => toggleEditRole(role.value)}>
|
|
<Sparkles size={13} />
|
|
<span>{role.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<RoleImpactGuide />
|
|
<div className="confirm-actions">
|
|
<button type="button" disabled={editPending} onClick={closeEdit}>
|
|
取消
|
|
</button>
|
|
<button type="submit" className="confirm-primary" disabled={editPending}>
|
|
<UserPen size={14} />
|
|
<span>{editPending ? "保存中…" : "保存用户"}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</ManagementDialog>
|
|
|
|
<ConfirmDialog
|
|
open={confirmRemoval !== null}
|
|
title="停用用户"
|
|
description={`确认停用 ${confirmRemoval?.user.displayName ?? ""}?该账号将无法登录,已有审计记录会保留。`}
|
|
confirmLabel="确认停用"
|
|
danger
|
|
busy={confirmBusy}
|
|
onCancel={() => {
|
|
if (confirmRemoval) {
|
|
setStatusDrafts((current) => ({ ...current, [confirmRemoval.user.id]: confirmRemoval.user.status }));
|
|
}
|
|
setConfirmRemoval(null);
|
|
}}
|
|
onConfirm={() => void deactivateUser()}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AccessFlowGuide() {
|
|
return (
|
|
<div className="workflow-hint-grid" aria-label="用户管理流程提示">
|
|
<div className="workflow-hint-card">
|
|
<strong>邀请用户</strong>
|
|
<span>默认待审核,先收集联系方式和备注。</span>
|
|
</div>
|
|
<div className="workflow-hint-card">
|
|
<strong>审核申请</strong>
|
|
<span>确认身份后启用;离职或拒绝统一走停用确认。</span>
|
|
</div>
|
|
<div className="workflow-hint-card">
|
|
<strong>绑定服务器范围</strong>
|
|
<span>服主/管理员的具体服务器范围在服务器详情中维护。</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RoleImpactGuide() {
|
|
return (
|
|
<div className="role-impact-list management-form-wide" aria-label="角色影响说明">
|
|
<strong>角色影响</strong>
|
|
<span>平台管理员:可管理用户、插件、AI 提供商和维护页。</span>
|
|
<span>服主:管理自己名下服务器,并可邀请或移除服务器管理员。</span>
|
|
<span>服务器管理员:只进入服务器工作区,适合日常值班。</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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(" · ") : "暂无联系方式";
|
|
}
|