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 { createEmojiPack, createGift, createResource, createResourceGroup, disableResourceShopItem, disableGift, disableResource, disableResourceGroup, enableResourceShopItem, enableGift, enableResource, enableResourceGroup, grantResource, grantResourceGroup, listEmojiPackCategories, listEmojiPacks, listGiftTypes, listGifts, listResourceGrants, listResourceGroups, listResourceShopItems, listResources, lookupResourceGrantTarget, updateGift, updateGiftTypes, updateResource, updateResourceGroup, upsertResourceShopItems, } from "@/features/resources/api"; import { defaultGiftTypeOptions, resourceShopSellableTypes } from "@/features/resources/constants.js"; import { useResourceAbilities } from "@/features/resources/permissions.js"; import { profileCardMetadataJSON } from "@/features/resources/profileCardLayout.js"; import { emojiPackFormSchema, giftFormSchema, giftTypeConfigFormSchema, resourceFormSchema, resourceGrantFormSchema, resourceGroupCreateFormSchema, resourceShopItemsFormSchema, } from "@/features/resources/schema.js"; const pageSize = 50; const dayMillis = 24 * 60 * 60 * 1000; const emptyData = { items: [], page: 1, pageSize, total: 0 }; const emptyResourceForm = (resource = {}) => ({ animationUrl: resource.animationUrl || resource.assetUrl || "", badgeForm: resource.resourceType === "badge" ? resource.badgeForm || badgeFormFromMetadata(resource.metadataJson) || "tile" : "tile", badgeKind: resource.resourceType === "badge" ? resource.badgeKind || badgeKindFromMetadata(resource.metadataJson) || "normal" : "normal", coinPrice: resource.coinPrice === 0 || resource.coinPrice ? String(resource.coinPrice) : "", enabled: resource.status ? resource.status === "active" : true, levelTrack: resource.resourceType === "badge" ? resource.levelTrack || levelTrackFromMetadata(resource.metadataJson) || "" : "", metadataJson: resource.metadataJson || "", name: resource.name || "", previewUrl: resource.previewUrl || "", priceType: resource.resourceId ? resource.priceType || (resource.coinPrice > 0 ? "coin" : "free") : "coin", resourceCode: resource.resourceCode || "", resourceType: resource.resourceType || "avatar_frame", walletAssetAmount: resource.walletAssetAmount ? String(resource.walletAssetAmount) : "", }); const emptyEmojiPackForm = () => ({ animationUrl: "", category: "默认", coverUrl: "", name: "", pricingType: "free", regionIds: ["0"], sortOrder: "0", }); 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 = {}) => ({ chargeAssetType: gift.chargeAssetType || "COIN", coinPrice: gift.coinPrice === 0 || gift.coinPrice ? String(gift.coinPrice) : "", effectTypes: gift.effectTypes || [], effectiveFrom: msToDatetimeLocal(gift.effectiveFromMs), effectiveTo: msToDatetimeLocal(gift.effectiveToMs), enabled: gift.status ? gift.status === "active" : true, giftId: gift.giftId || "", giftPointAmount: gift.giftPointAmount === 0 || gift.giftPointAmount ? String(gift.giftPointAmount) : "0", giftTypeCode: gift.giftTypeCode || "normal", 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", }); export function applyGiftResourceSelection(form, resource, fillIdentity = true) { const resourceId = resource?.resourceId ? String(resource.resourceId) : ""; const nextForm = { ...form, resourceId, }; if (fillIdentity) { nextForm.giftId = resourceId; nextForm.name = resource?.name || resource?.resourceCode || resourceId; } return applyGiftResourcePrice(nextForm, resource); } export function applyGiftPriceDefaults(form, coinPrice) { const priceValue = String(coinPrice ?? ""); const priceNumber = Number(priceValue); return { ...form, coinPrice: priceValue, giftPointAmount: priceValue, heatValue: priceValue.trim() && Number.isFinite(priceNumber) ? String(Math.ceil(priceNumber / 100)) : "", }; } function applyGiftResourcePrice(form, resource) { const priceType = resource?.priceType || (resource?.coinPrice > 0 ? "coin" : ""); if (priceType === "free") { return applyGiftPriceDefaults({ ...form, chargeAssetType: "COIN" }, 0); } if (priceType === "coin") { return applyGiftPriceDefaults({ ...form, chargeAssetType: "COIN" }, resource?.coinPrice || 0); } return form; } const emptyGrantForm = () => ({ durationDays: "7", groupId: "", quantity: "1", reason: "", resourceId: "", resourceIds: [], 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(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setResourceType(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeResourceType: resetSetter(setResourceType, setPage), changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, openCreateResource, openEditResource, resourceType, selectedResource, resetFilters, setForm, status, submitResource, toggleResource, }; } export function useResourceShopPage() { const abilities = useResourceAbilities(); const { showToast } = useToast(); const [query, setQuery] = useState(""); const [status, setStatus] = useState(""); const [resourceType, setResourceType] = useState(""); const [page, setPage] = useState(1); const [drawerOpen, setDrawerOpen] = useState(false); const [drawerQuery, setDrawerQuery] = useState(""); const [drawerResourceType, setDrawerResourceType] = useState(""); const [batchDurationDays, setBatchDurationDays] = useState("1"); const [resourceOptions, setResourceOptions] = useState([]); const [resourceOptionsLoading, setResourceOptionsLoading] = useState(false); const [draftByResourceId, setDraftByResourceId] = useState({}); const [loadingAction, setLoadingAction] = useState(""); const filters = useMemo( () => ({ keyword: query, resource_type: resourceType, status, }), [query, resourceType, status], ); const result = usePaginatedQuery({ errorMessage: "加载道具商店失败", fetcher: listResourceShopItems, filters, page, pageSize, queryKey: ["resource-shop", filters, page], }); const shopItemsByResourceId = useMemo(() => { const items = new Map(); (result.data?.items || []).forEach((item) => { if (item.resourceId) { items.set(Number(item.resourceId), item); } }); return items; }, [result.data?.items]); const selectedDrafts = useMemo( () => Object.values(draftByResourceId) .map((draft) => ({ ...draft, resource: resourceOptions.find( (resource) => String(resource.resourceId) === String(draft.resourceId), ), })) .sort((left, right) => Number(left.sortOrder || 0) - Number(right.sortOrder || 0)), [draftByResourceId, resourceOptions], ); const filteredResourceOptions = useMemo(() => { const keyword = drawerQuery.trim().toLowerCase(); return resourceOptions.filter((resource) => { if (drawerResourceType && resource.resourceType !== drawerResourceType) { return false; } if (!keyword) { return true; } return `${resource.name || ""} ${resource.resourceCode || ""}`.toLowerCase().includes(keyword); }); }, [drawerQuery, drawerResourceType, resourceOptions]); useEffect(() => { if (!drawerOpen) { return undefined; } let ignore = false; setResourceOptionsLoading(true); listResources({ page: 1, page_size: 100, status: "active" }) .then((data) => { if (!ignore) { setResourceOptions(toShopResourceOptions(data.items || [])); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载售卖资源失败", "error"); } }) .finally(() => { if (!ignore) { setResourceOptionsLoading(false); } }); return () => { ignore = true; }; }, [drawerOpen, showToast]); const openShopDrawer = () => { setDraftByResourceId({}); setDrawerQuery(""); setDrawerResourceType(""); setBatchDurationDays("1"); setDrawerOpen(true); }; const closeShopDrawer = () => { setDrawerOpen(false); setDraftByResourceId({}); }; const toggleDraftResource = (resource) => { if (!resource?.resourceId) { return; } const resourceId = String(resource.resourceId); setDraftByResourceId((current) => { if (current[resourceId]) { const next = { ...current }; delete next[resourceId]; return next; } const existing = shopItemsByResourceId.get(Number(resource.resourceId)); return { ...current, [resourceId]: { durationDays: String(existing?.durationDays || batchDurationDays), effectiveFrom: msToDatetimeLocal(existing?.effectiveFromMs), effectiveTo: msToDatetimeLocal(existing?.effectiveToMs), resourceId, sortOrder: String(existing?.sortOrder ?? Object.keys(current).length), }, }; }); }; const updateDraftItem = (resourceId, patch) => { setDraftByResourceId((current) => ({ ...current, [resourceId]: { ...current[resourceId], ...patch, }, })); }; const applyBatchDuration = (durationDays) => { setBatchDurationDays(durationDays); setDraftByResourceId((current) => Object.fromEntries( Object.entries(current).map(([resourceId, draft]) => [resourceId, { ...draft, durationDays }]), ), ); }; const submitShopItems = async (event) => { event.preventDefault(); if (!abilities.canUpdateShop) { return; } setLoadingAction("resource-shop-upsert"); try { const parsed = parseForm(resourceShopItemsFormSchema, { items: selectedDrafts }); await upsertResourceShopItems({ items: parsed.items.map((item) => ({ durationDays: Number(item.durationDays), effectiveFromMs: datetimeLocalToMs(item.effectiveFrom), effectiveToMs: datetimeLocalToMs(item.effectiveTo), resourceId: Number(item.resourceId), shopItemId: shopItemsByResourceId.get(Number(item.resourceId))?.shopItemId, sortOrder: Number(item.sortOrder || 0), status: "active", })), }); showToast("售卖资源已保存", "success"); closeShopDrawer(); await result.reload(); } catch (err) { showToast(err.message || "保存售卖资源失败", "error"); } finally { setLoadingAction(""); } }; const toggleShopItem = async (item, nextEnabled = item.status !== "active") => { if (!abilities.canUpdateShop || !item?.shopItemId) { return; } if ((item.status === "active") === nextEnabled) { return; } setLoadingAction(`resource-shop-status-${item.shopItemId}`); try { if (nextEnabled) { await enableResourceShopItem(item.shopItemId); } else { await disableResourceShopItem(item.shopItemId); } showToast(nextEnabled ? "售卖资源已启用" : "售卖资源已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setResourceType(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, applyBatchDuration, batchDurationDays, changeDrawerQuery: setDrawerQuery, changeDrawerResourceType: setDrawerResourceType, changeResourceType: resetSetter(setResourceType, setPage), changeStatus: resetSetter(setStatus, setPage), closeShopDrawer, drawerOpen, drawerQuery, drawerResourceType, filteredResourceOptions, loadingAction, openShopDrawer, resourceOptionsLoading, resourceType, resetFilters, selectedDrafts, selectedResourceIds: new Set(Object.keys(draftByResourceId)), status, submitShopItems, toggleDraftResource, toggleShopItem, updateDraftItem, }; } 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(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, addResourceItem, addWalletAssetItem, changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, openCreateGroup, openEditGroup, removeGroupItem, resourceOptions, resourceOptionsLoading, selectedGroup, resetFilters, 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 [giftTypes, setGiftTypes] = useState(defaultGiftTypeOptions); const [giftTypeForm, setGiftTypeForm] = useState({ items: defaultGiftTypeOptions.map(giftTypeToForm) }); const [giftTypeDialogOpen, setGiftTypeDialogOpen] = useState(false); const [giftTypesLoading, setGiftTypesLoading] = useState(false); 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(() => { let ignore = false; setGiftTypesLoading(true); listGiftTypes() .then((items = []) => { if (ignore) { return; } const nextItems = mergeGiftTypeOptions(items); setGiftTypes(nextItems); setGiftTypeForm({ items: nextItems.map(giftTypeToForm) }); }) .catch((err) => { if (!ignore) { showToast(err.message || "加载礼物类型失败", "error"); } }) .finally(() => { if (!ignore) { setGiftTypesLoading(false); } }); return () => { ignore = true; }; }, [showToast]); 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 selectGiftResource = (resourceId) => { const selectedResource = resourceOptions.find((resource) => String(resource.resourceId) === String(resourceId)); setForm((currentForm) => { return applyGiftResourceSelection( currentForm, selectedResource || { resourceId }, activeAction === "create", ); }); }; const changeGiftPrice = (coinPrice) => { setForm((currentForm) => applyGiftPriceDefaults(currentForm, coinPrice)); }; const openGiftTypeDialog = () => { setGiftTypeForm({ items: giftTypes.map(giftTypeToForm) }); setGiftTypeDialogOpen(true); }; const closeGiftTypeDialog = () => { setGiftTypeDialogOpen(false); }; const updateGiftTypeFormItem = (index, patch) => { setGiftTypeForm((current) => ({ ...current, items: current.items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)), })); }; const submitGiftTypes = async (event) => { event.preventDefault(); if (!abilities.canUpdateGift) { return; } setLoadingAction("gift-types"); try { const parsed = parseForm(giftTypeConfigFormSchema, giftTypeForm); const savedItems = await updateGiftTypes({ items: parsed.items.map((item) => ({ tabKey: item.tabKey, ...buildGiftTypePayload(item), })), }); const nextItems = mergeGiftTypeOptions(savedItems); setGiftTypes(nextItems); setGiftTypeForm({ items: nextItems.map(giftTypeToForm) }); showToast("礼物类型已保存", "success"); setGiftTypeDialogOpen(false); } catch (err) { showToast(err.message || "保存礼物类型失败", "error"); } finally { setLoadingAction(""); } }; 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(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setRegionId(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeGiftPrice, changeRegionId: resetSetter(setRegionId, setPage), changeStatus: resetSetter(setStatus, setPage), closeAction, closeGiftTypeDialog, form, giftTypeDialogOpen, giftTypeForm, giftTypeOptions: giftTypes, giftTypesLoading, loadingAction, loadingRegions, openCreateGift, openEditGift, openGiftTypeDialog, regionId, regionOptions, resourceOptions, resourceOptionsLoading, selectedGift, resetFilters, selectGiftResource, setForm, submitGiftTypes, status, submitGift, toggleGift, updateGiftTypeFormItem, }; } export function useEmojiPackListPage() { 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(emptyEmojiPackForm); const [loadingAction, setLoadingAction] = useState(""); const [categoryOptions, setCategoryOptions] = useState([]); const [categoryOptionsLoading, setCategoryOptionsLoading] = useState(false); const { loadingRegions, regionOptions } = useRegionOptions(); const filters = useMemo( () => ({ keyword: query, status, }), [query, status], ); const result = usePaginatedQuery({ errorMessage: "加载表情包列表失败", fetcher: listEmojiPacks, filters, page, pageSize, queryKey: ["emoji-packs", filters, page], }); useEffect(() => { let ignore = false; setCategoryOptionsLoading(true); listEmojiPackCategories() .then((options) => { if (!ignore) { setCategoryOptions(mergeEmojiPackCategoryOptions(options)); } }) .catch((err) => { if (!ignore) { showToast(err.message || "加载表情包分类失败", "error"); } }) .finally(() => { if (!ignore) { setCategoryOptionsLoading(false); } }); return () => { ignore = true; }; }, [showToast]); const openCreateEmojiPack = () => { setForm(emptyEmojiPackForm()); setActiveAction("create"); }; const closeAction = () => { setActiveAction(""); }; const submitEmojiPack = async (event) => { event.preventDefault(); if (!abilities.canCreateEmojiPack) { return; } setLoadingAction("emoji-pack-create"); try { const payload = buildEmojiPackPayload(parseForm(emojiPackFormSchema, form)); await createEmojiPack(payload); setCategoryOptions((options) => mergeEmojiPackCategoryOptions(options, payload.category)); showToast("表情包已创建", "success"); closeAction(); setForm(emptyEmojiPackForm()); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const toggleEmojiPack = async (emojiPack, nextEnabled = emojiPack.status !== "active") => { if (!abilities.canUpdate || !emojiPack?.resourceId) { return; } if ((emojiPack.status === "active") === nextEnabled) { return; } setLoadingAction(`emoji-pack-status-${emojiPack.resourceId}`); try { if (nextEnabled) { await enableResource(emojiPack.resourceId); } else { await disableResource(emojiPack.resourceId); } showToast(nextEnabled ? "表情包已启用" : "表情包已禁用", "success"); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, categoryOptions, categoryOptionsLoading, changeStatus: resetSetter(setStatus, setPage), closeAction, form, loadingAction, loadingRegions, openCreateEmojiPack, regionOptions, resetFilters, setForm, status, submitEmojiPack, toggleEmojiPack, }; } 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 [giftOptions, setGiftOptions] = useState([]); const [giftTypeOptions, setGiftTypeOptions] = useState(defaultGiftTypeOptions); const [groupOptions, setGroupOptions] = useState([]); const [resourceOptions, setResourceOptions] = 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: 500, status: "active" }), listGifts({ page: 1, page_size: 500, status: "active" }), listGiftTypes(), listResourceGroups({ page: 1, page_size: 100, status: "active" }), ]) .then(([resources, gifts, giftTypes, groups]) => { if (!ignore) { setResourceOptions( (resources.items || []).filter( (resource) => resource.status !== "disabled" && resource.grantable !== false, ), ); setGiftOptions((gifts.items || []).filter((gift) => gift.resourceId && gift.status !== "disabled")); setGiftTypeOptions(mergeGiftTypeOptions(giftTypes)); 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 parsedForm = parseForm(resourceGrantFormSchema, form); const targetUser = await lookupResourceGrantTarget(parsedForm.targetUserId); const targetUserId = normalizedResolvedUserId(targetUser); const payload = buildGrantPayload(parsedForm, targetUserId); if (payload.subjectType === "resource") { for (const resourcePayload of payload.resources) { await grantResource(resourcePayload); } } else { await grantResourceGroup(payload.group); } showToast("资源已赠送", "success"); closeAction(); setForm(emptyGrantForm()); await result.reload(); } catch (err) { showToast(err.message || "操作失败", "error"); } finally { setLoadingAction(""); } }; const resetFilters = () => { setQuery(""); setStatus(""); setPage(1); }; return { ...sharedPageState({ page, query, result, setPage, setQuery }), abilities, activeAction, changeStatus: resetSetter(setStatus, setPage), closeAction, form, giftOptions, giftTypeOptions, groupOptions, loadingAction, openCreateGrant, optionsLoading, resourceOptions, resetFilters, 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"; const animationUrl = form.animationUrl.trim(); return { amount: isCoin ? Number(form.walletAssetAmount) : 0, animationUrl, assetUrl: animationUrl, badgeForm: resourceType === "badge" ? form.badgeForm : undefined, badgeKind: resourceType === "badge" ? form.badgeKind || "normal" : undefined, coinPrice: form.priceType === "coin" ? Number(form.coinPrice) : 0, levelTrack: resourceType === "badge" && form.badgeKind === "level" ? form.levelTrack : undefined, metadataJson: resourceType === "profile_card" ? profileCardMetadataJSON(form.metadataJson) : undefined, name: form.name.trim(), previewUrl: form.previewUrl.trim(), priceType: form.priceType, resourceCode: form.resourceCode.trim(), resourceType, sortOrder: 0, status: form.enabled ? "active" : "disabled", }; } function badgeFormFromMetadata(metadataJson) { if (!metadataJson) { return ""; } try { const metadata = JSON.parse(metadataJson); const badgeForm = String(metadata?.badge_form || "") .trim() .toLowerCase(); if (badgeForm === "strip" || badgeForm === "tile") { return badgeForm; } } catch { return ""; } return ""; } function badgeKindFromMetadata(metadataJson) { if (!metadataJson) { return ""; } try { const metadata = JSON.parse(metadataJson); const badgeKind = String(metadata?.badge_kind || "") .trim() .toLowerCase(); if (badgeKind === "level") { return "level"; } } catch { return ""; } return ""; } function levelTrackFromMetadata(metadataJson) { if (!metadataJson) { return ""; } try { const metadata = JSON.parse(metadataJson); const levelTrack = String(metadata?.level_track || "") .trim() .toLowerCase(); if (levelTrack === "wealth" || levelTrack === "game" || levelTrack === "charm") { return levelTrack; } } catch { return ""; } return ""; } function buildEmojiPackPayload(form) { const regionIds = (form.regionIds || []) .map(Number) .filter((regionId) => Number.isInteger(regionId) && regionId >= 0); return { animationUrl: form.animationUrl.trim(), category: form.category.trim(), coverUrl: form.coverUrl.trim(), name: form.name.trim(), pricingType: form.pricingType, regionIds: regionIds.length ? regionIds : [0], sortOrder: Number(form.sortOrder || 0), }; } function mergeEmojiPackCategoryOptions(options = [], value = "默认") { const merged = new Set(["默认"]); options.forEach((option) => { const category = String(option || "").trim(); if (category) { merged.add(category); } }); const category = String(value || "").trim(); if (category) { merged.add(category); } return Array.from(merged).sort((left, right) => left.localeCompare(right, "zh-Hans-CN")); } 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 { chargeAssetType: form.chargeAssetType, coinPrice: Number(form.coinPrice), effectiveAtMs: Date.now(), effectiveFromMs: datetimeLocalToMs(form.effectiveFrom), effectiveToMs: datetimeLocalToMs(form.effectiveTo), effectTypes: form.effectTypes || [], giftId: form.giftId.trim(), giftPointAmount: Number(form.coinPrice || 0), giftTypeCode: form.giftTypeCode, 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 buildGiftTypePayload(form) { return { displayName: form.displayName.trim(), sortOrder: Number(form.sortOrder || 0), status: form.enabled ? "active" : "disabled", tabName: form.tabName.trim(), }; } function giftTypeToForm(item) { const normalized = normalizeGiftTypeOption(item); return { enabled: normalized.status ? normalized.status === "active" : true, displayName: normalized.displayName || "", sortOrder: normalized.sortOrder === 0 || normalized.sortOrder ? String(normalized.sortOrder) : "0", tabKey: normalized.tabKey || "", tabName: normalized.tabName || "", }; } function mergeGiftTypeOptions(items = []) { const byCode = new Map(defaultGiftTypeOptions.map((item) => [item.tabKey, normalizeGiftTypeOption(item)])); items.forEach((item) => { const normalized = normalizeGiftTypeOption(item); if (normalized.tabKey) { byCode.set(normalized.tabKey, { ...byCode.get(normalized.tabKey), ...normalized, }); } }); return Array.from(byCode.values()).sort( (left, right) => Number(left.sortOrder || 0) - Number(right.sortOrder || 0), ); } function normalizeGiftTypeOption(item = {}) { const tabKey = String(item.typeCode || item.tabKey || "").trim(); const tabName = String(item.tabName || (item.typeCode ? item.tabKey : "") || "").trim(); const displayName = String(item.displayName || item.name || "").trim(); return { ...item, displayName, tabKey, tabName, }; } function msToDatetimeLocal(value) { if (!value) { return ""; } const date = new Date(Number(value)); if (Number.isNaN(date.getTime())) { return ""; } const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000); return local.toISOString().slice(0, 16); } function datetimeLocalToMs(value) { const trimmed = String(value || "").trim(); if (!trimmed) { return 0; } const parsed = new Date(trimmed).getTime(); return Number.isFinite(parsed) ? parsed : 0; } function normalizedResolvedUserId(user) { const userId = String(user?.userId || user?.user_id || "").trim(); if (!/^[1-9]\d*$/.test(userId)) { throw new Error("用户不存在或用户 ID 无效"); } return userId; } function buildGrantPayload(form, resolvedTargetUserId) { const targetUserId = String(resolvedTargetUserId || "").trim(); const reason = form.reason.trim(); if (form.subjectType === "resource") { const resourceIds = grantResourceIds(form); return { resources: resourceIds.map((resourceId, index) => ({ commandId: makeCommandId(`resource-grant-${resourceId}-${index}`), durationMs: Math.round(Number(form.durationDays || 0) * dayMillis), quantity: Number(form.quantity || 1), reason, resourceId: Number(resourceId), targetUserId, })), subjectType: "resource", }; } return { group: { commandId: makeCommandId("resource-group-grant"), groupId: Number(form.groupId), reason, targetUserId, }, subjectType: "resource_group", }; } function grantResourceIds(form) { if (Array.isArray(form.resourceIds) && form.resourceIds.length) { return form.resourceIds; } return form.resourceId ? [form.resourceId] : []; } 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 toShopResourceOptions(items) { return uniqueResources(items).filter( (resource) => resourceShopSellableTypes.includes(resource.resourceType) && resource.status === "active" && resource.priceType === "coin" && Number(resource.coinPrice || 0) > 0, ); } 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)); }