From 110e75f0a15cb51985e6505d9647da8fbb76a00c Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Tue, 28 Apr 2026 15:46:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=BC=95=E5=85=A5=20sonner=20?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8F=8D=E9=A6=88=E7=B3=BB=E7=BB=9F=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=9C=A8=E9=A6=96=E9=A1=B5=E9=83=A8=E7=BD=B2=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E8=A1=8C=E6=83=85=E5=90=8C=E6=AD=A5=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 48 +++++++++++++------ app/dashboard/page.tsx | 5 ++ app/layout.tsx | 2 + package-lock.json | 11 +++++ package.json | 1 + src/components/assets/add-asset-dialog.tsx | 2 + src/components/assets/sync-button.tsx | 10 +++- .../transactions/add-transaction-dialog.tsx | 2 + src/components/ui/sonner.tsx | 45 +++++++++++++++++ 9 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 src/components/ui/sonner.tsx diff --git a/Memory.md b/Memory.md index 74d81a3..58b5dd2 100644 --- a/Memory.md +++ b/Memory.md @@ -1,17 +1,35 @@ -完成根目录的 Next.js 初始化、基础依赖安装与环境变量配置。 +# Omniledger 架构与开发记忆 (Memory) +## 基础设施与底层架构 +- 完成根目录的 Next.js 初始化、基础依赖安装与环境变量配置。 - 完成基于单例模式的数据库连接配置,并设定 Drizzle 迁移工具。 -- 修复网络连接,成功将 users 表推送至数据库。 -- 成功定義資產枚舉與 assets 表,支持跨資產標識。 -- 完成核心 transactions 表的建立,並嚴格運用了 numeric(36,18) 的高精度配置。 -- 完成高精度交易流水 (transactions) 的 Server Actions 开发,成功实现了字符串级别的高精度防腐层拦截。 -- 完成 shadcn/ui 初始化,集成 next-themes,并拉取核心组件库。 -- 完成 /dashboard 基础布局架构,接管根路由。 -- 引入 Zod 和 Big.js,完成资产表 (assets) 的 Server Actions 读写接口开发。 -- 完成 /dashboard/assets 页面,成功打通前后端资产录入数据流转。 -- 修复 /dashboard/transactions 404,完成高精度流水录入与展示功能。 -- 完成 UI 层的高精度数据格式化,实现不同资产类型的动态展示精度。 -- 完成持仓聚合计算逻辑,并构建了 Dashboard 首页持仓卡片矩阵。 -- 引入 recharts 图表引擎,完成 Dashboard 资产分布环形图的构建。 -- 汇率表已建立,支持跨币种(如 BTC->USD)的交叉汇率架构。 -- 资产表新增 name 字段,并补全了资产与流水的增删改查 Actions(updateAsset、deleteAsset、updateTransaction、deleteTransaction),createTransaction 支持根据 exchange 自动判定 txCurrency。 \ No newline at end of file +- 修复网络连接,成功将 tables 推送至 PostgreSQL 数据库。 +- 统一规范 Git 全量提交机制 (`git add -A`),确立了严谨的代码版本控制防腐层。 + +## 数据库设计 (Schema) +- 成功定义资产枚举与 `assets` 表,支持跨资产标识。 +- 完成核心 `transactions` (交易流水) 表的建立,并严格运用了 `numeric(36,18)` 的高精度配置。 +- `assets` 表完成多次业务演进:新增 `latestPrice` (支持现价追踪)、`exchange` (显式交易所绑定) 以及 `name` (中文名称解析) 字段。 +- `exchange_rates` (汇率表) 已建立,支持联合主键与跨币种交叉汇率架构。 + +## 核心业务与服务端逻辑 (Server Actions) +- 完成高精度交易流水与资产的 Server Actions 开发,成功实现字符串级别的高精度防腐层拦截(基于 Zod & Big.js)。 +- 补全资产与流水的全栈增删改查 (CRUD) 操作,`createTransaction` 现已支持根据 `exchange` 自动判定并锁定 `txCurrency`。 +- **估值与 P&L 引擎:** 完成底层估值引擎升级,打通交叉汇率换算逻辑;实现原币种 (Native) 与本位币 (CNY Base) 双轨制的历史成本追溯与真实盈亏 (P&L) 计算引擎。 + +## 外部行情接口与网络 (Market Data Engines) +- **股票行情引擎:** 彻底抛弃低效海外接口,自主研发智能路由接入腾讯财经 (`qt.gtimg.cn`) 极速接口。引入原生 `ArrayBuffer` 与 `TextDecoder(gbk)` 彻底解决历史中文乱码问题,实现沪、深、港、美四大市场毫秒级实时同步。 +- **Crypto 行情引擎:** 接入币安 (Binance) 公共 API,构建全市场(股票+加密货币)双轨双擎驱动架构。 +- **网络防腐层:** 引入 `undici` 底层网络库并配置 `setGlobalDispatcher`,成功突破境内防火墙对境外 API 的直连封锁 (Timeout)。 + +## 前端架构与 UI/UX 体验 +- 完成 shadcn/ui 初始化,集成 next-themes 支持暗黑模式,并拉取核心组件库。 +- 完成 `/dashboard` 基础布局架构,接管根路由。 +- 打通 `/dashboard/assets` 与 `/dashboard/transactions` 页面前后端数据流转,修复早期录入与 404 缺陷。 +- 完成 UI 层高精度数据格式化,针对不同资产类型实现动态精度展示,清理因数据库 `numeric` 导致的尾随零问题。 +- 引入 `recharts` 图表引擎,构建了基于实时 CNY 估值的资产分布环形图。 +- 优化表单交互:实装了交易所与币种的智能联动逻辑,并运用 `disabled` 属性实现了表单字段的只读防腐锁定。 + +## UX 与全局交互 (UI/UX) +- 引入 `sonner` 构建全局 Toast 消息通知系统,覆盖行情同步、CRUD 操作的成功与异常提示。 +- 重构 `` 并将其提升至 Dashboard 首页,实现总资产大盘的全局一键实盘刷新。 \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 170ae52..da0839b 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -2,6 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { getPortfolioSummary } from '@/actions/portfolio'; import { formatQuantity, formatAmount } from '@/lib/formatters'; import AllocationChart from '@/components/dashboard/allocation-chart'; +import { SyncButton } from '@/components/assets/sync-button'; import Big from 'big.js'; const CHART_COLORS = [ @@ -33,6 +34,10 @@ export default async function DashboardPage() { + + 总资产概览 + +
diff --git a/app/layout.tsx b/app/layout.tsx index 88d9997..b2405ba 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "@/components/ui/sonner"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -29,6 +30,7 @@ export default function RootLayout({ {children} + ); diff --git a/package-lock.json b/package-lock.json index 3b46b02..40fbd77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "react-dom": "19.2.4", "react-hook-form": "^7.74.0", "recharts": "^3.8.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", "undici": "^8.1.0", @@ -8032,6 +8033,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 545f474..4ec5c6d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-dom": "19.2.4", "react-hook-form": "^7.74.0", "recharts": "^3.8.1", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", "undici": "^8.1.0", diff --git a/src/components/assets/add-asset-dialog.tsx b/src/components/assets/add-asset-dialog.tsx index 8de169d..94a053d 100644 --- a/src/components/assets/add-asset-dialog.tsx +++ b/src/components/assets/add-asset-dialog.tsx @@ -33,6 +33,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Plus } from 'lucide-react'; +import { toast } from 'sonner'; import { createAsset } from '@/actions/asset'; const addAssetSchema = z.object({ @@ -94,6 +95,7 @@ export function AddAssetDialog() { startTransition(async () => { const result = await createAsset(values); if (result.success) { + toast.success('资产添加成功'); setOpen(false); form.reset(); router.refresh(); diff --git a/src/components/assets/sync-button.tsx b/src/components/assets/sync-button.tsx index 3eff066..845bd6d 100644 --- a/src/components/assets/sync-button.tsx +++ b/src/components/assets/sync-button.tsx @@ -3,14 +3,20 @@ import { useState, useTransition } from 'react'; import { Button } from '@/components/ui/button'; import { RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; import { syncAllMarketPrices } from '@/actions/market'; export function SyncButton() { const [isPending, startTransition] = useTransition(); - function handleClick() { + async function handleClick() { startTransition(async () => { - await syncAllMarketPrices(); + try { + await syncAllMarketPrices(); + toast.success('行情同步成功', { description: '全市场资产价格已更新为最新市价' }); + } catch (error) { + toast.error('行情同步异常', { description: '部分接口可能触发了熔断或网络阻断' }); + } }); } diff --git a/src/components/transactions/add-transaction-dialog.tsx b/src/components/transactions/add-transaction-dialog.tsx index 7722cf5..1fc5f21 100644 --- a/src/components/transactions/add-transaction-dialog.tsx +++ b/src/components/transactions/add-transaction-dialog.tsx @@ -33,6 +33,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Plus } from 'lucide-react'; +import { toast } from 'sonner'; import { createTransaction } from '@/actions/transaction'; const addTransactionSchema = z.object({ @@ -117,6 +118,7 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) { exchangeRate: '1', }); if (result.success) { + toast.success('流水记录成功'); setOpen(false); form.reset(); router.refresh(); diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..6e6fd25 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,45 @@ +"use client" + +import { + CircleCheck, + Info, + LoaderCircle, + OctagonX, + TriangleAlert, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + , + info: , + warning: , + error: , + loading: , + }} + toastOptions={{ + classNames: { + toast: + "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", + description: "group-[.toast]:text-muted-foreground", + actionButton: + "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", + cancelButton: + "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", + }, + }} + {...props} + /> + ) +} + +export { Toaster }