Files
jiang13-bbs/frontend/components/admin/charts.tsx

239 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Line,
LineChart,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const TOOLTIP_STYLE = {
background: "var(--panel)",
border: "1px solid var(--line)",
borderRadius: 12,
fontSize: 12,
color: "var(--ink)",
};
function shortDate(d: string) {
if (!d || d.length < 10) return d;
return d.slice(5);
}
export function AdminChartCard({
title,
hint,
children,
actions,
}: {
title: string;
hint?: string;
children: React.ReactNode;
actions?: React.ReactNode;
}) {
return (
<div className="panel p-4 sm:p-5 flex flex-col min-h-[260px]">
<div className="flex items-start justify-between gap-2 mb-3">
<div className="min-w-0">
<h3 className="text-[14.5px] font-semibold" style={{ color: "var(--ink)" }}>
{title}
</h3>
{hint ? (
<p className="meta text-[11.5px] mt-0.5 leading-relaxed">{hint}</p>
) : null}
</div>
{actions}
</div>
<div className="flex-1 min-h-[180px] w-full overflow-x-auto">{children}</div>
</div>
);
}
export function VisitTrendChart({
data,
}: {
data: { date: string; pv: number; uv: number }[];
}) {
const rows = data.map((d) => ({ ...d, label: shortDate(d.date) }));
return (
<ResponsiveContainer width="100%" height={220}>
<AreaChart data={rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="j13Pv" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--accent)" stopOpacity={0.35} />
<stop offset="100%" stopColor="var(--accent)" stopOpacity={0} />
</linearGradient>
<linearGradient id="j13Uv" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--ok)" stopOpacity={0.3} />
<stop offset="100%" stopColor="var(--ok)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid stroke="var(--line)" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="label" tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} width={36} allowDecimals={false} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Area type="monotone" dataKey="pv" name="PV" stroke="var(--accent)" fill="url(#j13Pv)" strokeWidth={2} />
<Area type="monotone" dataKey="uv" name="UV" stroke="var(--ok)" fill="url(#j13Uv)" strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
);
}
export function DualLineChart({
data,
aKey,
bKey,
aName,
bName,
aColor = "var(--accent)",
bColor = "var(--gold)",
}: {
data: { date: string; [k: string]: string | number }[];
aKey: string;
bKey: string;
aName: string;
bName: string;
aColor?: string;
bColor?: string;
}) {
const rows = data.map((d) => ({ ...d, label: shortDate(String(d.date)) }));
return (
<ResponsiveContainer width="100%" height={200}>
<LineChart data={rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<CartesianGrid stroke="var(--line)" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="label" tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} width={36} allowDecimals={false} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Line type="monotone" dataKey={aKey} name={aName} stroke={aColor} strokeWidth={2} dot={false} />
<Line type="monotone" dataKey={bKey} name={bName} stroke={bColor} strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
);
}
export function SingleAreaChart({
data,
dataKey,
name,
color = "var(--accent)",
}: {
data: { date: string; [k: string]: string | number }[];
dataKey: string;
name: string;
color?: string;
}) {
const rows = data.map((d) => ({ ...d, label: shortDate(String(d.date)) }));
return (
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<CartesianGrid stroke="var(--line)" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="label" tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} width={36} allowDecimals={false} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Area type="monotone" dataKey={dataKey} name={name} stroke={color} fill={color} fillOpacity={0.15} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
);
}
const PIE_COLORS = [
"var(--accent)",
"var(--ok)",
"var(--gold)",
"var(--clay)",
"var(--danger)",
"#8b5cf6",
"#06b6d4",
"#84cc16",
];
export function NamedBarChart({
data,
layout = "vertical",
}: {
data: { label: string; count: number }[];
layout?: "vertical" | "horizontal";
}) {
if (layout === "horizontal") {
return (
<ResponsiveContainer width="100%" height={Math.max(180, data.length * 28)}>
<BarChart data={data} layout="vertical" margin={{ top: 4, right: 12, left: 8, bottom: 4 }}>
<CartesianGrid stroke="var(--line)" strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} allowDecimals={false} />
<YAxis type="category" dataKey="label" width={72} tick={{ fill: "var(--ink-2)", fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Bar dataKey="count" name="数量" fill="var(--accent)" radius={[0, 6, 6, 0]} />
</BarChart>
</ResponsiveContainer>
);
}
return (
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<CartesianGrid stroke="var(--line)" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="label" tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} width={36} allowDecimals={false} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Bar dataKey="count" name="数量" fill="var(--accent)" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
}
export function NamedPieChart({ data }: { data: { label: string; count: number }[] }) {
const total = data.reduce((s, d) => s + d.count, 0) || 1;
return (
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie data={data} dataKey="count" nameKey="label" innerRadius={48} outerRadius={72} paddingAngle={2}>
{data.map((_, i) => (
<Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={TOOLTIP_STYLE}
formatter={(value, name) => {
const n = typeof value === "number" ? value : Number(value) || 0;
return [`${n}(${Math.round((n / total) * 100)}%)`, String(name ?? "")];
}}
/>
<Legend wrapperStyle={{ fontSize: 11 }} />
</PieChart>
</ResponsiveContainer>
);
}
export function DeltaBarChart({
data,
}: {
data: { date: string; granted: number; spent: number }[];
}) {
const rows = data.map((d) => ({ ...d, label: shortDate(d.date) }));
return (
<ResponsiveContainer width="100%" height={200}>
<BarChart data={rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<CartesianGrid stroke="var(--line)" strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="label" tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "var(--ink-3)", fontSize: 11 }} axisLine={false} tickLine={false} width={40} allowDecimals={false} />
<Tooltip contentStyle={TOOLTIP_STYLE} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Bar dataKey="granted" name="发放" fill="var(--ok)" radius={[4, 4, 0, 0]} />
<Bar dataKey="spent" name="消耗" fill="var(--clay)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
);
}