update:添加分页、成员添加优化

This commit is contained in:
xiaozhu
2026-08-31 15:41:06 +08:00
parent cc38bb1575
commit a4dd317828
20 changed files with 1729 additions and 920 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Plus, Search } from "lucide-react";
import { ApiRequestError, type Employee, type Role } from "../../services/api";
@@ -20,6 +20,8 @@ import {
TableRow,
} from "~/components/ui/table";
import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
import { AdminPagination } from "./AdminPagination";
import { useCursorPage } from "./useCursorPage";
import {
formFieldClass,
formInputClass,
@@ -42,6 +44,7 @@ import {
} from "./adminUi";
import { primaryGradientButtonClass } from "~/components/common/buttonClasses";
import { useDebouncedValue } from "./useDebouncedValue";
const EMPTY_FORM = {
username: "",
@@ -54,6 +57,7 @@ const EMPTY_FORM = {
const FORM_INPUT_CLASS =
"h-[39px] w-full rounded-md border border-[#d6e0e9] bg-white px-[11px] text-xs text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:ring-3 focus:ring-brand/10 disabled:cursor-not-allowed disabled:opacity-50";
export function UserManagementPage({
onNotify,
onConnectionChange,
@@ -63,42 +67,55 @@ export function UserManagementPage({
}) {
const api = useApi();
const { user } = useAuth();
const [employees, setEmployees] = useState<Employee[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState<Employee | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [form, setForm] = useState(EMPTY_FORM);
const [userSearchTerm, setUserSearchTerm] = useState("");
const [deleteTarget, setDeleteTarget] = useState<Employee | null>(null);
const debouncedSearch = useDebouncedValue(userSearchTerm, 300);
const canManage = user?.role_code === "admin";
const load = async (): Promise<void> => {
setLoading(true);
try {
const [employeeList, roleList] = await Promise.all([
api.listPlatformEmployees(),
api.listPlatformRoles(),
]);
setEmployees(employeeList);
setRoles(roleList);
onConnectionChange(true);
} catch (error) {
onConnectionChange(false);
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "用户列表加载失败",
});
} finally {
setLoading(false);
}
};
const fetchEmployees = useCallback(
async (input: { limit: number; cursor: string | null; q: string }) => {
try {
const page = await api.listPlatformEmployees(input);
onConnectionChange(true);
return page;
} catch (error) {
onConnectionChange(false);
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "用户列表加载失败",
});
throw error;
}
},
[api, onConnectionChange, onNotify],
);
const {
items: employees,
setItems: setEmployees,
page,
loading,
meta,
totalPages,
goNext,
goPrev,
goToPage,
canGoToPage,
reload,
refreshFromStart,
} = useCursorPage(fetchEmployees, debouncedSearch);
useEffect(() => {
void load();
}, []);
void api.listPlatformRoles().then(setRoles).catch(() => {
/* roles only needed for edit dialog */
});
}, [api]);
const openCreate = (): void => {
setEditing(null);
@@ -122,12 +139,10 @@ export function UserManagementPage({
const submit = async (event: React.FormEvent): Promise<void> => {
event.preventDefault();
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
// 新建用户时密码必填
if (!editing && !form.password.trim()) {
onNotify({ tone: "error", message: "请输入密码" });
return;
}
// 编辑自身时,前端二次拦截禁用 status / role_code 的修改;管理员账号禁止停用/锁定
if (editing) {
const editingSelf = editing.user_id === user?.user_id;
const targetIsAdmin = editing.role_code === "admin";
@@ -144,7 +159,6 @@ export function UserManagementPage({
return;
}
}
// 密码长度校验 8~72 字符
if (form.password && (form.password.length < 8 || form.password.length > 72)) {
onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" });
return;
@@ -158,20 +172,20 @@ export function UserManagementPage({
role_code: form.role_code,
status: form.status,
});
setEmployees((current) => current.map(
(item) => item.user_id === updated.user_id ? updated : item,
));
setEmployees((current) =>
current.map((item) => (item.user_id === updated.user_id ? updated : item)),
);
onNotify({ tone: "success", message: "用户信息已更新" });
} else {
const created = await api.createPlatformEmployee({
await api.createPlatformEmployee({
username: form.username.trim(),
display_name: form.display_name.trim(),
email: form.email.trim() || undefined,
password: form.password,
role_code: "developer",
});
setEmployees((current) => [...current, created]);
onNotify({ tone: "success", message: "用户已添加" });
await refreshFromStart();
}
setDialogOpen(false);
} catch (error) {
@@ -187,8 +201,12 @@ export function UserManagementPage({
const executeRemove = async (employee: Employee): Promise<void> => {
try {
await api.deletePlatformEmployee(employee.user_id);
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
onNotify({ tone: "success", message: "用户已删除" });
if (employees.length <= 1 && page > 1) {
goPrev();
} else {
reload();
}
} catch (error) {
onNotify({
tone: "error",
@@ -197,6 +215,12 @@ export function UserManagementPage({
}
};
const emptyMessage = useMemo(() => {
if (loading) return "正在加载用户…";
if (debouncedSearch.trim()) return "未找到匹配的用户";
return "暂无用户";
}, [loading, debouncedSearch]);
return (
<section className={adminPageClass}>
<div className={adminToolbarClass}>
@@ -236,61 +260,63 @@ export function UserManagementPage({
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
{loading || employees.length === 0 ? (
<TableRow className="min-h-[64px] border-t border-[#edf1f5] hover:bg-transparent data-[state=selected]:bg-transparent">
<TableCell colSpan={5} className="p-0">
<p className={adminEmptyClass}></p>
<p className={adminEmptyClass}>{emptyMessage}</p>
</TableCell>
</TableRow>
) : (
employees
.filter((employee) => {
const term = userSearchTerm.toLowerCase().trim();
if (!term) return true;
return (
employee.display_name.toLowerCase().includes(term) ||
employee.username.toLowerCase().includes(term) ||
(employee.email && employee.email.toLowerCase().includes(term))
);
})
.map((employee) => {
const isProtectedAdmin = employee.role_code === "admin";
return (
<TableRow key={employee.user_id} className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
<TableCell className="p-3 px-4 align-middle"><span className={employeeNameClass}><b className={employeeAvatarClass}>{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span></TableCell>
<TableCell className="p-3 px-4 align-middle"><code>{employee.username}</code></TableCell>
<TableCell className="p-3 px-4 align-middle">{employee.role_code ? (<span className={rolePillClass(employee.role_code)}>{employee.role_name}</span>) : (<span>-</span>)}</TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={statusPillClass(employee.status)}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span></TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={employeeActionsClass}>
<Button
variant="outline"
size="xs"
type="button"
disabled={!canManage}
onClick={() => openEdit(employee)}
className={rowButtonClass}
>
</Button>
<Button
variant="destructive"
size="xs"
type="button"
disabled={!canManage || isProtectedAdmin}
title={isProtectedAdmin ? "管理员账号不能删除" : "删除"}
onClick={() => setDeleteTarget(employee)}
className={rowDangerButtonClass}
>
</Button>
</span></TableCell>
</TableRow>
);
})
employees.map((employee) => {
const isProtectedAdmin = employee.role_code === "admin";
return (
<TableRow key={employee.user_id} className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
<TableCell className="p-3 px-4 align-middle"><span className={employeeNameClass}><b className={employeeAvatarClass}>{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span></TableCell>
<TableCell className="p-3 px-4 align-middle"><code>{employee.username}</code></TableCell>
<TableCell className="p-3 px-4 align-middle">{employee.role_code ? (<span className={rolePillClass(employee.role_code)}>{employee.role_name}</span>) : (<span>-</span>)}</TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={statusPillClass(employee.status)}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span></TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={employeeActionsClass}>
<Button
variant="outline"
size="xs"
type="button"
disabled={!canManage}
onClick={() => openEdit(employee)}
className={rowButtonClass}
>
</Button>
<Button
variant="destructive"
size="xs"
type="button"
disabled={!canManage || isProtectedAdmin}
title={isProtectedAdmin ? "管理员账号不能删除" : "删除"}
onClick={() => setDeleteTarget(employee)}
className={rowDangerButtonClass}
>
</Button>
</span></TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
<AdminPagination
page={page}
totalPages={totalPages}
totalCount={meta.total_count}
loading={loading}
hasMore={meta.has_more}
canGoToPage={canGoToPage}
onPrev={goPrev}
onNext={goNext}
onGoToPage={goToPage}
/>
<AppFormDialog
open={dialogOpen}
onOpenChange={(nextOpen) => {