降权全站字重;优化右栏公告/数据块;类型状态徽章用固定浅色区分;TipHost 仅在文案不一致、非文字或省略时显示。 Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { cookies } from "next/headers";
|
|
import { Coins, Flame, Gift, Layers } from "lucide-react";
|
|
import { fetchAdminEconomyStats } from "@/lib/api";
|
|
import { authCookieHeader } from "@/lib/cookies";
|
|
import { getMeCached } from "@/lib/serverData";
|
|
import { isAdminOrAbove } from "@/lib/roles";
|
|
import { postTypeLabel } from "@/lib/postMeta";
|
|
import { pointReasonLabel } from "@/lib/points";
|
|
import { formatRelative } from "@/lib/format";
|
|
import Forbidden from "../Forbidden";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "积分看板",
|
|
};
|
|
|
|
const fmt = (n: number) => n.toLocaleString("zh-CN");
|
|
|
|
export default async function AdminEconomyPage() {
|
|
const cookie = authCookieHeader(await cookies());
|
|
const me = await getMeCached(cookie || undefined);
|
|
if (!me.user || !isAdminOrAbove(me.user.role)) {
|
|
return <Forbidden text="积分看板仅管理员及以上可查看。" />;
|
|
}
|
|
|
|
let stats: Awaited<ReturnType<typeof fetchAdminEconomyStats>> | null = null;
|
|
try {
|
|
stats = await fetchAdminEconomyStats(cookie);
|
|
} catch {
|
|
stats = null;
|
|
}
|
|
|
|
if (!stats) {
|
|
return (
|
|
<div className="panel p-8 text-center">
|
|
<p className="meta">暂时无法加载积分数据</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const cards = [
|
|
{
|
|
label: "用户余额合计",
|
|
value: fmt(stats.total_balance),
|
|
icon: <Coins size={18} />,
|
|
hint: "全站可用积分之和",
|
|
},
|
|
{
|
|
label: "今日签到",
|
|
value: fmt(stats.checkin_today),
|
|
icon: <Flame size={18} />,
|
|
hint: "今日已签到人数",
|
|
},
|
|
{
|
|
label: "托管中悬赏",
|
|
value: `${fmt(stats.open_bounties)} / ${fmt(stats.escrowed_points)}`,
|
|
icon: <Gift size={18} />,
|
|
hint: "未结算帖数 / 托管积分",
|
|
},
|
|
{
|
|
label: "流水条数",
|
|
value: fmt(stats.ledger_count),
|
|
icon: <Layers size={18} />,
|
|
hint: "历史入账出账记录",
|
|
},
|
|
];
|
|
|
|
const typeEntries = Object.entries(stats.posts_by_type || {}).sort(
|
|
(a, b) => b[1] - a[1]
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-6">
|
|
<h1 className="text-[22px] font-extrabold" style={{ color: "var(--ink)" }}>
|
|
积分与类型帖看板
|
|
</h1>
|
|
<p className="meta text-[13px] mt-1">
|
|
观察全站积分流通、悬赏托管与帖子类型分布
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
|
{cards.map((c) => (
|
|
<div key={c.label} className="panel p-5">
|
|
<div className="flex items-center gap-2 meta text-[12.5px]">
|
|
<span style={{ color: "var(--accent)" }}>{c.icon}</span>
|
|
{c.label}
|
|
</div>
|
|
<p
|
|
className="mt-3 text-[22px] font-extrabold tabular-nums leading-none"
|
|
style={{ color: "var(--ink)" }}
|
|
>
|
|
{c.value}
|
|
</p>
|
|
<p className="meta text-[12px] mt-2">{c.hint}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
|
<section className="panel overflow-hidden">
|
|
<div
|
|
className="px-5 py-3.5 font-medium text-[15px]"
|
|
style={{ borderBottom: "1px solid var(--line)", color: "var(--ink)" }}
|
|
>
|
|
帖子类型分布
|
|
</div>
|
|
{typeEntries.length === 0 ? (
|
|
<p className="meta text-center py-10">暂无数据</p>
|
|
) : (
|
|
<ul>
|
|
{typeEntries.map(([t, n]) => (
|
|
<li
|
|
key={t}
|
|
className="px-5 py-3 flex items-center justify-between"
|
|
style={{ borderTop: "1px solid var(--line)" }}
|
|
>
|
|
<span className="text-[14px]" style={{ color: "var(--ink)" }}>
|
|
{postTypeLabel(t)}
|
|
</span>
|
|
<span className="tabular-nums meta">{fmt(n)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
|
|
<section className="panel overflow-hidden">
|
|
<div
|
|
className="px-5 py-3.5 font-medium text-[15px]"
|
|
style={{ borderBottom: "1px solid var(--line)", color: "var(--ink)" }}
|
|
>
|
|
最近流水
|
|
</div>
|
|
{(stats.recent_ledger || []).length === 0 ? (
|
|
<p className="meta text-center py-10">暂无流水</p>
|
|
) : (
|
|
<ul>
|
|
{stats.recent_ledger.map((it) => (
|
|
<li
|
|
key={it.id}
|
|
className="px-5 py-3 flex items-start justify-between gap-3"
|
|
style={{ borderTop: "1px solid var(--line)" }}
|
|
>
|
|
<div className="min-w-0">
|
|
<p className="text-[13.5px] truncate" style={{ color: "var(--ink)" }}>
|
|
{pointReasonLabel(it.reason)}
|
|
{it.note ? ` · ${it.note}` : ""}
|
|
</p>
|
|
<p className="meta text-[12px] mt-0.5">{formatRelative(it.created_at)}</p>
|
|
</div>
|
|
<span
|
|
className="tabular-nums font-medium shrink-0"
|
|
style={{ color: it.delta >= 0 ? "var(--accent)" : "var(--danger)" }}
|
|
>
|
|
{it.delta >= 0 ? `+${it.delta}` : it.delta}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
</div>
|
|
|
|
<p className="meta text-[12.5px] mt-5">
|
|
用户可在{" "}
|
|
<Link href="/points" className="hover:text-[var(--accent)]">
|
|
/points
|
|
</Link>{" "}
|
|
查看个人流水。
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|