261 lines
7.8 KiB
JavaScript
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);
|
|
}
|