feat: 积分流水互转合并展示,并优化个人主页与经济看板

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 02:37:12 +08:00
parent b3a4b8bb03
commit fa713bfc3f
9 changed files with 1098 additions and 230 deletions

View File

@@ -1,5 +1,6 @@
"use client";
import type { ComponentProps } from "react";
import {
Area,
AreaChart,
@@ -26,6 +27,14 @@ const TOOLTIP_STYLE = {
color: "var(--ink)",
};
const LEGEND_STYLE = { fontSize: 12, overflow: "hidden" as const };
const LEGEND_STYLE_SM = { fontSize: 11, overflow: "hidden" as const };
/** 统一 Tooltip:关闭默认 cursor 灰框,避免选中/悬停黑边 */
function ChartTooltip(props: Omit<ComponentProps<typeof Tooltip>, "contentStyle" | "cursor">) {
return <Tooltip {...props} cursor={false} contentStyle={TOOLTIP_STYLE} />;
}
function shortDate(d: string) {
if (!d || d.length < 10) return d;
return d.slice(5);
@@ -55,7 +64,13 @@ export function AdminChartCard({
</div>
{actions}
</div>
<div className="flex-1 min-h-[180px] w-full overflow-x-auto">{children}</div>
{/* 捕获阶段拦截 mousedown:冒泡阶段太晚,浏览器已开始 SVG 多柱选区黑框 */}
<div
className="flex-1 min-h-[180px] w-full overflow-hidden select-none [&_*]:select-none [&_svg]:outline-none [&_.recharts-surface]:outline-none [&_.recharts-tooltip-cursor]:hidden"
onMouseDownCapture={(e) => e.preventDefault()}
>
{children}
</div>
</div>
);
}
@@ -68,7 +83,7 @@ export function VisitTrendChart({
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 }}>
<AreaChart data={rows} margin={{ top: 12, right: 12, left: 0, bottom: 0 }} accessibilityLayer={false}>
<defs>
<linearGradient id="j13Pv" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--accent)" stopOpacity={0.35} />
@@ -82,8 +97,8 @@ export function VisitTrendChart({
<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 }} />
<ChartTooltip />
<Legend wrapperStyle={LEGEND_STYLE} />
<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>
@@ -111,12 +126,12 @@ export function DualLineChart({
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 }}>
<LineChart data={rows} margin={{ top: 12, right: 12, left: 0, bottom: 0 }} accessibilityLayer={false}>
<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 }} />
<ChartTooltip />
<Legend wrapperStyle={LEGEND_STYLE} />
<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>
@@ -138,11 +153,11 @@ export function SingleAreaChart({
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 }}>
<AreaChart data={rows} margin={{ top: 12, right: 12, left: 0, bottom: 0 }} accessibilityLayer={false}>
<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} />
<ChartTooltip />
<Area type="monotone" dataKey={dataKey} name={name} stroke={color} fill={color} fillOpacity={0.15} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
@@ -170,24 +185,24 @@ export function NamedBarChart({
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 }}>
<BarChart data={data} layout="vertical" margin={{ top: 8, right: 12, left: 8, bottom: 4 }} accessibilityLayer={false}>
<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]} />
<ChartTooltip />
<Bar dataKey="count" name="数量" fill="var(--accent)" radius={[0, 6, 6, 0]} activeBar={false} />
</BarChart>
</ResponsiveContainer>
);
}
return (
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<BarChart data={data} margin={{ top: 12, right: 12, left: 0, bottom: 0 }} accessibilityLayer={false}>
<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]} />
<ChartTooltip />
<Bar dataKey="count" name="数量" fill="var(--accent)" radius={[6, 6, 0, 0]} activeBar={false} />
</BarChart>
</ResponsiveContainer>
);
@@ -197,20 +212,19 @@ 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>
<PieChart accessibilityLayer={false}>
<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}
<ChartTooltip
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 }} />
<Legend wrapperStyle={LEGEND_STYLE_SM} />
</PieChart>
</ResponsiveContainer>
);
@@ -224,14 +238,14 @@ export function DeltaBarChart({
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 }}>
<BarChart data={rows} margin={{ top: 12, right: 12, left: 0, bottom: 0 }} accessibilityLayer={false}>
<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]} />
<ChartTooltip />
<Legend wrapperStyle={LEGEND_STYLE} />
<Bar dataKey="granted" name="发放" fill="var(--ok)" radius={[4, 4, 0, 0]} activeBar={false} />
<Bar dataKey="spent" name="消耗" fill="var(--clay)" radius={[4, 4, 0, 0]} activeBar={false} />
</BarChart>
</ResponsiveContainer>
);