From e63f309a3a9dfffc97fa6e2ce353963da24ff0a7 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Tue, 28 Apr 2026 19:45:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E4=BE=9D=E6=8D=AE=E5=8F=82?= =?UTF-8?q?=E8=80=83=E5=9B=BE=E5=85=A8=E9=9D=A2=E9=87=8D=E6=9E=84=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E5=B1=95=E7=A4=BA=20UI=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=BA=AF=E5=87=80=E7=9A=84=E5=8E=9F=E7=94=9F=E5=B8=81=E7=A7=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 10 ++- app/dashboard/page.tsx | 162 +++++++++++++++++++---------------------- 2 files changed, 84 insertions(+), 88 deletions(-) diff --git a/Memory.md b/Memory.md index 6d09b17..435a012 100644 --- a/Memory.md +++ b/Memory.md @@ -73,4 +73,12 @@ - 新增 Native 成本均價:`avgCostNative = totalBuyCostNative / totalBuyQuantity`、`dilutedCostNative = (totalBuyCostNative - realizedPnlNative - accumulatedDividendsNative) / currentQuantity`。 - 新增浮動盈虧指標:`marketValueNative = latestPrice * currentQuantity`、`floatingPnlNative = marketValueNative - (avgCostNative * currentQuantity)`、`floatingPnlPercent = floatingPnlNative / (avgCostNative * currentQuantity) * 100`。 - 新增累計盈虧指標:`cumulativePnlNative = floatingPnlNative + realizedPnlNative + accumulatedDividendsNative`、`cumulativePnlPercent = cumulativePnlNative / totalBuyCostNative * 100`。 -- SELL 交易的已實現盈虧計算從 CNY 基準改為 Native 基準,CNY 計算保留用於前端兼容展示。 \ No newline at end of file +- SELL 交易的已實現盈虧計算從 CNY 基準改為 Native 基準,CNY 計算保留用於前端兼容展示。 + +## 全面重構資產展示 UI (Task 39) +- UI 全面升級,復刻專業券商級數據排版,合併攤薄/成本,引入原生幣種盈虧百分比展示。 +- 徹底清理所有帶 (CNY) 和 (USD) 混雜的舊布局,所有 Native 金額根據 `baseCurrency` 渲染正確貨幣符號(USD→$、CNY/HKD→HK$、JPY→¥)。 +- 資產卡片全新字段:現價、市值、持倉、攤薄/成本(合併為 `[dilutedCostNative] / [avgCostNative]` 格式)、浮動盈虧(帶百分比)、累計盈虧(帶百分比)、持倉天數。 +- 盈虧顏色遵循中國市場慣例:大於 0 顯示紅色,小於 0 顯示綠色。 +- 所有百分比保留 2 位小數,0 值正常顯示 `0.00`。 +- 卡片佈局優化為響應式 `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`。 \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 2ad68f9..f0e9317 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -5,6 +5,33 @@ import AllocationChart from '@/components/dashboard/allocation-chart'; import { SyncButton } from '@/components/assets/sync-button'; import Big from 'big.js'; +function getCurrencySymbol(currency: string): string { + switch (currency) { + case 'USD': return '$'; + case 'CNY': + case 'HKD': return 'HK$'; + case 'JPY': return '¥'; + default: return currency + ' '; + } +} + +function formatNative(value: string, baseCurrency: string): string { + const symbol = getCurrencySymbol(baseCurrency); + const formatted = new Big(value).toFixed(2); + return `${symbol}${formatted}`; +} + +function formatPnl(value: string, percent: string, baseCurrency: string): { text: string; className: string } { + const isPositive = new Big(value).gte(0); + const symbol = getCurrencySymbol(baseCurrency); + const absValue = new Big(value).abs().toFixed(2); + const absPercent = new Big(percent).abs().toFixed(2); + const sign = isPositive ? '+' : ''; + const text = `${sign}${symbol}${absValue} (${sign}${absPercent}%)`; + const className = isPositive ? 'text-red-500' : 'text-green-500'; + return { text, className }; +} + export default async function DashboardPage() { const { positions, totalCnyValue, chartData, totalPnlCny, unrealizedPnlCny, marketAllocation } = await getPortfolioSummary(); @@ -14,6 +41,15 @@ export default async function DashboardPage() { const totalPnlIsPositive = new Big(totalPnlCny).gte(0); const unrealizedIsPositive = new Big(unrealizedPnlCny).gte(0); + function DataRow({ label, value, valueClass = 'font-medium' }: { label: string; value: string; valueClass?: string }) { + return ( +
+ {label} + {value} +
+ ); + } + return (
@@ -28,15 +64,9 @@ export default async function DashboardPage() {
- - ¥ - - - {formattedTotal} - - - 总资产 (CNY) - + ¥ + {formattedTotal} + 总资产 (CNY)
@@ -55,7 +85,7 @@ export default async function DashboardPage() { -
+
{positions.length === 0 ? ( @@ -64,94 +94,52 @@ export default async function DashboardPage() { ) : ( positions.map((pos) => { - const posPnl = new Big(pos.pnlCny); - const posPnlPositive = posPnl.gte(0); - const formattedPosPnl = formatAmount(pos.pnlCny); - const formattedDividends = formatAmount(pos.accumulatedDividendsCny); + const symbol = getCurrencySymbol(pos.baseCurrency); + const latestPriceFormatted = `${symbol}${new Big(pos.latestPrice || '0').toFixed(2)}`; + const marketValueFormatted = formatNative(pos.marketValueNative, pos.baseCurrency); + const quantityFormatted = formatQuantity(pos.quantity, pos.type); - const posPnlNative = new Big(pos.pnlNative); - const posPnlNativePositive = posPnlNative.gte(0); + const dilutedCostStr = new Big(pos.dilutedCostNative).toFixed(2); + const avgCostStr = new Big(pos.avgCostNative).toFixed(2); + const costCombined = `${dilutedCostStr} / ${avgCostStr}`; - const avgCostFormatted = !new Big(pos.avgCost).eq('0') ? formatAmount(pos.avgCost) : '-'; - const dilutedCostFormatted = !new Big(pos.dilutedCost).eq(0) && pos.dilutedCost !== '0' ? formatAmount(pos.dilutedCost) : '-'; + const floatingPnl = formatPnl(pos.floatingPnlNative, pos.floatingPnlPercent, pos.baseCurrency); + const cumulativePnl = formatPnl(pos.cumulativePnlNative, pos.cumulativePnlPercent, pos.baseCurrency); return ( - - + +
- {pos.name || pos.symbol} + {pos.name || pos.symbol} {pos.symbol}
- - {pos.type} + + {pos.baseCurrency}
- -
-
- 持仓数量 - - {formatQuantity(pos.quantity, pos.type)} - -
-
- 结算币种 - {pos.baseCurrency} -
-
- 平均成本 - - ¥{avgCostFormatted} - -
-
- 摊薄成本 - - ¥{dilutedCostFormatted} - -
-
- 持仓天数 - - {pos.holdingDays} 天 - -
- {pos.baseCurrency !== 'CNY' && ( - <> -
- 持仓成本 ({pos.baseCurrency}) - - {formatAmount(pos.totalCostNative)} - -
-
- 当前盈亏 ({pos.baseCurrency}) - - {posPnlNativePositive ? '+' : ''}{formatAmount(pos.pnlNative)} - -
- - )} -
- 投入本金 (CNY) - - ¥{formatAmount(pos.totalCostCny)} - -
-
- 综合总盈亏 (CNY) - - {posPnlPositive ? '+' : ''}{formattedPosPnl} - -
-
- 累計分紅 - - {formattedDividends} - -
+ +
+ + + + + + +