2026-06-08 19:40:13 +08:00

276 lines
10 KiB
JavaScript

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",
};
});
}