fix(ui): 修复价格尾零、增加币种下拉框,并重构持仓引擎实现原币种/本位币双轨制盈亏计算

This commit is contained in:
kennethcheng 2026-04-28 01:40:29 +08:00
parent 746be06840
commit 67ceb63b08
4 changed files with 48 additions and 12 deletions

View File

@ -10,6 +10,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import Big from 'big.js';
export default async function AssetsPage() {
const assets = await getAssets();
@ -53,14 +54,14 @@ export default async function AssetsPage() {
<TableCell className="font-medium">{asset.symbol}</TableCell>
<TableCell>{typeLabels[asset.type] || asset.type}</TableCell>
<TableCell>{asset.baseCurrency}</TableCell>
<TableCell>{asset.latestPrice}</TableCell>
<TableCell>{asset.latestPrice ? new Big(asset.latestPrice).toString() : '-'}</TableCell>
<TableCell>
{asset.createdAt
? new Date(asset.createdAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
<TableCell>
<UpdatePriceDialog assetId={asset.id} currentPrice={asset.latestPrice || '0'} />
<UpdatePriceDialog assetId={asset.id} currentPrice={asset.latestPrice ? new Big(asset.latestPrice).toString() : '0'} />
</TableCell>
</TableRow>
))

View File

@ -90,21 +90,33 @@ export default async function DashboardPage() {
<span className="font-medium">{pos.baseCurrency}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">CNY </span>
<span className="font-medium text-green-600">
¥{formatAmount(pos.cnyValue)}
<span className="text-muted-foreground"> ({pos.baseCurrency})</span>
<span className="font-medium">
{formatAmount(pos.totalCostNative)}
</span>
</div>
<div className="flex justify-between">
{(() => {
const posPnlNative = new Big(pos.pnlNative);
const posPnlNativePositive = posPnlNative.gte(0);
return (
<div className="flex justify-between">
<span className="text-muted-foreground"> ({pos.baseCurrency})</span>
<span className={`font-semibold ${posPnlNativePositive ? 'text-green-500' : 'text-red-500'}`}>
{posPnlNativePositive ? '+' : ''}{formatAmount(pos.pnlNative)}
</span>
</div>
);
})()}
<div className="flex justify-between opacity-50">
<span className="text-muted-foreground"> (CNY)</span>
<span className="font-medium">
¥{formatAmount(pos.totalCostCny)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground"></span>
<div className="flex justify-between opacity-50">
<span className="text-muted-foreground"> (CNY)</span>
<span className={`font-semibold ${posPnlPositive ? 'text-green-500' : 'text-red-500'}`}>
{posPnlPositive ? '+' : ''}{formattedPosPnl}
{posPnlPositive ? '+' : ''}{formatAmount(pos.pnlCny)}
</span>
</div>
</div>

View File

@ -14,6 +14,8 @@ interface Position {
cnyValue: string;
totalCostCny: string;
pnlCny: string;
totalCostNative: string;
pnlNative: string;
}
interface RawRate {
@ -95,6 +97,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
baseCurrency: string;
latestPrice: string;
totalCostCny: Big;
totalCostNative: Big;
}>();
for (const tx of allTransactions) {
@ -110,6 +113,7 @@ export async function getPortfolioPositions(): Promise<Position[]> {
baseCurrency: tx.assetBaseCurrency || '',
latestPrice: tx.assetLatestPrice || '0',
totalCostCny: new Big('0'),
totalCostNative: new Big('0'),
});
}
@ -118,10 +122,15 @@ export async function getPortfolioPositions(): Promise<Position[]> {
if (tx.txType === 'BUY') {
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
const costPerUnit = new Big(tx.quantity).times(new Big(tx.price));
holding.totalCostNative = holding.totalCostNative.plus(costPerUnit);
const costCny = costPerUnit.times(new Big(tx.exchangeRate || '1'));
holding.totalCostCny = holding.totalCostCny.plus(costCny);
} else if (tx.txType === 'SELL') {
holding.quantity = holding.quantity.minus(new Big(tx.quantity));
const sellCostPerUnit = new Big(tx.quantity).times(new Big(tx.price));
holding.totalCostNative = holding.totalCostNative.minus(sellCostPerUnit);
const sellCostCny = sellCostPerUnit.times(new Big(tx.exchangeRate || '1'));
holding.totalCostCny = holding.totalCostCny.minus(sellCostCny);
} else if (tx.txType === 'AIRDROP') {
holding.quantity = holding.quantity.plus(new Big(tx.quantity));
} else if (tx.txType === 'DIVIDEND') {
@ -160,6 +169,9 @@ export async function getPortfolioPositions(): Promise<Position[]> {
const pnlCny = cnyValue.minus(holding.totalCostCny);
totalPnlCny = totalPnlCny.plus(pnlCny);
const currentNativeValue = new Big(holding.latestPrice).times(holding.quantity);
const pnlNative = currentNativeValue.minus(holding.totalCostNative);
result.push({
assetId: holding.assetId,
symbol: holding.symbol,
@ -169,6 +181,8 @@ export async function getPortfolioPositions(): Promise<Position[]> {
cnyValue: cnyValue.toString(),
totalCostCny: holding.totalCostCny.toString(),
pnlCny: pnlCny.toString(),
totalCostNative: holding.totalCostNative.toString(),
pnlNative: pnlNative.toString(),
});
}

View File

@ -218,9 +218,18 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="USD" type="text" {...field} />
</FormControl>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择币种" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="USD">USD</SelectItem>
<SelectItem value="CNY">CNY</SelectItem>
<SelectItem value="HKD">HKD</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}