功能修改
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user