490 lines
18 KiB
JavaScript
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",
|
|
};
|
|
});
|
|
}
|