From e70c0602c8b82e779adae8188d173a32a9a407bf Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Wed, 29 Apr 2026 11:50:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E9=A6=96=E9=A1=B5=E5=AE=9E?= =?UTF-8?q?=E8=A3=85=E5=8E=86=E5=8F=B2=E5=87=80=E5=80=BC=E9=9D=A2=E7=A7=AF?= =?UTF-8?q?=E8=B5=B0=E5=8A=BF=E5=9B=BE=EF=BC=8C=E5=B9=B6=E6=8C=82=E8=BD=BD?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E5=BF=AB=E7=85=A7=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 1 + app/dashboard/page.tsx | 21 +++ src/components/dashboard/net-worth-chart.tsx | 141 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/components/dashboard/net-worth-chart.tsx diff --git a/Memory.md b/Memory.md index 2848744..d624a24 100644 --- a/Memory.md +++ b/Memory.md @@ -30,6 +30,7 @@ - 打通 `/dashboard/assets` 与 `/dashboard/transactions` 页面前后端数据流转,修复早期录入与 404 缺陷。 - 完成 UI 层高精度数据格式化,针对不同资产类型实现动态精度展示,清理因数据库 `numeric` 导致的尾随零问题。 - 引入 `recharts` 图表引擎,构建了基于实时 CNY 估值的资产分布环形图。 +- 实装基于 Recharts 的历史净值面积图 (NetWorthChart),支持总市值与投入本金的双轨趋势对比。 - 优化表单交互:实装了交易所与币种的智能联动逻辑,并运用 `disabled` 属性实现了表单字段的只读防腐锁定。 ## UX 与全局交互 (UI/UX) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 019e1f1..834159c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -22,8 +22,10 @@ import { import { toast } from 'sonner'; import { getPortfolioSummary } from '@/actions/portfolio'; import { getAssets } from '@/actions/asset'; +import { recordDailySnapshot, getSnapshots } from '@/actions/snapshots'; import { formatQuantity, formatAmount } from '@/lib/formatters'; import AllocationChart from '@/components/dashboard/allocation-chart'; +import NetWorthChart from '@/components/dashboard/net-worth-chart'; import { SyncButton } from '@/components/assets/sync-button'; import { AddTransactionDialog } from '@/components/transactions/add-transaction-dialog'; import { UpdateTransactionDialog } from '@/components/transactions/update-transaction-dialog'; @@ -84,6 +86,7 @@ export default function DashboardPage() { const [isPending, startTransition] = useTransition(); const [updateTarget, setUpdateTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); + const [snapshots, setSnapshots] = useState([]); useEffect(() => { async function loadData() { @@ -99,6 +102,15 @@ export default function DashboardPage() { loadData(); }, []); + useEffect(() => { + async function loadSnapshots() { + await recordDailySnapshot(); + const data = await getSnapshots({ limit: 30 }); + setSnapshots(data); + } + loadSnapshots(); + }, []); + const toggleExpand = (id: string) => { setExpandedIds(prev => ({ ...prev, [id]: !prev[id] })); }; @@ -170,6 +182,15 @@ export default function DashboardPage() { + + + 净值走势 + + + + + + 持仓明细 diff --git a/src/components/dashboard/net-worth-chart.tsx b/src/components/dashboard/net-worth-chart.tsx new file mode 100644 index 0000000..d1bc128 --- /dev/null +++ b/src/components/dashboard/net-worth-chart.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; + +interface Snapshot { + date: string; + totalValueCny: string; + totalCostCny: string; +} + +interface NetWorthChartProps { + snapshots: Snapshot[]; +} + +function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: string; payload: Snapshot }>; label?: string }) { + if (active && payload && payload.length) { + const data = payload[0].payload; + const totalValue = parseFloat(data.totalValueCny); + const totalCost = parseFloat(data.totalCostCny); + const pnl = totalValue - totalCost; + const pnlPercent = totalCost > 0 ? (pnl / totalCost) * 100 : 0; + const isPositive = pnl >= 0; + + const formattedDate = (label || '').replace(/-/g, '/'); + + return ( +
+
+ {formattedDate} +
+
+ + + 总市值 + + + ¥{totalValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+
+ + + 投入本金 + + + ¥{totalCost.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + +
+
+ 净盈亏 + + {isPositive ? '+' : ''}¥{pnl.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + {' '}({isPositive ? '+' : ''}{pnlPercent.toFixed(2)}%) + +
+
+ ); + } + return null; +} + +export default function NetWorthChart({ snapshots }: NetWorthChartProps) { + if (!snapshots || snapshots.length === 0) { + return ( +
+ 暂无历史数据 +
+ ); + } + + const chartData = snapshots.map(s => ({ + date: s.date.replace(/-/g, '/'), + totalValueCny: parseFloat(s.totalValueCny), + totalCostCny: parseFloat(s.totalCostCny), + })); + + return ( +
+ + + + + + + + + + + + + + { + if (value >= 1000000) return `¥${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `¥${(value / 1000).toFixed(0)}K`; + return `¥${value.toFixed(0)}`; + }} + /> + } /> + + + + +
+ ); +}