'use client'; import React, { useState, useEffect, useTransition } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; 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'; import { deleteTransaction } from '@/actions/transaction'; import { ChevronDown, ChevronUp, Plus, Edit3, Trash2 } from 'lucide-react'; import Big from 'big.js'; const txTypeMap: Record = { BUY: '买入', SELL: '卖出', DIVIDEND: '分红', AIRDROP: '空投', }; function getCurrencySymbol(currency: string): string { if (currency === 'USD') return '$'; if (currency === 'HKD') return 'HK$'; if (currency === 'CNY') return '¥'; if (currency === 'JPY') return '¥'; 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 text = `${symbol}${isPositive ? '' : '-'}${absValue} (${isPositive ? '' : '-'}${absPercent}%)`; const className = isPositive ? 'text-red-500' : 'text-green-500'; return { text, className }; } interface Asset { id: string; symbol: string; name: string | null; type: string; baseCurrency: string; exchange: string | null; } export default function DashboardPage() { const [positions, setPositions] = useState([]); const [totalCnyValue, setTotalCnyValue] = useState('0'); const [totalPnlCny, setTotalPnlCny] = useState('0'); const [unrealizedPnlCny, setUnrealizedPnlCny] = useState('0'); const [marketAllocation, setMarketAllocation] = useState([]); const [assets, setAssets] = useState([]); const [expandedIds, setExpandedIds] = useState>({}); const [dialogOpen, setDialogOpen] = useState(false); const [selectedAssetId, setSelectedAssetId] = useState(''); const [isPending, startTransition] = useTransition(); const [updateTarget, setUpdateTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [snapshots, setSnapshots] = useState([]); useEffect(() => { async function loadData() { const summary = await getPortfolioSummary(); const allAssets = await getAssets(); setPositions(summary.positions); setTotalCnyValue(summary.totalCnyValue); setTotalPnlCny(summary.totalPnlCny); setUnrealizedPnlCny(summary.unrealizedPnlCny); setMarketAllocation(summary.marketAllocation); setAssets(allAssets as Asset[]); } 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] })); }; const handleOpenDialog = (assetId: string) => { setSelectedAssetId(assetId); setDialogOpen(true); }; const handleUpdate = (tx: any) => { setUpdateTarget(tx); }; function handleUpdateSubmit() { setUpdateTarget(null); } function handleDelete(tx: any) { startTransition(async () => { const result = await deleteTransaction(tx.id); if (result.success) { toast.success('交易記錄已刪除'); setDeleteTarget(null); window.location.reload(); } else if (result.error) { toast.error(result.error); } }); } const formattedTotal = formatAmount(totalCnyValue); const formattedTotalPnl = formatAmount(totalPnlCny); const formattedUnrealized = formatAmount(unrealizedPnlCny); const totalPnlIsPositive = new Big(totalPnlCny).gte(0); const unrealizedIsPositive = new Big(unrealizedPnlCny).gte(0); return (

欢迎来到 Omniledger

您的跨界记账中枢。

总资产概览
¥ {formattedTotal} 总资产 (CNY)
持仓盈亏: {formattedUnrealized}
总盈亏: {formattedTotalPnl}
净值走势 持仓明细 {positions.length === 0 ? (
暂无持仓,请先添加资产和交易记录。
) : ( 名称/代码 现價 市值 持倉 攤薄/成本 浮動盈虧 累計盈虧 操作 {positions.map((pos) => { 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 dilutedCostStr = new Big(pos.dilutedCostNative).toFixed(2); const avgCostStr = new Big(pos.avgCostNative).toFixed(2); let costDisplay = '-'; if (!new Big(pos.dilutedCostNative).eq('0') || !new Big(pos.avgCostNative).eq('0')) { costDisplay = `${dilutedCostStr} / ${avgCostStr}`; } const floatingPnl = formatPnl(pos.floatingPnlNative, pos.floatingPnlPercent, pos.baseCurrency); const cumulativePnl = formatPnl(pos.cumulativePnlNative, pos.cumulativePnlPercent, pos.baseCurrency); const isExpanded = !!expandedIds[pos.assetId]; return ( toggleExpand(pos.assetId)}>
{pos.name || pos.symbol} {pos.symbol}
{latestPriceFormatted} {marketValueFormatted} {quantityFormatted} {costDisplay} {floatingPnl.text} {cumulativePnl.text} e.stopPropagation()}>
{isExpanded ? '收起' : '展开'} {isExpanded ? ( ) : ( )}
{isExpanded && (
流水明細
{pos.transactions && pos.transactions.length > 0 ? (
交易日期 類型 價格/數量 手續費 備註 操作 {pos.transactions.map((tx: any) => { const txDate = tx.executedAt ? new Date(tx.executedAt).toLocaleString('zh-TW', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }) : '-'; return ( {txDate} {txTypeMap[tx.txType] || tx.txType}
量: {new Big(tx.quantity).toString()} 價: {new Big(tx.price).toString()}
{new Big(tx.fee || 0).toString()} -
); })}
) : (

暫無交易記錄

)}
)} ); })} )} 資產分布 !open && setUpdateTarget(null)} assets={assets} transaction={updateTarget} onSuccess={handleUpdateSubmit} /> !open && setDeleteTarget(null)}> 確認刪除 確定要刪除這筆交易記錄嗎?此操作不可撤銷。 ); }