From 22b5eab7694ebedb01949129794e41b4d0c3923a Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Thu, 23 Apr 2026 15:44:10 +0800 Subject: [PATCH] fix(admin): restore LineChart component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LineChart is used by the dashboard page to render DAU trend charts. It was accidentally removed during metrics cleanup — restore it. --- admin/src/components/admin/LineChart.tsx | 169 +++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 admin/src/components/admin/LineChart.tsx diff --git a/admin/src/components/admin/LineChart.tsx b/admin/src/components/admin/LineChart.tsx new file mode 100644 index 0000000..f50da74 --- /dev/null +++ b/admin/src/components/admin/LineChart.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { format, parseISO } from "date-fns"; + +interface DataPoint { + date: string; + value: number; +} + +interface LineChartProps { + data: DataPoint[]; + width?: number; + height?: number; + color?: string; + label: string; + unit?: string; +} + +export default function LineChart({ + data, + width = 600, + height = 200, + color = "#6366f1", + label, + unit = "", +}: LineChartProps) { + if (data.length === 0) { + return ( +
+ 暂无数据 +
+ ); + } + + const padding = { top: 16, right: 16, bottom: 36, left: 44 }; + const chartWidth = width - padding.left - padding.right; + const chartHeight = height - padding.top - padding.bottom; + + const values = data.map((d) => d.value); + const maxValue = Math.max(...values, 1); + const minValue = 0; + + function xScale(i: number) { + return padding.left + (i / (data.length - 1 || 1)) * chartWidth; + } + + function yScale(v: number) { + return padding.top + chartHeight - ((v - minValue) / (maxValue - minValue || 1)) * chartHeight; + } + + // Build SVG path + const pathD = data + .map((d, i) => `${i === 0 ? "M" : "L"} ${xScale(i).toFixed(1)} ${yScale(d.value).toFixed(1)}`) + .join(" "); + + // Area path (fill under the line) + const areaD = + pathD + + ` L ${xScale(data.length - 1).toFixed(1)} ${(padding.top + chartHeight).toFixed(1)}` + + ` L ${padding.left} ${(padding.top + chartHeight).toFixed(1)} Z`; + + // Y-axis ticks (5 ticks) + const yTicks = Array.from({ length: 5 }, (_, i) => { + const v = minValue + ((maxValue - minValue) * i) / 4; + return { v, y: yScale(v) }; + }); + + // X-axis ticks (show every nth label based on data length) + const step = Math.max(1, Math.floor(data.length / 6)); + const xTicks = data.filter((_, i) => i % step === 0 || i === data.length - 1); + + const gradientId = `grad-${label.replace(/\s/g, "-")}`; + + return ( + + + + + + + + + {/* Y-axis gridlines */} + {yTicks.map((t, i) => ( + + ))} + + {/* Y-axis labels */} + {yTicks.map((t, i) => ( + + {Math.round(t.v)} + + ))} + + {/* X-axis labels */} + {xTicks.map((d, i) => { + const idx = data.findIndex((x) => x.date === d.date); + return ( + + {(() => { + try { + return format(parseISO(d.date), "MM-dd"); + } catch { + return d.date.slice(5); + } + })()} + + ); + })} + + {/* Area fill */} + + + {/* Line */} + + + {/* Data points (dots) */} + {data.map((d, i) => ( + + + {(() => { + try { + return `${format(parseISO(d.date), "yyyy-MM-dd")}: ${d.value}${unit}`; + } catch { + return `${d.date}: ${d.value}${unit}`; + } + })()} + + + ))} + + ); +}