feat(ui): 修复流水页 404,实现高精度交易录入表单与列表展示

This commit is contained in:
kennethcheng 2026-04-27 22:03:31 +08:00
parent 19e23dc933
commit 7dbfd24cb1
2 changed files with 351 additions and 0 deletions

View File

@ -0,0 +1,88 @@
import { getAssets } from '@/actions/asset';
import { getTransactions } from '@/actions/transaction';
import { AddTransactionDialog } from '@/components/transactions/add-transaction-dialog';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
export default async function TransactionsPage() {
const [assets, transactions] = await Promise.all([
getAssets(),
getTransactions(),
]);
const typeLabels: Record<string, string> = {
BUY: '买入',
SELL: '卖出',
DIVIDEND: '分红',
AIRDROP: '空投',
FEE: '手续费',
};
const assetMap = new Map(assets.map((a) => [a.id, a.symbol]));
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<AddTransactionDialog assets={assets} />
</div>
<div className="rounded-md border">
<Table>
<TableCaption>
{transactions.length}
</TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.length === 0 ? (
<TableRow>
<TableCell
colSpan={7}
className="text-center text-muted-foreground"
>
"添加流水"
</TableCell>
</TableRow>
) : (
transactions.map((tx) => {
const symbol = assetMap.get(tx.assetId) || tx.assetId;
return (
<TableRow key={tx.id}>
<TableCell className="font-medium">{symbol}</TableCell>
<TableCell>{typeLabels[tx.txType] || tx.txType}</TableCell>
<TableCell>{tx.quantity}</TableCell>
<TableCell>{tx.price}</TableCell>
<TableCell>{tx.fee}</TableCell>
<TableCell>{tx.txCurrency}</TableCell>
<TableCell>
{tx.executedAt
? new Date(tx.executedAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@ -0,0 +1,263 @@
'use client';
import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Plus } from 'lucide-react';
import { createTransaction } from '@/actions/transaction';
const addTransactionSchema = z.object({
assetId: z.string().uuid('请选择一个资产'),
txType: z.enum(['BUY', 'SELL', 'DIVIDEND', 'AIRDROP', 'FEE']),
quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字字符串'),
price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字字符串'),
fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字字符串'),
txCurrency: z.string().min(1, '交易币种不能为空'),
executedAt: z.date(),
});
type AddTransactionForm = z.infer<typeof addTransactionSchema>;
interface Asset {
id: string;
symbol: string;
type: string;
baseCurrency: string;
}
interface AddTransactionDialogProps {
assets: Asset[];
}
const txTypeLabels: Record<string, string> = {
BUY: '买入 (BUY)',
SELL: '卖出 (SELL)',
DIVIDEND: '分红 (DIVIDEND)',
AIRDROP: '空投 (AIRDROP)',
FEE: '手续费 (FEE)',
};
export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const router = useRouter();
const form = useForm<AddTransactionForm>({
resolver: zodResolver(addTransactionSchema),
defaultValues: {
assetId: '',
txType: 'BUY' as const,
quantity: '',
price: '',
fee: '0',
txCurrency: 'USD',
executedAt: new Date(),
},
});
function onSubmit(values: AddTransactionForm) {
startTransition(async () => {
const result = await createTransaction({
assetId: values.assetId,
txType: values.txType,
quantity: String(values.quantity),
price: String(values.price),
fee: String(values.fee),
txCurrency: values.txCurrency,
executedAt: values.executedAt,
exchangeRate: '1',
});
if (result.success) {
setOpen(false);
form.reset();
router.refresh();
}
});
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="assetId"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择资产" />
</SelectTrigger>
</FormControl>
<SelectContent>
{assets.length === 0 ? (
<SelectItem value="__none__"></SelectItem>
) : (
assets.map((asset) => (
<SelectItem key={asset.id} value={asset.id}>
{asset.symbol}
</SelectItem>
))
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="txType"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择交易类型" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(txTypeLabels).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="0.001234567890123456" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="0.00" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="fee"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="0" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="txCurrency"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="USD" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="executedAt"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
type="datetime-local"
{...field}
onChange={(e) => {
field.onChange(new Date(e.target.value));
}}
value={
field.value
? new Date(field.value).toISOString().slice(0, 16)
: ''
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '确认添加'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}