261 lines
8.3 KiB
TypeScript
261 lines
8.3 KiB
TypeScript
'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>
|
||
);
|
||
}
|