2026-05-26 02:39:46 +08:00

306 lines
9.6 KiB
JavaScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { downloadCsv } from "@/shared/api/download";
import { toPageQuery } from "@/shared/api/query";
import { parseForm } from "@/shared/forms/validation";
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
import { mergePaginatedItems } from "@/shared/hooks/usePaginatedQuery.js";
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { listRoles } from "@/features/roles/api";
import {
batchUpdateUserStatus,
createUser,
exportUsers,
listUsers,
resetUserPassword,
updateUser,
updateUserStatus,
} from "@/features/users/api";
import { useUserAbilities } from "@/features/users/permissions.js";
import { userCreateFormSchema, userUpdateFormSchema } from "@/features/users/schema";
const pageSize = 50;
const emptyUsers = { items: [], total: 0, page: 1, pageSize };
const emptyData = { roles: [], users: emptyUsers };
const emptyForm = {
mfaEnabled: false,
name: "",
password: "",
roleIds: [],
status: "active",
team: "",
username: "",
};
export function useUsersPage() {
const [query, setQuery] = useState("");
const [roleFilter, setRoleFilter] = useState("");
const [statusFilter, setStatusFilter] = useState("");
const [page, setPage] = useState(1);
const [selectedIds, setSelectedIds] = useState([]);
const [formOpen, setFormOpen] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [profileUser, setProfileUser] = useState(null);
const [form, setForm] = useState(emptyForm);
const [mergedUsers, setMergedUsers] = useState(emptyUsers);
const abilities = useUserAbilities();
const confirm = useConfirm();
const { showToast } = useToast();
const navigate = useNavigate();
const requestQuery = useMemo(
() => toPageQuery({ keyword: query, page, pageSize, role: roleFilter, status: statusFilter }),
[page, query, roleFilter, statusFilter],
);
const queryFn = useCallback(async () => {
const [userData, roleData] = await Promise.all([
listUsers(requestQuery),
abilities.canViewRoles ? listRoles() : [],
]);
setSelectedIds([]);
return { roles: roleData || [], users: userData || emptyUsers };
}, [abilities.canViewRoles, requestQuery]);
const { data, error, loading: queryLoading, reload } = useAdminQuery(queryFn, {
errorMessage: "加载用户失败",
initialData: emptyData,
keepPreviousData: true,
queryKey: ["users", requestQuery, abilities.canViewRoles],
});
useEffect(() => {
setMergedUsers(emptyUsers);
}, [query, roleFilter, statusFilter]);
useEffect(() => {
const nextUsers = data?.users || emptyUsers;
const nextItems = Array.isArray(nextUsers.items) ? nextUsers.items : [];
const nextPage = Number(nextUsers.page || page || 1);
const nextPageSize = Number(nextUsers.pageSize || pageSize);
const nextTotal = Number(nextUsers.total || 0);
setMergedUsers((current) => {
if (nextPage <= 1) {
return { ...nextUsers, items: nextItems, page: nextPage, pageSize: nextPageSize, total: nextTotal };
}
return {
...nextUsers,
items: mergePaginatedItems(current.items || [], nextItems),
page: nextPage,
pageSize: nextPageSize,
total: nextTotal,
};
});
}, [data?.users, page]);
const users = mergedUsers;
const loading = queryLoading && (users.items || []).length === 0;
const roles = useMemo(() => data?.roles || [], [data?.roles]);
const roleFilters = useMemo(() => [["", "全部角色"], ...roles.map((role) => [role.name, role.name])], [roles]);
const stats = useMemo(() => {
const items = users.items || [];
const active = items.filter((user) => user.status === "active").length;
const locked = items.filter((user) => user.status === "locked").length;
const admin = items.filter((user) => String(user.role).includes("管理员")).length;
return { active, admin, locked };
}, [users.items]);
const changeQuery = (value) => {
setQuery(value);
setPage(1);
};
const changeRoleFilter = (value) => {
setRoleFilter(value);
setPage(1);
};
const changeStatusFilter = (value) => {
setStatusFilter(value);
setPage(1);
};
const resetFilters = () => {
setQuery("");
setRoleFilter("");
setStatusFilter("");
setPage(1);
};
const openCreate = () => {
setEditingUser(null);
setForm(emptyForm);
setFormOpen(true);
};
const openEdit = (user) => {
setEditingUser(user);
setForm({
mfaEnabled: user.mfaEnabled,
name: user.name,
password: "",
roleIds: user.roleIds || [],
status: user.status,
team: user.team || "",
username: user.account,
});
setFormOpen(true);
};
const closeForm = () => {
setFormOpen(false);
};
const openProfile = (user) => {
setProfileUser(user);
};
const closeProfile = () => {
setProfileUser(null);
};
const submitUser = async (event) => {
event.preventDefault();
try {
if (editingUser) {
const payload = parseForm(userUpdateFormSchema, form);
await updateUser(editingUser.id, payload);
showToast("用户已更新", "success");
} else {
const payload = parseForm(userCreateFormSchema, form);
const result = await createUser(payload);
showToast(`用户已创建,初始密码:${result.initialPassword}`, "success");
}
setFormOpen(false);
reload();
} catch (err) {
showToast(err.message || "保存用户失败", "error");
}
};
const toggleLocked = async (user) => {
const nextStatus = user.status === "locked" ? "active" : "locked";
const ok = await confirm({
confirmText: user.status === "locked" ? "解锁" : "锁定",
message: `${user.name} 的登录状态会立即变更。`,
title: user.status === "locked" ? "解锁用户" : "锁定用户",
tone: user.status === "locked" ? "primary" : "danger",
});
if (!ok) {
return;
}
await updateUserStatus(user.id, nextStatus);
showToast("用户状态已更新", "success");
reload();
};
const toggleDisabled = async (user) => {
const nextStatus = user.status === "disabled" ? "active" : "disabled";
const ok = await confirm({
confirmText: user.status === "disabled" ? "启用" : "停用",
message: `${user.name} 的账号状态会立即变更。`,
title: user.status === "disabled" ? "启用用户" : "停用用户",
tone: user.status === "disabled" ? "primary" : "danger",
});
if (!ok) {
return;
}
await updateUserStatus(user.id, nextStatus);
showToast("用户状态已更新", "success");
reload();
};
const resetPassword = async (user) => {
const ok = await confirm({
confirmText: "重置",
message: `${user.name} 的密码会重置为系统生成的初始密码。`,
title: "重置密码",
tone: "danger",
});
if (!ok) {
return;
}
const result = await resetUserPassword(user.id);
showToast(`已重置,初始密码:${result.initialPassword}`, "success");
};
const batchStatus = async (status) => {
if (!selectedIds.length) {
showToast("请先选择用户", "warning");
return;
}
const ok = await confirm({
confirmText: "确认",
message: `将更新 ${selectedIds.length} 个用户状态。`,
title: "批量更新状态",
tone: status === "active" ? "primary" : "danger",
});
if (!ok) {
return;
}
await batchUpdateUserStatus(selectedIds, status);
showToast("批量状态已更新", "success");
reload();
};
const downloadUsers = () => {
return downloadCsv(
() => exportUsers({ keyword: query, role: roleFilter, status: statusFilter }),
"hyapp-users.csv",
);
};
const setRoleChecked = (roleId, checked) => {
setForm((current) => ({
...current,
roleIds: checked ? [...current.roleIds, roleId] : current.roleIds.filter((id) => id !== roleId),
}));
};
return {
abilities,
batchStatus,
changeQuery,
changeRoleFilter,
changeStatusFilter,
closeForm,
closeProfile,
downloadUsers,
editingUser,
error,
form,
formOpen,
loading,
navigate,
openCreate,
openEdit,
openProfile,
page,
profileUser,
query,
reload,
resetPassword,
resetFilters,
roleFilter,
roleFilters,
roles,
selectedIds,
setForm,
setPage,
setRoleChecked,
setSelectedIds,
stats,
statusFilter,
submitUser,
toggleDisabled,
toggleLocked,
users,
};
}