feat: 积分流水互转合并展示,并优化个人主页与经济看板
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user