import { useCallback, useEffect, useMemo, useState } from "react"; import { getCPConfig, 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: "姐妹", }; const defaultExpireHours = 24; const defaultThresholds = [0, 10000, 30000, 60000, 100000]; export function useCPConfigPage() { const abilities = useCPConfigAbilities(); const { showToast } = useToast(); const [config, setConfig] = useState({ relations: defaultRelations() }); const [form, setForm] = useState({ relations: defaultRelations().map(relationToForm) }); 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 reload = 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 || []); setConfig({ ...remoteConfig, relations }); setForm({ relations: relations.map(relationToForm) }); } 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 reload(); }, [reload]); const openDrawer = () => { setForm({ relations: completeRelations(config.relations || []).map(relationToForm) }); setDrawerOpen(true); }; const closeDrawer = () => { if (saving) { return; } setForm({ relations: completeRelations(config.relations || []).map(relationToForm) }); 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 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 || []); setConfig({ ...saved, relations }); setForm({ relations: relations.map(relationToForm) }); setDrawerOpen(false); showToast("CP 配置已保存", "success"); } catch (err) { showToast(err.message || "保存 CP 配置失败", "error"); } finally { setSaving(false); } }; return { abilities, activeRelationCount, closeDrawer, config, drawerOpen, form, loading, openDrawer, reload, resourceGroups, saving, submit, updateLevel, updateRelation, }; } 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 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 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), }; }); return { relations }; } 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", }; }); }