import { useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { createPrettyDisplayIDPool, generatePrettyDisplayIDs, grantPrettyDisplayID, listPrettyDisplayIDPools, listPrettyDisplayIDs, setPrettyDisplayIDStatus, updatePrettyDisplayIDPool, } from "@/features/app-users/api"; import { useAppUserAbilities } from "@/features/app-users/permissions.js"; import { prettyDisplayIDGenerateSchema, prettyDisplayIDGrantSchema, prettyDisplayIDPoolSchema, prettyDisplayIDStatusSchema, } from "@/features/app-users/schema"; const pageSize = 20; const emptyPoolForm = () => ({ levelTrack: "wealth", maxLevel: 10, minLevel: 1, name: "", ruleConfigJson: "", ruleType: "aabbcc", sortOrder: 0, status: "active", }); const emptyGenerateForm = () => ({ count: 100, poolId: "", ruleConfigJson: "", ruleType: "aabbcc" }); const emptyGrantForm = () => ({ displayUserId: "", durationDays: "", reason: "", targetUserId: "" }); const emptyStatusForm = () => ({ prettyId: "", reason: "", status: "available" }); export function usePrettyIdsPage() { const abilities = useAppUserAbilities(); const { showToast } = useToast(); const [poolPage, setPoolPage] = useState(1); const [poolStatus, setPoolStatus] = useState(""); const [poolTrack, setPoolTrack] = useState(""); const [idPage, setIdPage] = useState(1); const [idKeyword, setIdKeyword] = useState(""); const [idStatus, setIdStatus] = useState(""); const [idSource, setIdSource] = useState(""); const [idPoolId, setIdPoolId] = useState(""); const [assignedUserId, setAssignedUserId] = useState(""); const [activeDialog, setActiveDialog] = useState(""); const [activePool, setActivePool] = useState(null); const [activePrettyID, setActivePrettyID] = useState(null); const [poolForm, setPoolForm] = useState(emptyPoolForm); const [generateForm, setGenerateForm] = useState(emptyGenerateForm); const [grantForm, setGrantForm] = useState(emptyGrantForm); const [statusForm, setStatusForm] = useState(emptyStatusForm); const [loadingAction, setLoadingAction] = useState(""); // 靓号池和靓号明细是两个独立分页接口,筛选条件保持服务端字段名,避免在 hook 内再做一次字段映射。 const poolFilters = useMemo( () => ({ level_track: poolTrack, status: poolStatus, }), [poolStatus, poolTrack], ); // 靓号明细筛选会直接透传到后台列表接口,assigned_user_id 在输入阶段已 trim,keyword 保留原值用于模糊搜索。 const idFilters = useMemo( () => ({ assigned_user_id: assignedUserId, keyword: idKeyword, pool_id: idPoolId, source: idSource, status: idStatus, }), [assignedUserId, idKeyword, idPoolId, idSource, idStatus], ); const poolQuery = usePaginatedQuery({ errorMessage: "加载靓号池失败", fetcher: listPrettyDisplayIDPools, filters: poolFilters, page: poolPage, pageSize, queryKey: ["pretty-id-pools", poolFilters, poolPage], }); const idQuery = usePaginatedQuery({ errorMessage: "加载靓号列表失败", fetcher: listPrettyDisplayIDs, filters: idFilters, page: idPage, pageSize, queryKey: ["pretty-ids", idFilters, idPage], }); // 生成弹窗和明细筛选只需要当前已加载池列表;没有额外拉全量,避免页面初始化时放大后台查询。 const poolOptions = useMemo( () => (poolQuery.data.items || []).map((pool) => ({ label: `${pool.name} · ${pool.minLevel}-${pool.maxLevel}`, value: pool.poolId, })), [poolQuery.data.items], ); const openCreatePool = () => { setActivePool(null); setPoolForm(emptyPoolForm()); setActiveDialog("pool"); }; const openEditPool = (pool) => { setActivePool(pool); // 编辑弹窗以服务端当前值为准回填,缺失字段只兜底到首版规则,避免把旧数据误清空。 setPoolForm({ levelTrack: pool.levelTrack || "wealth", maxLevel: pool.maxLevel ?? 10, minLevel: pool.minLevel ?? 1, name: pool.name || "", ruleConfigJson: pool.ruleConfigJson || "", ruleType: pool.ruleType || "aabbcc", sortOrder: pool.sortOrder || 0, status: pool.status || "active", }); setActiveDialog("pool"); }; const openGenerate = (pool) => { setActivePool(pool || null); // 从池行进入时优先使用该池,从工具栏进入时沿用列表筛选池,减少管理员重复选择。 setGenerateForm({ ...emptyGenerateForm(), poolId: pool?.poolId || idPoolId || "", ruleConfigJson: pool?.ruleConfigJson || "", ruleType: pool?.ruleType || "aabbcc", }); setActiveDialog("generate"); }; const openGrant = () => { setGrantForm(emptyGrantForm()); setActiveDialog("grant"); }; const openStatus = (item) => { setActivePrettyID(item); // 状态弹窗默认给出最常用的反向操作:禁用中的号码恢复可用,其余未占用号码默认禁用。 setStatusForm({ prettyId: item?.prettyId || "", reason: "", status: item?.status === "disabled" ? "available" : "disabled", }); setActiveDialog("status"); }; const closeDialog = () => { setActiveDialog(""); setActivePool(null); setActivePrettyID(null); }; const submitPool = async (event) => { event.preventDefault(); await runAction("pool", activePool ? "靓号池已更新" : "靓号池已创建", async () => { // 表单字段使用前端 camelCase,提交边界统一转换为后端 JSON 的 snake_case。 const form = parseForm(prettyDisplayIDPoolSchema, poolForm); const payload = { level_track: form.levelTrack, max_level: form.maxLevel, min_level: form.minLevel, name: form.name, rule_config_json: form.ruleConfigJson, rule_type: form.ruleType, sort_order: form.sortOrder, status: form.status, }; if (activePool?.poolId) { await updatePrettyDisplayIDPool(activePool.poolId, payload); } else { await createPrettyDisplayIDPool(payload); } closeDialog(); await reloadAll(); }); }; const submitGenerate = async (event) => { event.preventDefault(); await runAction("generate", "靓号已生成", async () => { // 批量生成只让后台接收池 ID、规则和数量,冲突过滤与批次审计由 user-service 事务完成。 const form = parseForm(prettyDisplayIDGenerateSchema, generateForm); await generatePrettyDisplayIDs(form.poolId, { count: form.count, rule_config_json: form.ruleConfigJson, rule_type: form.ruleType, }); closeDialog(); await reloadAll(); }); }; const submitGrant = async (event) => { event.preventDefault(); await runAction("grant", "靓号已发放", async () => { const form = parseForm(prettyDisplayIDGrantSchema, grantForm); // 后台发放用 0 表示长期,正数天数在前端换算成毫秒,服务端只处理 duration_ms 这个统一单位。 const days = Number(form.durationDays || 0); await grantPrettyDisplayID({ display_user_id: form.displayUserId, duration_ms: days > 0 ? days * 24 * 60 * 60 * 1000 : 0, reason: form.reason, target_user_id: form.targetUserId, }); closeDialog(); await idQuery.reload(); }); }; const submitStatus = async (event) => { event.preventDefault(); await runAction("status", "靓号状态已更新", async () => { // 状态变更只允许未分配号码,前端提交 pretty_id 和目标状态,真正可变状态由后端再次校验。 const form = parseForm(prettyDisplayIDStatusSchema, statusForm); await setPrettyDisplayIDStatus(form.prettyId, { reason: form.reason, status: form.status, }); closeDialog(); await idQuery.reload(); }); }; const changePoolStatus = (value) => { setPoolStatus(value); // 任意筛选变化都回到第一页,避免旧页码在新条件下落到空页。 setPoolPage(1); }; const changePoolTrack = (value) => { setPoolTrack(value); setPoolPage(1); }; const changeIDKeyword = (value) => { setIdKeyword(value); // 明细列表的每个筛选项都会改变总数,页码统一回到第一页。 setIdPage(1); }; const changeIDStatus = (value) => { setIdStatus(value); setIdPage(1); }; const changeIDSource = (value) => { setIdSource(value); setIdPage(1); }; const changeIDPool = (value) => { setIdPoolId(value); setIdPage(1); }; const changeAssignedUserId = (value) => { setAssignedUserId(value.trim()); setIdPage(1); }; const resetPoolFilters = () => { // 重置筛选后立即回到第一页,保持 URL 之外的页面状态和查询条件同步。 setPoolStatus(""); setPoolTrack(""); setPoolPage(1); }; const resetIDFilters = () => { // 靓号列表筛选项彼此独立,重置时一次性清空,避免残留 assigned_user_id 影响排查。 setAssignedUserId(""); setIdKeyword(""); setIdPoolId(""); setIdSource(""); setIdStatus(""); setIdPage(1); }; const reloadAll = async () => { // 池配置会影响生成入口和号码列表展示,池相关操作完成后两个分页一起刷新。 await Promise.all([poolQuery.reload(), idQuery.reload()]); }; const runAction = async (action, successMessage, submitter) => { // 所有写操作共享 loading、toast 和异常展示;弹窗只在 submitter 内确认上游成功后关闭。 setLoadingAction(action); try { await submitter(); showToast(successMessage, "success"); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; return { abilities, activeDialog, activePool, activePrettyID, assignedUserId, changeAssignedUserId, changeIDKeyword, changeIDPool, changeIDSource, changeIDStatus, changePoolStatus, changePoolTrack, closeDialog, generateForm, grantForm, idKeyword, idPage, idPoolId, idQuery, idSource, idStatus, loadingAction, openCreatePool, openEditPool, openGenerate, openGrant, openStatus, poolForm, poolOptions, poolPage, poolQuery, poolStatus, poolTrack, reloadAll, resetIDFilters, resetPoolFilters, setGenerateForm, setGrantForm, setIdPage, setPoolForm, setPoolPage, setStatusForm, statusForm, submitGenerate, submitGrant, submitPool, submitStatus, }; }