347 lines
12 KiB
JavaScript
347 lines
12 KiB
JavaScript
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,
|
||
};
|
||
}
|