import { useEffect, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { emptyUserIdentityFilter, userIdentityFilterParams } from "@/shared/ui/userIdentityFilter.js"; import { banAppUser, listAppUsers, setAppUserPassword, unbanAppUser, updateAppUser, } from "@/features/app-users/api"; import { useAppUserAbilities } from "@/features/app-users/permissions.js"; import { appUserCountrySchema, appUserPasswordSchema, appUserUpdateSchema } from "@/features/app-users/schema"; import { useCountryOptions } from "@/features/host-org/hooks/useCountryOptions.js"; const pageSize = 50; const emptyData = { items: [], page: 1, pageSize, total: 0 }; const emptyEditForm = () => ({ avatar: "", gender: "", username: "" }); const emptyCountryForm = () => ({ country: "" }); const emptyPasswordForm = () => ({ password: "" }); export function useAppUsersPage() { const abilities = useAppUserAbilities(); const confirm = useConfirm(); const { showToast } = useToast(); const { countryOptions, loadingCountries } = useCountryOptions(); const { loadingRegions, regionOptions } = useRegionOptions(); const [country, setCountry] = useState(""); const [userFilter, setUserFilter] = useState(emptyUserIdentityFilter); const [regionId, setRegionId] = useState(""); const [status, setStatus] = useState(""); const [timeRange, setTimeRange] = useState({ endMs: "", startMs: "" }); const [sortBy, setSortBy] = useState("created_at"); const [sortDirection, setSortDirection] = useState("desc"); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [activeUser, setActiveUser] = useState(null); const [editForm, setEditForm] = useState(emptyEditForm); const [countryForm, setCountryForm] = useState(emptyCountryForm); const [passwordForm, setPasswordForm] = useState(emptyPasswordForm); const [loadingAction, setLoadingAction] = useState(""); const [selectedUserIds, setSelectedUserIds] = useState([]); const [patchedUsers, setPatchedUsers] = useState({}); const filters = useMemo( () => ({ country, region_id: regionId, sort_by: sortBy, sort_direction: sortDirection, end_ms: timeRange.endMs, status, start_ms: timeRange.startMs, ...userIdentityFilterParams(userFilter), }), [country, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs, userFilter], ); const { data: queryData = emptyData, error, loading, reload, } = usePaginatedQuery({ errorMessage: "加载用户列表失败", fetcher: listAppUsers, filters, page, pageSize, queryKey: ["app-users", filters, page], }); const data = useMemo(() => { const items = queryData.items || []; return { ...queryData, items: items.map((user) => (patchedUsers[user.userId] ? { ...user, ...patchedUsers[user.userId] } : user)), }; }, [patchedUsers, queryData]); useEffect(() => { setSelectedUserIds([]); setPatchedUsers({}); }, [country, page, regionId, sortBy, sortDirection, status, timeRange.endMs, timeRange.startMs, userFilter]); const locationFilterOptions = useMemo(() => { const countryItems = countryOptions.map((option) => ({ label: `国家 · ${option.label}`, value: `country:${option.value}`, })); const regionItems = [ { label: "区域 · GLOBAL · 0", value: "region:0" }, ...regionOptions.map((option) => ({ label: `区域 · ${option.label}`, value: `region:${option.value}`, })), ]; return [...countryItems, ...regionItems]; }, [countryOptions, regionOptions]); const locationFilterValue = country ? `country:${country}` : regionId !== "" ? `region:${regionId}` : ""; const changeUserFilter = (value) => { setUserFilter(value); setPage(1); }; const changeStatus = (value) => { setStatus(value); setPage(1); }; const changeLocationFilter = (value) => { const [kind, rawValue = ""] = String(value || "").split(":"); if (kind === "country" && rawValue) { setCountry(rawValue); setRegionId(""); } else if (kind === "region") { setCountry(""); setRegionId(rawValue); } else { setCountry(""); setRegionId(""); } setPage(1); }; const changeTimeRange = (value) => { setTimeRange(value); setPage(1); }; const changeSort = (field) => { setSortDirection((current) => (sortBy === field ? (current === "asc" ? "desc" : "asc") : "desc")); setSortBy(field); setPage(1); }; const resetFilters = () => { setCountry(""); setUserFilter(emptyUserIdentityFilter); setRegionId(""); setStatus(""); setTimeRange({ endMs: "", startMs: "" }); setPage(1); }; const openEdit = (user) => { setActiveUser(user); setEditForm({ avatar: user.avatar || "", gender: user.gender || "", username: user.username || "", }); setActiveAction("edit"); }; const openPassword = (user) => { setActiveUser(user); setPasswordForm(emptyPasswordForm()); setActiveAction("password"); }; const openCountry = (user) => { setActiveUser(user); setCountryForm({ country: user.country || "" }); setActiveAction("country"); }; const openCoinLedger = (user) => { setActiveUser(user); setActiveAction("coin-ledger"); }; const closeAction = () => { setActiveAction(""); setActiveUser(null); }; const submitEdit = async (event) => { event.preventDefault(); if (!activeUser) { return; } await runAction("edit", "用户已更新", async () => { const payload = parseForm(appUserUpdateSchema, editForm); await updateAppUser(activeUser.userId, payload); closeAction(); await reload(); }); }; const submitCountry = async (event) => { event.preventDefault(); if (!activeUser) { return; } await runAction("country", "国家已更新", async () => { const payload = parseForm(appUserCountrySchema, countryForm); await updateAppUser(activeUser.userId, payload); closeAction(); await reload(); }); }; const submitPassword = async (event) => { event.preventDefault(); if (!activeUser) { return; } await runAction("password", "密码已设置", async () => { const payload = parseForm(appUserPasswordSchema, passwordForm); await setAppUserPassword(activeUser.userId, payload); closeAction(); }); }; const toggleBan = async (user) => { const banned = isBanned(user); await runAction(`status-${user.userId}`, banned ? "用户已解封" : "用户已封禁", async () => { const result = banned ? await unbanAppUser(user.userId) : await banAppUser(user.userId); patchUsers([mergeUserStatus(user, result, banned ? "active" : "banned")]); setSelectedUserIds((current) => current.filter((userId) => userId !== user.userId)); }); }; const batchBan = async () => { const users = (data.items || []).filter((user) => selectedUserIds.includes(user.userId) && !isBanned(user)); if (!users.length) { showToast("请先选择未封禁用户", "warning"); return; } const ok = await confirm({ confirmText: "封禁", message: `将封禁 ${users.length} 个用户。`, title: "批量封禁用户", tone: "danger", }); if (!ok) { return; } setLoadingAction("batch-ban"); try { const results = await Promise.allSettled(users.map((user) => banAppUser(user.userId))); const updatedUsers = results .map((result, index) => result.status === "fulfilled" ? mergeUserStatus(users[index], result.value, "banned") : null, ) .filter(Boolean); const failedCount = results.length - updatedUsers.length; if (updatedUsers.length) { patchUsers(updatedUsers); setSelectedUserIds((current) => current.filter((userId) => !updatedUsers.some((updatedUser) => updatedUser.userId === userId)), ); } if (failedCount > 0) { showToast( updatedUsers.length ? `已封禁 ${updatedUsers.length} 个,${failedCount} 个失败` : "批量封禁失败", updatedUsers.length ? "warning" : "error", ); return; } showToast(`已封禁 ${updatedUsers.length} 个用户`, "success"); } finally { setLoadingAction(""); } }; const runAction = async (action, successMessage, submitter) => { setLoadingAction(action); try { await submitter(); showToast(successMessage, "success"); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const patchUsers = (users) => { setPatchedUsers((current) => { const next = { ...current }; users.forEach((user) => { if (!user?.userId) { return; } next[user.userId] = { ...(next[user.userId] || {}), ...user }; }); return next; }); }; return { abilities, activeAction, activeUser, batchBan, changeUserFilter, changeStatus, closeAction, changeLocationFilter, countryForm, countryOptions, data, editForm, error, loading, loadingAction, loadingCountries, loadingRegions, locationFilterOptions, locationFilterValue, openCountry, openCoinLedger, openEdit, openPassword, page, passwordForm, regionId, regionOptions, reload, resetFilters, selectedUserIds, setCountryForm, setEditForm, setPage, setPasswordForm, setSelectedUserIds, sortBy, sortDirection, status, timeRange, userFilter, changeSort, changeTimeRange, submitCountry, submitEdit, submitPassword, toggleBan, }; } function isBanned(user) { return user.status === "banned" || user.status === "disabled"; } function mergeUserStatus(currentUser, result, fallbackStatus) { return { ...currentUser, ...(result || {}), status: result?.status || fallbackStatus, }; }