import { useCallback, useEffect, useMemo, useState } from "react"; import { getCPConfig, listCPApplications, listCPRelationships, updateCPConfig } from "@/features/cp-config/api"; import { useCPConfigAbilities } from "@/features/cp-config/permissions.js"; import { listResourceGroups } from "@/features/resources/api"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; export const cpRelationTypes = ["cp", "brother", "sister"]; export const cpRelationLabels = { cp: "CP", brother: "兄弟", sister: "姐妹", }; export const cpWeeklyRankRanks = [1, 2, 3]; const defaultExpireHours = 24; const defaultThresholds = [0, 10000, 30000, 60000, 100000]; const listPageSize = 50; const emptyListPage = () => ({ items: [], page: 1, pageSize: listPageSize, serverTimeMs: 0, total: 0 }); export function useCPConfigPage() { const abilities = useCPConfigAbilities(); const { showToast } = useToast(); const [activeTab, setActiveTabValue] = useState("relationships"); const [relationships, setRelationships] = useState(emptyListPage); const [applications, setApplications] = useState(emptyListPage); const [relationshipsLoading, setRelationshipsLoading] = useState(false); const [applicationsLoading, setApplicationsLoading] = useState(false); const [relationshipsError, setRelationshipsError] = useState(""); const [applicationsError, setApplicationsError] = useState(""); const [applicationsLoaded, setApplicationsLoaded] = useState(false); const [config, setConfig] = useState({ relations: defaultRelations(), weeklyRank: defaultWeeklyRank() }); const [form, setForm] = useState({ relations: defaultRelations().map(relationToForm), weeklyRank: weeklyRankToForm(defaultWeeklyRank()), }); const [drawerOpen, setDrawerOpen] = useState(false); const [loading, setLoading] = useState(false); const [resourceGroups, setResourceGroups] = useState([]); const [saving, setSaving] = useState(false); const activeRelationCount = useMemo( () => (config.relations || []).filter((relation) => relation.status === "active").length, [config.relations], ); const reloadConfig = useCallback(async () => { setLoading(true); try { const [configResult, groupResult] = await Promise.allSettled([ getCPConfig(), listResourceGroups({ page: 1, page_size: 200, status: "active" }), ]); if (configResult.status === "fulfilled") { const remoteConfig = configResult.value; const relations = completeRelations(remoteConfig.relations || []); const weeklyRank = completeWeeklyRank(remoteConfig.weeklyRank); setConfig({ ...remoteConfig, relations, weeklyRank }); setForm({ relations: relations.map(relationToForm), weeklyRank: weeklyRankToForm(weeklyRank) }); } else { showToast(configResult.reason?.message || "加载 CP 配置失败", "error"); } if (groupResult.status === "fulfilled") { setResourceGroups(groupResult.value?.items || []); } else { showToast(groupResult.reason?.message || "加载资源组失败", "error"); } } catch (err) { showToast(err.message || "加载 CP 配置失败", "error"); } finally { setLoading(false); } }, [showToast]); useEffect(() => { void reloadConfig(); }, [reloadConfig]); const loadRelationships = useCallback( async (page = 1) => { const normalizedPage = Number(page || 1); if (normalizedPage <= 1) { setRelationshipsLoading(true); setRelationshipsError(""); } try { const result = await listCPRelationships({ page: normalizedPage, page_size: listPageSize }); setRelationships((current) => normalizedPage > 1 ? { ...result, items: [...(current.items || []), ...(result.items || [])] } : result, ); } catch (err) { const message = err.message || "加载用户关系列表失败"; if (normalizedPage <= 1) { setRelationshipsError(message); } else { showToast(message, "error"); } } finally { if (normalizedPage <= 1) { setRelationshipsLoading(false); } } }, [showToast], ); const loadApplications = useCallback( async (page = 1) => { const normalizedPage = Number(page || 1); if (normalizedPage <= 1) { setApplicationsLoading(true); setApplicationsError(""); } try { const result = await listCPApplications({ page: normalizedPage, page_size: listPageSize }); setApplications((current) => normalizedPage > 1 ? { ...result, items: [...(current.items || []), ...(result.items || [])] } : result, ); setApplicationsLoaded(true); } catch (err) { const message = err.message || "加载 CP 申请列表失败"; if (normalizedPage <= 1) { setApplicationsError(message); } else { showToast(message, "error"); } } finally { if (normalizedPage <= 1) { setApplicationsLoading(false); } } }, [showToast], ); useEffect(() => { void loadRelationships(1); }, [loadRelationships]); const setActiveTab = useCallback( (tab) => { setActiveTabValue(tab); if (tab === "applications" && !applicationsLoaded) { void loadApplications(1); } }, [applicationsLoaded, loadApplications], ); const reloadActiveTab = useCallback(() => { if (activeTab === "applications") { void loadApplications(1); return; } void loadRelationships(1); }, [activeTab, loadApplications, loadRelationships]); const reload = useCallback(async () => { await Promise.allSettled([ reloadConfig(), activeTab === "applications" ? loadApplications(1) : loadRelationships(1), ]); }, [activeTab, loadApplications, loadRelationships, reloadConfig]); const openDrawer = () => { setForm({ relations: completeRelations(config.relations || []).map(relationToForm), weeklyRank: weeklyRankToForm(completeWeeklyRank(config.weeklyRank)), }); setDrawerOpen(true); }; const closeDrawer = () => { if (saving) { return; } setForm({ relations: completeRelations(config.relations || []).map(relationToForm), weeklyRank: weeklyRankToForm(completeWeeklyRank(config.weeklyRank)), }); setDrawerOpen(false); }; const updateRelation = (relationType, patch) => { setForm((current) => ({ ...current, relations: (current.relations || []).map((relation) => relation.relationType === relationType ? { ...relation, ...patch } : relation, ), })); }; const updateLevel = (relationType, levelNumber, patch) => { setForm((current) => ({ ...current, relations: (current.relations || []).map((relation) => relation.relationType === relationType ? { ...relation, levels: (relation.levels || []).map((level) => Number(level.level) === Number(levelNumber) ? { ...level, ...patch } : level, ), } : relation, ), })); }; const updateWeeklyRank = (patch) => { setForm((current) => ({ ...current, weeklyRank: { ...(current.weeklyRank || weeklyRankToForm(defaultWeeklyRank())), ...patch }, })); }; const updateWeeklyRankReward = (rankNo, patch) => { setForm((current) => ({ ...current, weeklyRank: { ...(current.weeklyRank || weeklyRankToForm(defaultWeeklyRank())), rewards: completeWeeklyRankRewardForms(current.weeklyRank?.rewards || []).map((reward) => Number(reward.rankNo) === Number(rankNo) ? { ...reward, ...patch } : reward, ), }, })); }; const submit = async (event) => { event.preventDefault(); if (!abilities.canUpdate) { return; } let payload; try { payload = payloadFromForm(form); } catch (err) { showToast(err.message || "CP 配置参数不正确", "error"); return; } setSaving(true); try { const saved = await updateCPConfig(payload); const relations = completeRelations(saved.relations || []); const weeklyRank = completeWeeklyRank(saved.weeklyRank); setConfig({ ...saved, relations, weeklyRank }); setForm({ relations: relations.map(relationToForm), weeklyRank: weeklyRankToForm(weeklyRank) }); setDrawerOpen(false); showToast("CP 配置已保存", "success"); } catch (err) { showToast(err.message || "保存 CP 配置失败", "error"); } finally { setSaving(false); } }; return { abilities, activeTab, activeRelationCount, applications, applicationsError, applicationsLoading, closeDrawer, config, drawerOpen, form, loading, loadApplications, loadRelationships, openDrawer, reload, reloadActiveTab, relationships, relationshipsError, relationshipsLoading, resourceGroups, saving, setActiveTab, submit, updateLevel, updateRelation, updateWeeklyRank, updateWeeklyRankReward, }; } export function defaultRelations() { return cpRelationTypes.map((relationType) => ({ relationType, displayName: cpRelationLabels[relationType], maxCountPerUser: relationType === "cp" ? 1 : 5, applicationExpireHours: defaultExpireHours, breakupCostCoins: 0, status: "active", levels: defaultLevels(), updatedAtMs: 0, })); } function defaultLevels() { return defaultThresholds.map((threshold, index) => ({ level: index + 1, intimacyThreshold: threshold, rewardResourceGroupId: 0, levelIconUrl: "", status: "active", updatedAtMs: 0, })); } function defaultWeeklyRank() { return { enabled: true, relationType: "cp", topCount: 3, rewards: cpWeeklyRankRanks.map((rankNo) => ({ rankNo, resourceGroupId: 0 })), updatedAtMs: 0, }; } function completeRelations(relations) { const byType = new Map((relations || []).map((relation) => [relation.relationType, relation])); return defaultRelations().map((fallback) => { const relation = { ...fallback, ...(byType.get(fallback.relationType) || {}) }; if (relation.relationType === "cp") { relation.maxCountPerUser = 1; } relation.displayName = relation.displayName || cpRelationLabels[relation.relationType]; relation.levels = completeLevels(relation.levels || []); return relation; }); } function completeLevels(levels) { const byLevel = new Map((levels || []).map((level) => [Number(level.level), level])); return defaultLevels().map((fallback) => ({ ...fallback, ...(byLevel.get(fallback.level) || {}), level: fallback.level, })); } function relationToForm(relation) { return { relationType: relation.relationType, displayName: relation.displayName || cpRelationLabels[relation.relationType], maxCountPerUser: relation.relationType === "cp" ? "1" : String(relation.maxCountPerUser || 5), applicationExpireHours: String(relation.applicationExpireHours || defaultExpireHours), breakupCostCoins: String(relation.breakupCostCoins || 0), status: relation.status || "active", levels: completeLevels(relation.levels || []).map(levelToForm), }; } function levelToForm(level) { return { level: Number(level.level || 0), intimacyThreshold: String(level.intimacyThreshold || 0), rewardResourceGroupId: level.rewardResourceGroupId ? String(level.rewardResourceGroupId) : "", levelIconUrl: level.levelIconUrl || "", status: level.status || "active", }; } function completeWeeklyRank(weeklyRank) { const fallback = defaultWeeklyRank(); const normalized = { ...fallback, ...(weeklyRank || {}) }; normalized.topCount = 3; normalized.relationType = "cp"; normalized.rewards = completeWeeklyRankRewards(normalized.rewards || []); return normalized; } function completeWeeklyRankRewards(rewards) { const byRank = new Map((rewards || []).map((reward) => [Number(reward.rankNo), reward])); return cpWeeklyRankRanks.map((rankNo) => ({ rankNo, resourceGroupId: Number(byRank.get(rankNo)?.resourceGroupId || 0), })); } function weeklyRankToForm(weeklyRank) { const completed = completeWeeklyRank(weeklyRank); return { enabled: completed.enabled !== false, relationType: "cp", topCount: "3", rewards: completeWeeklyRankRewards(completed.rewards).map((reward) => ({ rankNo: reward.rankNo, resourceGroupId: reward.resourceGroupId ? String(reward.resourceGroupId) : "", })), }; } function completeWeeklyRankRewardForms(rewards) { const byRank = new Map((rewards || []).map((reward) => [Number(reward.rankNo), reward])); return cpWeeklyRankRanks.map((rankNo) => ({ rankNo, resourceGroupId: byRank.get(rankNo)?.resourceGroupId || "", })); } function payloadFromForm(form) { const relations = completeFormRelations(form.relations || []).map((relation) => { const relationType = relation.relationType; const maxCountPerUser = relationType === "cp" ? 1 : Number(relation.maxCountPerUser || 0); const applicationExpireHours = Number(relation.applicationExpireHours || 0); const breakupCostCoins = Number(relation.breakupCostCoins || 0); const status = relation.status === "disabled" ? "disabled" : "active"; if (!Number.isInteger(maxCountPerUser) || maxCountPerUser <= 0) { throw new Error(`${cpRelationLabels[relationType]}数量必须大于 0`); } if (!Number.isInteger(applicationExpireHours) || applicationExpireHours <= 0) { throw new Error(`${cpRelationLabels[relationType]}申请过期小时必须大于 0`); } if (!Number.isInteger(breakupCostCoins) || breakupCostCoins < 0) { throw new Error(`${cpRelationLabels[relationType]}解除费用不能小于 0`); } return { relationType, maxCountPerUser, applicationExpireHours, breakupCostCoins, status, levels: completeFormLevels(relation.levels || [], relationType), }; }); const weeklyRank = weeklyRankPayloadFromForm(form.weeklyRank || weeklyRankToForm(defaultWeeklyRank())); return { relations, weeklyRank }; } function weeklyRankPayloadFromForm(weeklyRank) { const enabled = weeklyRank.enabled !== false; const rewards = completeWeeklyRankRewardForms(weeklyRank.rewards || []).map((reward) => ({ rankNo: Number(reward.rankNo), resourceGroupId: Number(reward.resourceGroupId || 0), })); if (enabled) { const missing = rewards.find( (reward) => !Number.isInteger(reward.resourceGroupId) || reward.resourceGroupId <= 0, ); if (missing) { throw new Error(`CP周榜 Top${missing.rankNo} 奖励资源组必须配置`); } } return { enabled, topCount: 3, rewards, }; } function completeFormRelations(relations) { const byType = new Map((relations || []).map((relation) => [relation.relationType, relation])); return defaultRelations().map((fallback) => ({ ...relationToForm(fallback), ...(byType.get(fallback.relationType) || {}), })); } function completeFormLevels(levels, relationType) { const byLevel = new Map((levels || []).map((level) => [Number(level.level), level])); let previous = -1; return defaultLevels().map((fallback) => { const level = { ...levelToForm(fallback), ...(byLevel.get(fallback.level) || {}) }; const threshold = Number(level.intimacyThreshold || 0); const rewardResourceGroupId = Number(level.rewardResourceGroupId || 0); if (!Number.isInteger(threshold) || threshold < 0) { throw new Error(`${cpRelationLabels[relationType]}${fallback.level}级亲密值不能小于 0`); } if (threshold <= previous) { throw new Error(`${cpRelationLabels[relationType]}等级亲密值必须递增`); } if (!Number.isInteger(rewardResourceGroupId) || rewardResourceGroupId < 0) { throw new Error(`${cpRelationLabels[relationType]}${fallback.level}级奖励资源组不正确`); } previous = threshold; return { level: fallback.level, intimacyThreshold: threshold, rewardResourceGroupId, levelIconUrl: String(level.levelIconUrl || "").trim(), status: level.status === "disabled" ? "disabled" : "active", }; }); }