171 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|