306 lines
9.6 KiB
JavaScript
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,
|
|
};
|
|
}
|