stock-portfolio_byQwen3.6/app/dashboard/page.tsx

162 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
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();
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);
function DataRow({ label, value, valueClass = 'font-medium' }: { label: string; value: string; valueClass?: string }) {
return (
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">{label}</span>
<span className={valueClass}>{value}</span>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold"> Omniledger</h1>
<p className="text-muted-foreground"></p>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<SyncButton />
</CardHeader>
<CardContent className="pt-6 pb-6">
<div className="flex items-center gap-3">
<span className="text-2xl font-semibold text-muted-foreground">¥</span>
<span className="text-5xl font-bold">{formattedTotal}</span>
<span className="text-xl text-muted-foreground ml-2"> (CNY)</span>
</div>
<div className="mt-3 flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<span className={`text-lg font-semibold ${unrealizedIsPositive ? 'text-green-500' : 'text-red-500'}`}>
{unrealizedIsPositive ? '+' : ''}{formattedUnrealized}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<span className={`text-lg font-semibold ${totalPnlIsPositive ? 'text-green-500' : 'text-red-500'}`}>
{totalPnlIsPositive ? '+' : ''}{formattedTotalPnl}
</span>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{positions.length === 0 ? (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground"></p>
</CardContent>
</Card>
) : (
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);
const costCombined = `${dilutedCostStr} / ${avgCostStr}`;
const floatingPnl = formatPnl(pos.floatingPnlNative, pos.floatingPnlPercent, pos.baseCurrency);
const cumulativePnl = formatPnl(pos.cumulativePnlNative, pos.cumulativePnlPercent, pos.baseCurrency);
return (
<Card key={pos.assetId} className="flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="flex items-center justify-between">
<div className="flex flex-col">
<span className="text-base font-semibold">{pos.name || pos.symbol}</span>
<span className="text-sm font-normal text-muted-foreground">{pos.symbol}</span>
</div>
<span className="text-xs font-normal text-muted-foreground bg-muted px-2 py-0.5 rounded">
{pos.baseCurrency}
</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-2 flex-1">
<div className="space-y-3">
<DataRow label="现价" value={latestPriceFormatted} />
<DataRow label="市值" value={marketValueFormatted} />
<DataRow label="持仓" value={quantityFormatted} />
<DataRow label="摊薄 / 成本" value={costCombined} />
<DataRow
label="浮动盈亏"
value={floatingPnl.text}
valueClass={`text-sm font-semibold ${floatingPnl.className}`}
/>
<DataRow
label="累计盈亏"
value={cumulativePnl.text}
valueClass={`text-sm font-semibold ${cumulativePnl.className}`}
/>
<DataRow
label="持仓天数"
value={`${pos.holdingDays}`}
valueClass="text-sm text-muted-foreground"
/>
</div>
</CardContent>
</Card>
);
})
)}
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<AllocationChart data={marketAllocation} />
</CardContent>
</Card>
</div>
);
}