import { useCallback, useEffect, useMemo, useState } from "react"; import { getRedPacket, getRedPacketConfig, listRedPackets, retryRedPacketRefund, updateRedPacketConfig, } from "@/features/red-packets/api"; import { useRedPacketAbilities } from "@/features/red-packets/permissions.js"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; const pageSize = 50; const emptyPackets = { items: [], page: 1, pageSize, total: 0 }; const emptyForm = { enabled: false, countTiers: [""], amountTiers: [""], delayedOpenSeconds: "0", expireSeconds: "86400", dailySendLimit: "5", ruleUrl: "", }; export function useRedPacketPage() { const abilities = useRedPacketAbilities(); const { showToast } = useToast(); const [config, setConfig] = useState(null); const [configDrawerOpen, setConfigDrawerOpen] = useState(false); const [configLoading, setConfigLoading] = useState(false); const [configSaving, setConfigSaving] = useState(false); const [form, setForm] = useState(emptyForm); const [roomId, setRoomId] = useState(""); const [senderUserId, setSenderUserId] = useState(""); const [packetType, setPacketType] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [detailOpen, setDetailOpen] = useState(false); const [detailLoading, setDetailLoading] = useState(false); const [selectedPacket, setSelectedPacket] = useState(null); const filters = useMemo( () => ({ roomId: roomId.trim() || undefined, senderUserId: senderUserId.trim() || undefined, packetMode: packetType || undefined, status: status || undefined, }), [packetType, roomId, senderUserId, status], ); const { data: packets = emptyPackets, error: packetsError, loading: packetsLoading, reload: reloadPackets, } = usePaginatedQuery({ errorMessage: "加载红包列表失败", fetcher: listRedPackets, filters, page, pageSize, queryKey: ["red-packets", filters, page], }); const reloadConfig = useCallback(async () => { setConfigLoading(true); try { const config = await getRedPacketConfig(); setConfig(config); setForm(formFromConfig(config)); } catch (err) { showToast(err.message || "加载红包配置失败", "error"); } finally { setConfigLoading(false); } }, [showToast]); useEffect(() => { void reloadConfig(); }, [reloadConfig]); const openConfigDrawer = useCallback(() => { setForm(config ? formFromConfig(config) : emptyForm); setConfigDrawerOpen(true); }, [config]); const closeConfigDrawer = useCallback(() => { setConfigDrawerOpen(false); setForm(config ? formFromConfig(config) : emptyForm); }, [config]); const submitConfig = async (event) => { event.preventDefault(); if (!abilities.canUpdate) { return; } setConfigSaving(true); try { const saved = await updateRedPacketConfig(payloadFromForm(form)); setConfig(saved); setForm(formFromConfig(saved)); setConfigDrawerOpen(false); showToast("红包配置已保存", "success"); } catch (err) { showToast(err.message || "保存红包配置失败", "error"); } finally { setConfigSaving(false); } }; const openPacketDetail = async (packetId) => { if (!packetId) { return; } setDetailOpen(true); setDetailLoading(true); try { const packet = await getRedPacket(packetId); setSelectedPacket(packet); } catch (err) { showToast(err.message || "加载红包详情失败", "error"); } finally { setDetailLoading(false); } }; const retryRefund = async (packetId) => { if (!packetId || !abilities.canUpdate) { return; } try { await retryRedPacketRefund(packetId); showToast("退款重试已提交", "success"); await reloadPackets(); if (selectedPacket?.packetId === packetId) { await openPacketDetail(packetId); } } catch (err) { showToast(err.message || "退款重试失败", "error"); } }; const closePacketDetail = () => { setDetailOpen(false); setSelectedPacket(null); }; const changeFilter = (setter) => (value) => { setter(value); setPage(1); }; const resetFilters = () => { setRoomId(""); setSenderUserId(""); setPacketType(""); setStatus(""); setPage(1); }; return { abilities, closeConfigDrawer, closePacketDetail, config, configDrawerOpen, configLoading, configSaving, detailLoading, detailOpen, form, openConfigDrawer, openPacketDetail, packetType, packets, packetsError, packetsLoading, page, reloadConfig, reloadPackets, resetFilters, retryRefund, roomId, selectedPacket, senderUserId, setForm, setPacketType: changeFilter(setPacketType), setPage, setRoomId: changeFilter(setRoomId), setSenderUserId: changeFilter(setSenderUserId), setStatus: changeFilter(setStatus), status, submitConfig, }; } function formFromConfig(config) { return { enabled: Boolean(config.enabled), countTiers: valuesToForm(config.countTiers), amountTiers: valuesToForm(config.amountTiers), delayedOpenSeconds: String(config.delayedOpenSeconds || 0), expireSeconds: "86400", dailySendLimit: String(config.dailySendLimit || 0), ruleUrl: config.ruleUrl || "", }; } function valuesToForm(values) { return values?.length ? values.map((value) => String(value)) : [""]; } function payloadFromForm(form) { const countTiers = normalizePositiveIntegers(form.countTiers); const amountTiers = normalizePositiveIntegers(form.amountTiers); const delayedOpenSeconds = Number(form.delayedOpenSeconds || 0); const expireSeconds = 86400; const dailySendLimit = Number(form.dailySendLimit || 0); if (!countTiers.length) { throw new Error("红包数量档位不能为空"); } if (!amountTiers.length) { throw new Error("红包金额档位不能为空"); } if (!Number.isFinite(delayedOpenSeconds) || delayedOpenSeconds < 0) { throw new Error("延迟秒数不能小于 0"); } if (!Number.isFinite(expireSeconds) || expireSeconds <= delayedOpenSeconds) { throw new Error("过期秒数必须大于延迟秒数"); } if (!Number.isFinite(dailySendLimit) || dailySendLimit <= 0) { throw new Error("每日发送次数必须大于 0"); } return { enabled: Boolean(form.enabled), count_tiers: countTiers, amount_tiers: amountTiers, delayed_open_seconds: delayedOpenSeconds, expire_seconds: expireSeconds, daily_send_limit: dailySendLimit, rule_url: String(form.ruleUrl || "").trim(), }; } function normalizePositiveIntegers(values) { const seen = new Set(); return (values || []) .map((value) => Number(value || 0)) .filter((value) => Number.isInteger(value) && value > 0) .filter((value) => { if (seen.has(value)) { return false; } seen.add(value); return true; }) .sort((a, b) => a - b); }