feat(ui): 實裝資產流水下鑽明細表格及行內 CRUD 操作

This commit is contained in:
2026-04-29 00:21:16 +08:00
parent 1c6c36b147
commit 574d27968d
4 changed files with 394 additions and 60 deletions
+6
View File
@@ -41,9 +41,11 @@ interface Position {
}
interface TransactionRecord {
id: string;
txType: string;
quantity: string;
price: string;
fee: string;
txCurrency: string;
executedAt: Date | null;
}
@@ -137,9 +139,11 @@ function getTodayInShanghai(): Date {
export async function getPortfolioPositions(): Promise<Position[]> {
const allTransactions = await db
.select({
id: transactions.id,
txType: transactions.txType,
quantity: transactions.quantity,
price: transactions.price,
fee: transactions.fee,
exchangeRate: transactions.exchangeRate,
txCurrency: transactions.txCurrency,
assetId: transactions.assetId,
@@ -279,9 +283,11 @@ export async function getPortfolioPositions(): Promise<Position[]> {
}
holding.transactions.push({
id: tx.id,
txType: tx.txType,
quantity: tx.quantity,
price: tx.price,
fee: tx.fee,
txCurrency: tx.txCurrency,
executedAt: tx.executedAt,
});
@@ -0,0 +1,211 @@
'use client';
import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { updateTransaction } from '@/actions/transaction';
const updateTransactionSchema = z.object({
quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字'),
price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字'),
fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字').default('0'),
txCurrency: z.string().min(1, '交易币种不能为空'),
executedAt: z.string(),
});
type UpdateForm = z.infer<typeof updateTransactionSchema>;
interface UpdateTransactionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
transaction: {
id: string;
txType: string;
quantity: string;
price: string;
fee: string;
txCurrency: string;
executedAt: Date | null;
} | null;
onSuccess: () => void;
}
export function UpdateTransactionDialog({
open,
onOpenChange,
transaction,
onSuccess,
}: UpdateTransactionDialogProps) {
const [isPending, startTransition] = useTransition();
const form = useForm<UpdateForm>({
resolver: zodResolver(updateTransactionSchema),
defaultValues: {
quantity: '',
price: '',
fee: '0',
txCurrency: 'USD',
executedAt: '',
},
});
useState(() => {
if (transaction && open) {
form.reset({
quantity: transaction.quantity.toString(),
price: transaction.price.toString(),
fee: transaction.fee.toString(),
txCurrency: transaction.txCurrency,
executedAt: transaction.executedAt
? new Date(transaction.executedAt).toISOString().slice(0, 16)
: '',
});
}
});
function handleSubmit(values: UpdateForm) {
if (!transaction) return;
startTransition(async () => {
const result = await updateTransaction({
id: transaction.id,
quantity: values.quantity,
price: values.price,
fee: values.fee,
txCurrency: values.txCurrency,
executedAt: new Date(values.executedAt),
});
if (result.success) {
toast.success('交易记录已更新');
onOpenChange(false);
onSuccess();
} else if (result.error) {
toast.error(result.error);
}
});
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input 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 type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="txCurrency"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} value={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>
)}
/>
</div>
<FormField
control={form.control}
name="executedAt"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}