Files
jiang13-bbs/frontend/app/admin/economy/EconomyClient.tsx

171 lines
5.6 KiB
TypeScript

"use client";
import Link from "next/link";
import { Coins, Flame, Gift, TrendingUp } from "lucide-react";
import type { EconomyStats } from "@/lib/api";
import { postTypeLabel } from "@/lib/postMeta";
import { pointReasonLabel } from "@/lib/points";
import { formatRelative } from "@/lib/format";
import { AdminPageHeader, AdminStatCard } from "@/components/admin";
import {
AdminChartCard,
DeltaBarChart,
NamedBarChart,
NamedPieChart,
SingleAreaChart,
} from "@/components/admin/charts";
const fmt = (n: number) => n.toLocaleString("zh-CN");
function netDeltaLabel(n: number) {
if (n > 0) return `+${fmt(n)}`;
return fmt(n);
}
export default function EconomyClient({ stats }: { stats: EconomyStats }) {
const checkinSeries = (stats.checkin_series || []).map((d) => ({
date: d.date,
count: d.count,
}));
const ledgerByReason = (stats.ledger_by_reason || []).map((r) => ({
label: pointReasonLabel(r.key || r.label),
count: r.count,
}));
const deltaSeries = stats.delta_series || [];
const postsByType = Object.entries(stats.posts_by_type || {})
.map(([t, n]) => ({ label: postTypeLabel(t), count: Number(n) || 0 }))
.filter((d) => d.count > 0)
.sort((a, b) => b.count - a.count);
const net = stats.net_delta_7d ?? 0;
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="近 7 日净变动"
value={netDeltaLabel(net)}
hint="发放减去消耗"
icon={<TrendingUp size={16} />}
color={net >= 0 ? "var(--ok)" : "var(--danger)"}
soft={net >= 0 ? "var(--ok-soft)" : "var(--danger-soft)"}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
<AdminChartCard title="签到趋势" hint="近 14 日每日签到人数">
{checkinSeries.length === 0 ? (
<p className="meta text-center py-16">暂无签到数据</p>
) : (
<SingleAreaChart
data={checkinSeries}
dataKey="count"
name="签到"
color="var(--clay)"
/>
)}
</AdminChartCard>
<AdminChartCard title="流水原因分布" hint="按入账原因聚合条数">
{ledgerByReason.length === 0 ? (
<p className="meta text-center py-16">暂无流水</p>
) : (
<NamedBarChart data={ledgerByReason} layout="horizontal" />
)}
</AdminChartCard>
<AdminChartCard title="发放 / 消耗" hint="近 14 日积分进出">
{deltaSeries.length === 0 ? (
<p className="meta text-center py-16">暂无数据</p>
) : (
<DeltaBarChart data={deltaSeries} />
)}
</AdminChartCard>
<AdminChartCard title="帖子类型分布" hint="全站帖子按类型占比">
{postsByType.length === 0 ? (
<p className="meta text-center py-16">暂无数据</p>
) : (
<NamedPieChart data={postsByType} />
)}
</AdminChartCard>
</div>
<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>
<p className="meta text-[12.5px] mt-5">
用户可在{" "}
<Link href="/points" className="hover:text-[var(--accent)]">
/points
</Link>{" "}
查看个人流水。
</p>
</div>
);
}