2026-06-18 19:38:25 +08:00

490 lines
18 KiB
JavaScript

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