2026-06-11 01:01:53 +08:00

347 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 在输入阶段已 trimkeyword 保留原值用于模糊搜索。
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,
};
}