feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
170
frontend/app/admin/economy/EconomyClient.tsx
Normal file
170
frontend/app/admin/economy/EconomyClient.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user