"use client"; import { useEffect, useMemo, useState } from "react"; import { Plus, RotateCcw, Trash2 } from "lucide-react"; import { AdminStepper, AdminSwitch } from "@/components/admin"; import ConfirmDialog from "@/components/ConfirmDialog"; import useUnsavedGuard from "@/hooks/useUnsavedGuard"; import { apiGetSettings, apiUpdateSiteSettings } from "@/lib/api"; import { DEFAULT_LEVEL_DEFS, LEVEL_BADGE_CLASS, LEVEL_COLOR_KEYS, LEVEL_COLOR_TOKENS, MAX_LEVEL_COUNT, splitBackground, type LevelDef, } from "@/lib/levels"; import { toast } from "@/lib/toast"; const cloneDefs = (defs: LevelDef[]): LevelDef[] => defs.map((d) => ({ threshold: d.threshold, color: d.color })); const sameDefs = (a: LevelDef[] | null, b: LevelDef[] | null) => { if (a === b) return true; if (!a || !b || a.length !== b.length) return false; return a.every((d, i) => d.threshold === b[i].threshold && d.color === b[i].color); }; /** 等级管理:配置等级数量(2–12)、每级所需累计积分与前景/背景配色、特效开关 */ export default function LevelsAdmin() { const [form, setForm] = useState(null); // null = 加载中 const [saved, setSaved] = useState(null); const [fx, setFx] = useState(true); const [savedFx, setSavedFx] = useState(true); const [resetOpen, setResetOpen] = useState(false); const [saving, setSaving] = useState(false); const dirty = useMemo( () => !sameDefs(form, saved) || fx !== savedFx, [form, saved, fx, savedFx], ); const leave = useUnsavedGuard(dirty); useEffect(() => { let alive = true; apiGetSettings() .then((s) => { if (!alive) return; const defs = cloneDefs(s.levels); setForm(defs); setSaved(defs); setFx(s.levels_fx); setSavedFx(s.levels_fx); }) .catch((e) => { toast(e instanceof Error ? e.message : "获取等级配置失败"); if (alive) setForm([]); }); return () => { alive = false; }; }, []); const setThreshold = (i: number, v: number) => setForm((f) => (f ? f.map((d, j) => (j === i ? { ...d, threshold: v } : d)) : f)); const setColor = (i: number, color: string) => setForm((f) => (f ? f.map((d, j) => (j === i ? { ...d, color } : d)) : f)); const addLevel = () => setForm((f) => { if (!f || f.length >= MAX_LEVEL_COUNT) return f; const last = f[f.length - 1]; return [...f, { threshold: (last?.threshold ?? 0) + 100, color: "neutral" }]; }); const removeLast = () => setForm((f) => (f && f.length > 2 ? f.slice(0, -1) : f)); const save = async () => { if (saving || !dirty || !form) return; if (form.length < 2) { toast("至少保留 2 个等级"); return; } if (form[0].threshold !== 0) { toast("Lv.1 所需积分须为 0"); return; } for (let i = 1; i < form.length; i++) { if (!(form[i].threshold > form[i - 1].threshold)) { toast(`Lv.${i + 1} 所需积分须大于上一级`); return; } } setSaving(true); try { const res = await apiUpdateSiteSettings({ levels: form, levels_fx: fx }); const defs = cloneDefs(res.levels); setForm(defs); setSaved(defs); setSavedFx(fx); toast("等级配置已保存", "ok"); } catch (e) { toast(e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } }; // 重置:仅回填默认等级表(2–12 约束内),需再点保存才生效 const resetToDefault = () => { if (!form) return; setForm(cloneDefs(DEFAULT_LEVEL_DEFS)); setResetOpen(false); toast("已恢复默认等级表,点击保存生效", "ok"); }; return (

等级管理

用户等级由累计获得积分决定,达到某级阈值即晋升。可配置等级数量(2–{MAX_LEVEL_COUNT}{" "} 级)、每级所需积分与配色;等级徽章仅显示 Lv.n,不弹悬浮提示。

{!form ? (

加载等级配置…

) : form.length === 0 ? (

等级配置加载失败,请刷新页面重试。

) : (
{form.map((d, i) => { const n = i + 1; const tokens = LEVEL_COLOR_TOKENS[d.color] ?? LEVEL_COLOR_TOKENS.neutral; const textBg = splitBackground(tokens.text ?? "none"); const isLast = i === form.length - 1; return (
{/* 预览:与 LevelBadge 同构(外层底色 + .lv-text 文字层),实时反映未保存的修改 */} Lv.{n}

所需累计积分

setThreshold(i, v)} min={0} max={1000000} unit="分" ariaLabel={`Lv.${n} 所需积分`} disabled={saving || n === 1} />

配色(前景/背景)

{LEVEL_COLOR_KEYS.map((c) => { const t = LEVEL_COLOR_TOKENS[c]; const active = d.color === c; return (
{isLast && ( )}
); })} {/* 添加新等级:整宽虚线条目,与上方项同宽 */} {form.length < MAX_LEVEL_COUNT ? ( ) : (
已达 {MAX_LEVEL_COUNT} 级上限
)}
)}
setFx((v) => !v)} label="特效动画" />
{dirty ? "有未保存的修改" : "已保存的配色在页面刷新后生效,等级数值即时按新阈值重算"}
setResetOpen(false)} onConfirm={resetToDefault} />
); }