fix(ui): 修复价格尾零、增加币种下拉框,并重构持仓引擎实现原币种/本位币双轨制盈亏计算
This commit is contained in:
parent
746be06840
commit
67ceb63b08
@ -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>
|
||||
))
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user