261 lines
7.8 KiB
JavaScript

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);
}