161 lines
5.3 KiB
TypeScript
161 lines
5.3 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 { AdminPageHeader, AdminStatCard } from "@/components/admin";
|
|
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 typeEntries = Object.entries(stats.posts_by_type || {}).sort(
|
|
(a, b) => b[1] - a[1]
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<AdminPageHeader
|
|
icon={Coins}
|
|
title="积分与类型帖看板"
|
|
description="观察全站积分流通、悬赏托管与帖子类型分布"
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 mb-5">
|
|
<AdminStatCard
|
|
label="用户余额合计"
|
|
value={fmt(stats.total_balance)}
|
|
hint="全站可用积分之和"
|
|
icon={<Coins size={16} />}
|
|
/>
|
|
<AdminStatCard
|
|
label="今日签到"
|
|
value={fmt(stats.checkin_today)}
|
|
hint="今日已签到人数"
|
|
icon={<Flame size={16} />}
|
|
color="var(--clay)"
|
|
soft="var(--clay-soft)"
|
|
/>
|
|
<AdminStatCard
|
|
label="托管中悬赏"
|
|
value={`${fmt(stats.open_bounties)} / ${fmt(stats.escrowed_points)}`}
|
|
hint="未结算帖数 / 托管积分"
|
|
icon={<Gift size={16} />}
|
|
color="var(--gold)"
|
|
soft="var(--gold-soft)"
|
|
/>
|
|
<AdminStatCard
|
|
label="流水条数"
|
|
value={fmt(stats.ledger_count)}
|
|
hint="历史入账出账记录"
|
|
icon={<Layers size={16} />}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<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], idx) => (
|
|
<li
|
|
key={t}
|
|
className="px-5 py-3 flex items-center justify-between"
|
|
style={idx === 0 ? undefined : { 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, idx) => (
|
|
<li
|
|
key={it.id}
|
|
className="px-5 py-3 flex items-start justify-between gap-3"
|
|
style={idx === 0 ? undefined : { 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>
|
|
);
|
|
}
|