stock-portfolio_byQwen3.6/app/dashboard/assets/assets-client.tsx

261 lines
8.3 KiB
TypeScript
Raw Permalink 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.

'use client';
import { useState, useTransition } from 'react';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
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 { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Edit, Trash2 } from 'lucide-react';
import Big from 'big.js';
import { updateAsset, deleteAsset } from '@/actions/asset';
import { AddAssetDialog } from '@/components/assets/add-asset-dialog';
import { SyncButton } from '@/components/assets/sync-button';
interface Asset {
id: string;
symbol: string;
name: string | null;
type: string;
exchange: string | null;
baseCurrency: string;
latestPrice: string;
createdAt: Date | null;
}
export default function AssetsPageClient({ assets }: { assets: Asset[] }) {
const [isPending, startTransition] = useTransition();
const [deleteTarget, setDeleteTarget] = useState<Asset | null>(null);
const [editTarget, setEditTarget] = useState<Asset | null>(null);
const typeLabels: Record<string, string> = {
STOCK: '股票',
CRYPTO: '加密貨幣',
CASH: '現金',
};
const editForm = useForm<{ symbol: string; name: string; baseCurrency: string }>({
resolver: zodResolver(z.object({
symbol: z.string().min(1, '资产代码不能为空'),
name: z.string(),
baseCurrency: z.string().min(2, '基础币种至少2个字符'),
})),
defaultValues: { symbol: '', name: '', baseCurrency: '' },
});
function handleEditClick(asset: Asset) {
setEditTarget(asset);
editForm.reset({ symbol: asset.symbol, name: asset.name || '', baseCurrency: asset.baseCurrency });
}
function handleEditSubmit(values: { symbol: string; name: string; baseCurrency: string }) {
if (!editTarget) return;
startTransition(async () => {
const result = await updateAsset({
id: editTarget.id,
symbol: values.symbol,
baseCurrency: values.baseCurrency,
});
if (result.success) {
setEditTarget(null);
editForm.reset();
window.location.reload();
}
});
}
function handleDelete(asset: Asset) {
startTransition(async () => {
await deleteAsset(asset.id);
setDeleteTarget(null);
window.location.reload();
});
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<div className="flex gap-2">
<SyncButton />
<AddAssetDialog />
</div>
</div>
<div className="rounded-md border">
<Table>
<TableCaption></TableCaption>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> (Latest Price)</TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{assets.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center text-muted-foreground">
"添加资产"
</TableCell>
</TableRow>
) : (
assets.map((asset) => (
<TableRow key={asset.id}>
<TableCell className="font-medium">{asset.name || '-'}</TableCell>
<TableCell>{asset.symbol}</TableCell>
<TableCell>{typeLabels[asset.type] || asset.type}</TableCell>
<TableCell>{asset.baseCurrency}</TableCell>
<TableCell>{asset.latestPrice ? new Big(asset.latestPrice).toString() : '-'}</TableCell>
<TableCell>
{asset.createdAt
? new Date(asset.createdAt).toLocaleString('zh-CN')
: '-'}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditClick(asset)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setDeleteTarget(asset)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<Dialog open={!!editTarget} onOpenChange={(open) => !open && setEditTarget(null)}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(handleEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="symbol"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="可选" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="baseCurrency"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<strong>{deleteTarget?.symbol}</strong>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setDeleteTarget(null)}
>
</Button>
<Button
variant="destructive"
onClick={() => deleteTarget && handleDelete(deleteTarget)}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}