feat: 用户徽章与等级体系,积分经济管理重构

- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件
- 新增等级模型:model/level 及前端 levels 元数据
- 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置
- 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
2026-09-25 16:38:29 +08:00
parent 7b327bc8cc
commit 4ae57d9392
57 changed files with 3939 additions and 147 deletions

View File

@@ -0,0 +1,93 @@
"use client";
import { useCallback, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { AdminSegmented } from "@/components/admin";
import type { EconomyStats, PublicSettings } from "@/lib/api";
import EconomyOverview from "./EconomyOverview";
import EconomySettings from "../settings/EconomySettings";
import UserPointsTab from "./UserPointsTab";
type TabKey = "overview" | "rules" | "users";
const TAB_OPTIONS = [
{ key: "overview", label: "经济总览" },
{ key: "rules", label: "积分规则" },
{ key: "users", label: "用户积分" },
];
/**
* 积分管理页标签壳:经济总览(管理员及以上)/ 积分规则 + 用户积分(超管/站长)。
* tab 记在 URL query(?tab=)便于刷新/分享保持;三个面板保持挂载、用 display 切换,
* 避免切标签丢失规则的未保存修改与用户列表状态。
*/
export default function PointsTabs({
stats,
settings,
canManage,
}: {
stats: EconomyStats | null;
settings: PublicSettings | null;
canManage: boolean;
}) {
const router = useRouter();
const searchParams = useSearchParams();
const queryTab = searchParams.get("tab");
const [tab, setTab] = useState<TabKey>(() => {
const t = queryTab;
if (canManage && (t === "rules" || t === "users")) return t;
return "overview";
});
const switchTab = useCallback(
(next: string) => {
setTab(next as TabKey);
const params = new URLSearchParams(searchParams.toString());
if (next === "overview") params.delete("tab");
else params.set("tab", next);
const qs = params.toString();
router.replace(qs ? `?${qs}` : "?", { scroll: false });
},
[router, searchParams]
);
const options = canManage
? TAB_OPTIONS
: TAB_OPTIONS.filter((o) => o.key === "overview");
return (
<div>
{canManage ? (
<div className="mb-4">
<AdminSegmented
options={options}
value={tab}
onChange={(v) => switchTab(String(v))}
ariaLabel="积分管理标签"
/>
</div>
) : null}
{/* 各面板保持挂载(display 切换):规则未保存修改与用户列表状态不因切标签丢失 */}
<div style={tab === "overview" ? undefined : { display: "none" }}>
{stats ? (
<EconomyOverview stats={stats} />
) : (
<div className="panel p-8 text-center">
<p className="meta">暂时无法加载积分数据</p>
</div>
)}
</div>
{canManage ? (
<>
<div style={tab === "rules" ? undefined : { display: "none" }}>
{settings ? <EconomySettings initial={settings} /> : null}
</div>
<div style={tab === "users" ? undefined : { display: "none" }}>
<UserPointsTab />
</div>
</>
) : null}
</div>
);
}