功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+75 -67
View File
@@ -1,5 +1,5 @@
import { HeartHandshake, Sparkles, UserPen, UserRoundCheck, UserRoundPlus } from "lucide-react";
import { type FormEvent, useEffect, useMemo, useState } from "react";
import { type FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
import type { UserCreateRequest, UserResponse, UserStatus } from "../api/types";
@@ -7,7 +7,6 @@ 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 { userAccess } from "../contracts/shell";
import { type UserEditFormState, type UserListSource, type UserRemovalConfirmationState, userEditFormFromResponse } from "../contracts/users";
import { isPlatformAdmin } from "../contracts/workspace";
import { userCreateRequestFromDraft, userDeactivateRequest, userUpdateRequestFromEditForm } from "../schemas/users";
@@ -25,21 +24,6 @@ const statusOptions: Array<{ value: UserStatus; label: string }> = [
{ value: "disabled", label: "停用" }
];
const fallbackUsers: UserResponse[] = userAccess.map((user, index) => ({
id: user.id,
displayName: user.displayName,
email: `${user.id}@local.example.test`,
status: user.status,
roles: user.roles,
profile: {
phone: index === 0 ? "13800000000" : "",
qq: index === 0 ? "10001" : "",
contactNote: user.review
},
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
}));
interface UsersPageInitialState {
users?: UserResponse[];
loading?: boolean;
@@ -63,6 +47,9 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
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: "",
@@ -71,43 +58,33 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
profile: { phone: "", qq: "", contactNote: "" }
});
useEffect(() => {
let cancelled = false;
const loadUsers = useCallback(async () => {
if (initialState?.users) {
setUsers(initialState.users);
setLoading(initialState.loading ?? false);
setSource(initialState.source ?? "api");
setLoadError(initialState.loadError ?? "");
return () => {
cancelled = true;
};
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);
}
void platformApiClient
.listUsers()
.then((response) => {
if (!cancelled) {
setUsers(response.items);
setSource("api");
setLoadError("");
}
})
.catch(() => {
if (!cancelled) {
setUsers(import.meta.env.DEV ? fallbackUsers : []);
setSource(import.meta.env.DEV ? "local-development" : "api");
setLoadError(import.meta.env.DEV ? "账号 API 加载失败,当前显示本地开发样例;状态变更会被平台 API 拒绝或写入。" : "账号 API 加载失败,未显示本地样例数据。");
}
})
.finally(() => {
if (!cancelled) {
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [initialState]);
useEffect(() => {
void loadUsers();
}, [loadUsers]);
useEffect(() => {
setStatusDrafts(Object.fromEntries(users.map((user) => [user.id, user.status])));
}, [users]);
@@ -121,6 +98,23 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
[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();
@@ -267,34 +261,48 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
<PageFrame
kicker="身份"
title="用户管理"
status={source === "api" ? "账号 API 已连接" : "本地开发数据"}
status={loadError ? "账号 API 不可用" : source === "api" ? "账号 API 已连接" : "本地开发数据"}
metrics={[
{ label: "用户", value: `${users.length}`, tone: "success" },
{ label: "角色", value: `${counts.roleCount}`, tone: "neutral" },
{ label: "待审核", value: `${counts.pending}`, tone: "warning" }
{ 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:fallback" compact />}
{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 === "api" ? "平台数据" : "本地开发样例 / 禁止假成功"}</span>
<button type="button" className="primary-command" disabled={persistenceDisabled} onClick={() => setCreateOpen(true)}>
<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>{persistenceDisabled ? "等待 API" : "邀请用户"}</span>
<span>{mutationDisabled ? "等待 API" : "邀请用户"}</span>
</button>
</div>
<AccessFlowGuide />
<RoleImpactGuide />
{users.length === 0 ? (
<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">
{users.map((user) => (
{filteredUsers.map((user) => (
<article key={user.id} className="resource-list-item user-management-item">
<div>
<strong>{user.displayName}</strong>
@@ -307,7 +315,7 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
<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={persistenceDisabled} aria-label={`编辑 ${user.displayName}`} onClick={() => startEdit(user)}>
<button type="button" className="theme-upload" disabled={mutationDisabled || operations.isPending(user.id, "编辑用户")} aria-label={`编辑 ${user.displayName}`} onClick={() => startEdit(user)}>
<UserPen size={13} />
</button>
@@ -315,7 +323,7 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
<span></span>
<select
value={statusDrafts[user.id] ?? user.status}
disabled={persistenceDisabled}
disabled={mutationDisabled || operations.isPending(user.id, "更新用户状态")}
aria-label={`选择 ${user.displayName} 状态`}
onChange={(event) => setStatusDrafts((current) => ({ ...current, [user.id]: event.target.value as UserStatus }))}
>
@@ -329,7 +337,7 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
<button
type="button"
className="theme-upload"
disabled={persistenceDisabled || (statusDrafts[user.id] ?? user.status) === user.status}
disabled={mutationDisabled || operations.isPending(user.id, "更新用户状态") || (statusDrafts[user.id] ?? user.status) === user.status}
aria-label={`应用 ${user.displayName} 状态变更`}
onClick={() => applyUserStatus(user)}
>
@@ -343,7 +351,7 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
)}
</section>
<ManagementDialog open={createOpen} title="邀请用户" wide onClose={() => setCreateOpen(false)}>
<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>
@@ -400,18 +408,18 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
</div>
<RoleImpactGuide />
<div className="confirm-actions">
<button type="button" onClick={() => setCreateOpen(false)}>
<button type="button" disabled={createPending} onClick={() => setCreateOpen(false)}>
</button>
<button type="submit" className="confirm-primary" disabled={persistenceDisabled}>
<button type="submit" className="confirm-primary" disabled={mutationDisabled || createPending}>
<UserRoundPlus size={14} />
<span>{persistenceDisabled ? "等待 API" : "发送邀请"}</span>
<span>{mutationDisabled ? "等待 API" : createPending ? "发送中…" : "发送邀请"}</span>
</button>
</div>
</form>
</ManagementDialog>
<ManagementDialog open={editDraft !== null} title="编辑用户" wide onClose={closeEdit}>
<ManagementDialog open={editDraft !== null} title="编辑用户" wide onClose={() => { if (!editPending) closeEdit(); }}>
{editDraft && (
<form className="management-form dialog-form" onSubmit={(event) => void saveUserEdit(event)}>
<label>
@@ -454,12 +462,12 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
</div>
<RoleImpactGuide />
<div className="confirm-actions">
<button type="button" onClick={closeEdit}>
<button type="button" disabled={editPending} onClick={closeEdit}>
</button>
<button type="submit" className="confirm-primary">
<button type="submit" className="confirm-primary" disabled={editPending}>
<UserPen size={14} />
<span></span>
<span>{editPending ? "保存中…" : "保存用户"}</span>
</button>
</div>
</form>