import { useEffect, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js"; import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { createGift, createResource, createResourceGroup, disableGift, disableResource, disableResourceGroup, enableGift, enableResource, enableResourceGroup, grantResource, grantResourceGroup, listGifts, listResourceGrants, listResourceGroups, listResources, updateGift, updateResource, updateResourceGroup, } from "@/features/resources/api"; import { useResourceAbilities } from "@/features/resources/permissions.js"; import { giftFormSchema, resourceFormSchema, resourceGrantFormSchema, resourceGroupCreateFormSchema, } from "@/features/resources/schema.js"; const pageSize = 10; const dayMillis = 24 * 60 * 60 * 1000; const emptyData = { items: [], page: 1, pageSize, total: 0 }; const emptyResourceForm = (resource = {}) => ({ assetUrl: resource.assetUrl || "", enabled: resource.status ? resource.status === "active" : true, name: resource.name || "", previewUrl: resource.previewUrl || "", resourceCode: resource.resourceCode || "", resourceType: resource.resourceType || "avatar_frame", walletAssetAmount: resource.walletAssetAmount ? String(resource.walletAssetAmount) : "", }); const emptyResourceGroupForm = (group = {}) => ({ description: group.description || "", enabled: group.status ? group.status === "active" : true, groupCode: group.groupCode || "", items: group.items?.length ? group.items.map(groupItemToForm) : [emptyGroupResourceItem()], name: group.name || "", sortOrder: group.sortOrder === 0 || group.sortOrder ? String(group.sortOrder) : "0", }); const emptyGroupResourceItem = () => ({ durationDays: "1", itemType: "resource", resourceId: "", sortOrder: "0", walletAssetAmount: "", walletAssetType: "", }); const emptyGroupWalletAssetItem = (walletAssetType) => ({ durationDays: "", itemType: "wallet_asset", resourceId: "", sortOrder: "0", walletAssetAmount: "", walletAssetType, }); const emptyGiftForm = (gift = {}) => ({ coinPrice: gift.coinPrice === 0 || gift.coinPrice ? String(gift.coinPrice) : "", enabled: gift.status ? gift.status === "active" : true, giftId: gift.giftId || "", giftPointAmount: gift.giftPointAmount === 0 || gift.giftPointAmount ? String(gift.giftPointAmount) : "0", heatValue: gift.heatValue === 0 || gift.heatValue ? String(gift.heatValue) : "0", name: gift.name || "", presentationJson: gift.presentationJson || "{}", priceVersion: gift.priceVersion || "default", regionIds: (gift.regionIds?.length ? gift.regionIds : [0]).map(String), resourceId: gift.resourceId ? String(gift.resourceId) : "", sortOrder: gift.sortOrder === 0 || gift.sortOrder ? String(gift.sortOrder) : "0", }); const emptyGrantForm = () => ({ durationDays: "0", groupId: "", quantity: "1", reason: "", resourceId: "", subjectType: "resource", targetUserId: "", }); function groupItemToForm(item) { const sortOrder = item.sortOrder === 0 || item.sortOrder ? String(item.sortOrder) : "0"; if (item.itemType === "wallet_asset") { return { durationDays: "", itemType: "wallet_asset", resourceId: "", sortOrder, walletAssetAmount: item.walletAssetAmount === 0 || item.walletAssetAmount ? String(item.walletAssetAmount) : "", walletAssetType: item.walletAssetType || "", }; } return { durationDays: item.durationMs ? formatDurationDays(item.durationMs) : "1", itemType: "resource", resourceId: item.resourceId ? String(item.resourceId) : "", sortOrder, walletAssetAmount: "", walletAssetType: "", }; } export function useResourceListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [resourceType, setResourceType] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyResourceForm); const [selectedResource, setSelectedResource] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const filters = useMemo( () => ({ keyword: query, resource_type: resourceType, status, }), [query, resourceType, status], ); const result = usePaginatedQuery({ errorMessage: "加载资源列表失败", fetcher: listResources, filters, page, pageSize, queryKey: ["resources", filters, page], }); const openCreateResource = () => { setForm(emptyResourceForm()); setSelectedResource(null); setActiveAction("create"); }; const openEditResource = (resource) => { if (!resource?.resourceId) { return; } setForm(emptyResourceForm(resource)); setSelectedResource(resource); setActiveAction("edit"); }; const closeAction = () => { setActiveAction(""); setSelectedResource(null); }; const submitResource = async (event) => { event.preventDefault(); const editing = activeAction === "edit"; await runAction( editing ? "resource-edit" : "resource-create", editing ? "资源已更新" : "资源已创建", async () => { const payload = buildResourcePayload(parseForm(resourceFormSchema, form)); if (editing) { await updateResource(selectedResource.resourceId, payload); } else { await createResource(payload); } setForm(emptyResourceForm()); closeAction(); await result.reload(); }, ); }; const toggleResource = async (resource, nextEnabled = resource.status !== "active") => { if (!abilities.canUpdate || !resource?.resourceId) { return; } const nextStatus = nextEnabled ? "active" : "disabled"; if ((resource.status === "active") === nextEnabled) { return; } await runAction( `resource-status-${resource.resourceId}`, nextEnabled ? "资源已启用" : "资源已禁用", async () => { if (nextStatus === "active") { await enableResource(resource.resourceId); } else { await disableResource(resource.resourceId); } await result.reload(); }, ); }; const runAction = async (action, successMessage, submitter) => { setLoadingAction(action); try { await submitter(); showToast(successMessage, "success"); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeResourceType: resetSetter(setResourceType, setPage), changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, openCreateResource, openEditResource, resourceType, selectedResource, setForm, status, submitResource, toggleResource, }; } export function useResourceGroupListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyResourceGroupForm); const [selectedGroup, setSelectedGroup] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const [resourceOptions, setResourceOptions] = useState([]); const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false); const filters = useMemo( () => ({ keyword: query, status, }), [query, status], ); const result = usePaginatedQuery({ errorMessage: "加载资源组列表失败", fetcher: listResourceGroups, filters, page, pageSize, queryKey: ["resource-groups", filters, page], }); useEffect(() => { if (activeAction !== "create" && activeAction !== "edit") { return undefined; } let ignore = false; setResourceOptionsLoading(true); listResources({ page: 1, page_size: 100, status: "active" }) .then((data) => { if (!ignore) { setResourceOptions(toGroupResourceOptions(data.items || [], selectedGroup)); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载资源选项失败", "error"); } }) .finally(() => { if (!ignore) { setResourceOptionsLoading(false); } }); return () => { ignore = true; }; }, [activeAction, selectedGroup, showToast]); const openCreateGroup = () => { setForm(emptyResourceGroupForm()); setSelectedGroup(null); setActiveAction("create"); }; const openEditGroup = (group) => { if (!group?.groupId) { return; } setForm(emptyResourceGroupForm(group)); setSelectedGroup(group); setActiveAction("edit"); }; const closeAction = () => { setActiveAction(""); setSelectedGroup(null); }; const addResourceItem = () => { setForm((current) => ({ ...current, items: [...current.items, emptyGroupResourceItem()] })); }; const addWalletAssetItem = (walletAssetType) => { setForm((current) => ({ ...current, items: [...current.items, emptyGroupWalletAssetItem(walletAssetType)] })); }; const updateGroupItem = (index, patch) => { setForm((current) => ({ ...current, items: current.items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)), })); }; const removeGroupItem = (index) => { setForm((current) => ({ ...current, items: current.items.filter((_, itemIndex) => itemIndex !== index), })); }; const submitGroup = async (event) => { event.preventDefault(); const editing = activeAction === "edit"; const action = editing ? "group-edit" : "group-create"; const successMessage = editing ? "资源组已更新" : "资源组已创建"; setLoadingAction(action); try { const payload = buildResourceGroupPayload(parseForm(resourceGroupCreateFormSchema, form)); if (editing) { await updateResourceGroup(selectedGroup.groupId, payload); } else { await createResourceGroup(payload); } showToast(successMessage, "success"); setForm(emptyResourceGroupForm()); closeAction(); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const toggleGroup = async (group, nextEnabled = group.status !== "active") => { if (!abilities.canUpdateGroup || !group?.groupId) { return; } if ((group.status === "active") === nextEnabled) { return; } setLoadingAction(`group-status-${group.groupId}`); try { if (nextEnabled) { await enableResourceGroup(group.groupId); } else { await disableResourceGroup(group.groupId); } showToast(nextEnabled ? "资源组已启用" : "资源组已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, addResourceItem, addWalletAssetItem, changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, openCreateGroup, openEditGroup, removeGroupItem, resourceOptions, resourceOptionsLoading, selectedGroup, setForm, status, submitGroup, toggleGroup, updateGroupItem, }; } export function useGiftListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [regionId, setRegionId] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyGiftForm); const [selectedGift, setSelectedGift] = useState(null); const [loadingAction, setLoadingAction] = useState(""); const [resourceOptions, setResourceOptions] = useState([]); const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false); const { loadingRegions, regionOptions } = useRegionOptions(); const filters = useMemo( () => ({ keyword: query, region_id: regionId, status, }), [query, regionId, status], ); const result = usePaginatedQuery({ errorMessage: "加载礼物列表失败", fetcher: listGifts, filters, page, pageSize, queryKey: ["gifts", filters, page], }); useEffect(() => { if (activeAction !== "create" && activeAction !== "edit") { return undefined; } let ignore = false; setResourceOptionsLoading(true); listResources({ page: 1, page_size: 100, resource_type: "gift" }) .then((data) => { if (!ignore) { setResourceOptions(toGiftResourceOptions(data.items || [], selectedGift)); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载礼物资源失败", "error"); } }) .finally(() => { if (!ignore) { setResourceOptionsLoading(false); } }); return () => { ignore = true; }; }, [activeAction, selectedGift, showToast]); const openCreateGift = () => { setForm(emptyGiftForm()); setSelectedGift(null); setActiveAction("create"); }; const openEditGift = (gift) => { if (!gift?.giftId) { return; } setForm(emptyGiftForm(gift)); setSelectedGift(gift); setActiveAction("edit"); }; const closeAction = () => { setActiveAction(""); setSelectedGift(null); }; const submitGift = async (event) => { event.preventDefault(); const editing = activeAction === "edit"; if (editing && !selectedGift?.giftId) { return; } setLoadingAction(editing ? "gift-edit" : "gift-create"); try { const payload = buildGiftPayload(parseForm(giftFormSchema, form)); if (editing) { await updateGift(selectedGift.giftId, payload); } else { await createGift(payload); } showToast(editing ? "礼物已更新" : "礼物已创建", "success"); closeAction(); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const toggleGift = async (gift, nextEnabled = gift.status !== "active") => { if (!abilities.canStatusGift || !gift?.giftId) { return; } if ((gift.status === "active") === nextEnabled) { return; } setLoadingAction(`gift-status-${gift.giftId}`); try { if (nextEnabled) { await enableGift(gift.giftId); } else { await disableGift(gift.giftId); } showToast(nextEnabled ? "礼物已启用" : "礼物已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeRegionId: resetSetter(setRegionId, setPage), changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, loadingRegions, openCreateGift, openEditGift, regionId, regionOptions, resourceOptions, resourceOptionsLoading, selectedGift, setForm, status, submitGift, toggleGift, }; } export function useResourceGrantListPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [page, setPage] = useState(1); const [activeAction, setActiveAction] = useState(""); const [form, setForm] = useState(emptyGrantForm); const [resourceOptions, setResourceOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]); const [optionsLoading, setOptionsLoading] = useState(false); const [loadingAction, setLoadingAction] = useState(""); const filters = useMemo( () => ({ status, target_user_id: query.trim(), }), [query, status], ); const result = usePaginatedQuery({ errorMessage: "加载资源赠送记录失败", fetcher: listResourceGrants, filters, page, pageSize, queryKey: ["resource-grants", filters, page], }); useEffect(() => { if (activeAction !== "create") { return undefined; } let ignore = false; setOptionsLoading(true); Promise.all([ listResources({ page: 1, page_size: 100, status: "active" }), listResourceGroups({ page: 1, page_size: 100, status: "active" }), ]) .then(([resources, groups]) => { if (!ignore) { setResourceOptions((resources.items || []).filter((resource) => resource.grantable !== false)); setGroupOptions(groups.items || []); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载资源选项失败", "error"); } }) .finally(() => { if (!ignore) { setOptionsLoading(false); } }); return () => { ignore = true; }; }, [activeAction, showToast]); const openCreateGrant = () => { setForm(emptyGrantForm()); setActiveAction("create"); }; const closeAction = () => { setActiveAction(""); }; const submitGrant = async (event) => { event.preventDefault(); if (!abilities.canCreateGrant) { return; } setLoadingAction("grant-create"); try { const payload = buildGrantPayload(parseForm(resourceGrantFormSchema, form)); if (payload.subjectType === "resource") { await grantResource(payload.resource); } else { await grantResourceGroup(payload.group); } showToast("资源已赠送", "success"); closeAction(); setForm(emptyGrantForm()); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeStatus: resetSetter(setStatus, setPage), closeAction, form, groupOptions, loadingAction, openCreateGrant, optionsLoading, resourceOptions, setForm, status, submitGrant, }; } function sharedPageState({ page, query, result, setPage, setQuery }) { return { data: result.data || emptyData, error: result.error, loading: result.loading, page, query, reload: result.reload, setPage, changeQuery: resetSetter(setQuery, setPage), }; } function resetSetter(setValue, setPage) { return (value) => { setValue(value); setPage(1); }; } function buildResourcePayload(form) { const resourceType = form.resourceType; const isCoin = resourceType === "coin"; return { amount: isCoin ? Number(form.walletAssetAmount) : 0, assetUrl: form.assetUrl.trim(), name: form.name.trim(), previewUrl: form.previewUrl.trim(), resourceCode: form.resourceCode.trim(), resourceType, sortOrder: 0, status: form.enabled ? "active" : "disabled", }; } function buildResourceGroupPayload(form) { return { description: (form.description || "").trim(), groupCode: form.groupCode.trim(), items: form.items.map((item, index) => { const sortOrder = Number(item.sortOrder || index); if (item.itemType === "wallet_asset") { return { amount: Number(item.walletAssetAmount), assetType: item.walletAssetType, itemType: "wallet_asset", sortOrder: Number.isInteger(sortOrder) ? sortOrder : index, }; } return { durationDays: Number(item.durationDays), itemType: "resource", resourceId: Number(item.resourceId), sortOrder: Number.isInteger(sortOrder) ? sortOrder : index, }; }), name: form.name.trim(), sortOrder: Number(form.sortOrder || 0), status: form.enabled ? "active" : "disabled", }; } function buildGiftPayload(form) { return { coinPrice: Number(form.coinPrice), effectiveAtMs: Date.now(), giftId: form.giftId.trim(), giftPointAmount: Number(form.giftPointAmount || 0), heatValue: Number(form.heatValue || 0), name: form.name.trim(), presentationJson: form.presentationJson?.trim() || "{}", priceVersion: form.priceVersion.trim(), regionIds: form.regionIds.map(Number), resourceId: Number(form.resourceId), sortOrder: Number(form.sortOrder || 0), status: form.enabled ? "active" : "disabled", }; } function buildGrantPayload(form) { const targetUserId = Number(form.targetUserId); const reason = form.reason.trim(); const commandId = makeCommandId(form.subjectType === "resource" ? "resource-grant" : "resource-group-grant"); if (form.subjectType === "resource") { return { resource: { commandId, durationMs: Math.round(Number(form.durationDays || 0) * dayMillis), quantity: Number(form.quantity || 1), reason, resourceId: Number(form.resourceId), targetUserId, }, subjectType: "resource", }; } return { group: { commandId, groupId: Number(form.groupId), reason, targetUserId, }, subjectType: "resource_group", }; } function makeCommandId(prefix) { return `${prefix}-${Date.now()}`; } function toGroupResourceOptions(items, selectedGroup) { const selectedResources = (selectedGroup?.items || []) .map((item) => item.resource) .filter((resource) => resource?.resourceId); return uniqueResources([...items, ...selectedResources]).filter( (resource) => resource.resourceType !== "coin" && resource.status === "active" && resource.grantable !== false, ); } function toGiftResourceOptions(items, selectedGift) { const selectedResource = selectedGift?.resource?.resourceId ? [selectedGift.resource] : selectedGift?.resourceId ? [{ name: String(selectedGift.resourceId), resourceId: selectedGift.resourceId, resourceType: "gift" }] : []; return uniqueResources([...items, ...selectedResource]).filter( (resource) => resource.resourceType === "gift" || resource.resourceId === selectedGift?.resourceId, ); } function uniqueResources(resources) { const seen = new Set(); return resources.filter((resource) => { if (!resource?.resourceId || seen.has(resource.resourceId)) { return false; } seen.add(resource.resourceId); return true; }); } function formatDurationDays(durationMs) { const days = Number(durationMs || 0) / dayMillis; return Number.isInteger(days) ? String(days) : String(days.toFixed(1)); }