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 ( +