756 lines
29 KiB
TypeScript
756 lines
29 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useSession } from "next-auth/react"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||
import { Label } from "@/components/ui/label"
|
||
import { CalendarIcon, PlusCircle, Trash2, LogIn, Edit, Check, X } from "lucide-react"
|
||
import { Calendar } from "@/components/ui/calendar"
|
||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
import { cn } from "@/lib/utils"
|
||
import { format } from "date-fns"
|
||
import { zhCN } from "date-fns/locale"
|
||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||
import { toast } from "sonner"
|
||
|
||
interface FinanceProduct {
|
||
id: string
|
||
principal: number
|
||
depositDate: Date | null
|
||
endDate: Date | null
|
||
currentNetValue: number | null
|
||
annualRate: number | null
|
||
profit: number | null
|
||
dailyProfit: number | null
|
||
monthlyProfit: number | null
|
||
calculatedAnnualRate: number | null
|
||
averageAnnualProfit: number | null
|
||
currency: string
|
||
notes: string | null
|
||
createdAt?: Date
|
||
updatedAt?: Date
|
||
}
|
||
|
||
interface NewProduct {
|
||
principal: number
|
||
depositDate: Date | null
|
||
endDate: Date | null
|
||
currentNetValue: number | null
|
||
annualRate: number | null
|
||
currency: string
|
||
notes: string | null
|
||
}
|
||
|
||
export default function FinanceCalculator() {
|
||
const { data: session, status } = useSession()
|
||
const [products, setProducts] = useState<FinanceProduct[]>([])
|
||
const [newProduct, setNewProduct] = useState<NewProduct>(createEmptyProduct())
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
|
||
// 编辑相关状态
|
||
const [editingProductId, setEditingProductId] = useState<string | null>(null)
|
||
const [editingProduct, setEditingProduct] = useState<NewProduct>(createEmptyProduct())
|
||
const [isUpdating, setIsUpdating] = useState(false)
|
||
|
||
function createEmptyProduct(): NewProduct {
|
||
const lastProduct = products.length > 0 ? products[products.length - 1] : null
|
||
return {
|
||
principal: lastProduct ? lastProduct.principal : 0,
|
||
depositDate: null,
|
||
endDate: null,
|
||
currentNetValue: null,
|
||
annualRate: null,
|
||
currency: lastProduct ? lastProduct.currency : "RMB",
|
||
notes: null,
|
||
}
|
||
}
|
||
|
||
// 从数据库加载产品
|
||
async function loadProducts() {
|
||
if (!session?.user?.id) return
|
||
|
||
setIsLoading(true)
|
||
try {
|
||
const response = await fetch('/api/products')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
// 转换日期字符串为Date对象
|
||
const productsWithDates = data.map((product: any) => ({
|
||
...product,
|
||
depositDate: product.depositDate ? new Date(product.depositDate) : null,
|
||
endDate: product.endDate ? new Date(product.endDate) : null,
|
||
createdAt: product.createdAt ? new Date(product.createdAt) : undefined,
|
||
updatedAt: product.updatedAt ? new Date(product.updatedAt) : undefined,
|
||
}))
|
||
setProducts(productsWithDates)
|
||
} else {
|
||
toast.error('加载产品数据失败')
|
||
}
|
||
} catch (error) {
|
||
toast.error('加载产品数据失败')
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
// 当用户登录状态改变时加载产品
|
||
useEffect(() => {
|
||
if (status === 'authenticated') {
|
||
loadProducts()
|
||
} else if (status === 'unauthenticated') {
|
||
setProducts([])
|
||
}
|
||
}, [status, session?.user?.id])
|
||
|
||
async function handleAddProduct() {
|
||
if (!session?.user?.id) {
|
||
toast.error('请先登录')
|
||
return
|
||
}
|
||
|
||
if (newProduct.principal <= 0) {
|
||
setError("本金必须大于0")
|
||
return
|
||
}
|
||
|
||
// 更新验证逻辑:如果有完整的时间和净值信息,则不需要年化利率
|
||
const hasCompleteInfo = newProduct.depositDate && newProduct.endDate && newProduct.principal && newProduct.currentNetValue
|
||
const hasEitherValue = newProduct.currentNetValue !== null || newProduct.annualRate !== null
|
||
|
||
if (!hasCompleteInfo && !hasEitherValue) {
|
||
setError("请提供足够的信息:要么提供当前净值或年化利率,要么提供完整的时间和净值信息以自动计算年化利率")
|
||
return
|
||
}
|
||
|
||
setIsSubmitting(true)
|
||
setError(null)
|
||
|
||
try {
|
||
const requestData = {
|
||
principal: newProduct.principal,
|
||
depositDate: newProduct.depositDate?.toISOString() || null,
|
||
endDate: newProduct.endDate?.toISOString() || null,
|
||
currentNetValue: newProduct.currentNetValue,
|
||
annualRate: newProduct.annualRate,
|
||
currency: newProduct.currency,
|
||
notes: newProduct.notes,
|
||
}
|
||
|
||
console.log('Sending data to API:', requestData)
|
||
|
||
const response = await fetch('/api/products', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(requestData),
|
||
})
|
||
|
||
const responseData = await response.json()
|
||
console.log('API response:', responseData)
|
||
|
||
if (response.ok) {
|
||
// 转换日期字符串为Date对象
|
||
const productWithDates = {
|
||
...responseData,
|
||
depositDate: responseData.depositDate ? new Date(responseData.depositDate) : null,
|
||
endDate: responseData.endDate ? new Date(responseData.endDate) : null,
|
||
createdAt: responseData.createdAt ? new Date(responseData.createdAt) : undefined,
|
||
updatedAt: responseData.updatedAt ? new Date(responseData.updatedAt) : undefined,
|
||
}
|
||
setProducts([productWithDates, ...products])
|
||
setNewProduct(createEmptyProduct())
|
||
toast.success('产品添加成功')
|
||
} else {
|
||
console.error('API error response:', responseData)
|
||
|
||
if (responseData.details && Array.isArray(responseData.details)) {
|
||
// 显示详细的验证错误
|
||
const errorMessages = responseData.details.map((detail: any) =>
|
||
`${detail.field}: ${detail.message}`
|
||
).join(', ')
|
||
setError(`数据验证失败: ${errorMessages}`)
|
||
} else {
|
||
setError(responseData.error || '添加产品失败')
|
||
}
|
||
toast.error('添加产品失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Request error:', error)
|
||
setError('网络请求失败,请重试')
|
||
toast.error('网络请求失败,请重试')
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
// 开始编辑产品
|
||
function handleEditProduct(product: FinanceProduct) {
|
||
setEditingProductId(product.id)
|
||
setEditingProduct({
|
||
principal: product.principal,
|
||
depositDate: product.depositDate,
|
||
endDate: product.endDate,
|
||
currentNetValue: product.currentNetValue,
|
||
annualRate: product.annualRate,
|
||
currency: product.currency,
|
||
notes: product.notes,
|
||
})
|
||
}
|
||
|
||
// 取消编辑
|
||
function handleCancelEdit() {
|
||
setEditingProductId(null)
|
||
setEditingProduct(createEmptyProduct())
|
||
}
|
||
|
||
// 保存编辑
|
||
async function handleSaveEdit() {
|
||
if (!session?.user?.id || !editingProductId) {
|
||
toast.error('请先登录')
|
||
return
|
||
}
|
||
|
||
if (editingProduct.principal <= 0) {
|
||
toast.error("本金必须大于0")
|
||
return
|
||
}
|
||
|
||
setIsUpdating(true)
|
||
|
||
try {
|
||
const requestData = {
|
||
principal: editingProduct.principal,
|
||
depositDate: editingProduct.depositDate?.toISOString() || null,
|
||
endDate: editingProduct.endDate?.toISOString() || null,
|
||
currentNetValue: editingProduct.currentNetValue,
|
||
annualRate: editingProduct.annualRate,
|
||
currency: editingProduct.currency,
|
||
notes: editingProduct.notes,
|
||
}
|
||
|
||
console.log('Updating product with data:', requestData)
|
||
|
||
const response = await fetch(`/api/products/${editingProductId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(requestData),
|
||
})
|
||
|
||
const responseData = await response.json()
|
||
console.log('Update API response:', responseData)
|
||
|
||
if (response.ok) {
|
||
// 转换日期字符串为Date对象
|
||
const updatedProductWithDates = {
|
||
...responseData,
|
||
depositDate: responseData.depositDate ? new Date(responseData.depositDate) : null,
|
||
endDate: responseData.endDate ? new Date(responseData.endDate) : null,
|
||
createdAt: responseData.createdAt ? new Date(responseData.createdAt) : undefined,
|
||
updatedAt: responseData.updatedAt ? new Date(responseData.updatedAt) : undefined,
|
||
}
|
||
|
||
// 更新产品列表
|
||
setProducts(products.map(p =>
|
||
p.id === editingProductId ? updatedProductWithDates : p
|
||
))
|
||
|
||
// 清除编辑状态
|
||
setEditingProductId(null)
|
||
setEditingProduct(createEmptyProduct())
|
||
|
||
toast.success('产品更新成功')
|
||
} else {
|
||
console.error('Update API error response:', responseData)
|
||
|
||
if (responseData.details && Array.isArray(responseData.details)) {
|
||
// 显示详细的验证错误
|
||
const errorMessages = responseData.details.map((detail: any) =>
|
||
`${detail.field}: ${detail.message}`
|
||
).join(', ')
|
||
toast.error(`数据验证失败: ${errorMessages}`)
|
||
} else {
|
||
toast.error(responseData.error || '更新产品失败')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Update request error:', error)
|
||
toast.error('网络请求失败,请重试')
|
||
} finally {
|
||
setIsUpdating(false)
|
||
}
|
||
}
|
||
|
||
async function handleRemoveProduct(id: string) {
|
||
if (!session?.user?.id) {
|
||
toast.error('请先登录')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/products/${id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (response.ok) {
|
||
setProducts(products.filter((product) => product.id !== id))
|
||
toast.success('产品删除成功')
|
||
} else {
|
||
toast.error('删除产品失败')
|
||
}
|
||
} catch (error) {
|
||
toast.error('删除产品失败')
|
||
}
|
||
}
|
||
|
||
function handleUseCurrentTime() {
|
||
setNewProduct({
|
||
...newProduct,
|
||
endDate: new Date(),
|
||
})
|
||
}
|
||
|
||
function handleUseCurrentTimeForEdit() {
|
||
setEditingProduct({
|
||
...editingProduct,
|
||
endDate: new Date(),
|
||
})
|
||
}
|
||
|
||
// 当产品列表变化时,更新新产品的默认本金
|
||
useEffect(() => {
|
||
if (products.length > 0) {
|
||
setNewProduct((prev) => ({
|
||
...prev,
|
||
principal: products[0].principal,
|
||
}))
|
||
}
|
||
}, [products.length])
|
||
|
||
// 如果用户未登录,显示登录提示
|
||
if (status === 'unauthenticated') {
|
||
return (
|
||
<div className="space-y-8">
|
||
<Card>
|
||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||
<LogIn className="h-12 w-12 text-muted-foreground mb-4" />
|
||
<h3 className="text-lg font-semibold mb-2">需要登录</h3>
|
||
<p className="text-muted-foreground text-center mb-4">
|
||
请登录以使用理财计算器功能,您的数据将安全保存在云端
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 加载中状态
|
||
if (status === 'loading' || isLoading) {
|
||
return (
|
||
<div className="space-y-8">
|
||
<Card>
|
||
<CardContent className="py-12">
|
||
<div className="flex items-center justify-center">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||
<span className="ml-2">加载中...</span>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-8">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>添加新理财产品</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="principal">本金</Label>
|
||
<Input
|
||
id="principal"
|
||
type="number"
|
||
value={newProduct.principal || ""}
|
||
onChange={(e) => setNewProduct({ ...newProduct, principal: Number.parseFloat(e.target.value) || 0 })}
|
||
placeholder="输入本金"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="depositDate">存入时间</Label>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
className={cn(
|
||
"w-full justify-start text-left font-normal",
|
||
!newProduct.depositDate && "text-muted-foreground",
|
||
)}
|
||
>
|
||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||
{newProduct.depositDate ? format(newProduct.depositDate, "PPP", { locale: zhCN }) : "选择日期"}
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0">
|
||
<Calendar
|
||
mode="single"
|
||
selected={newProduct.depositDate || undefined}
|
||
onSelect={(date) => setNewProduct({ ...newProduct, depositDate: date || null })}
|
||
initialFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="endDate">截止时间</Label>
|
||
<div className="flex gap-2">
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
className={cn(
|
||
"w-full justify-start text-left font-normal",
|
||
!newProduct.endDate && "text-muted-foreground",
|
||
)}
|
||
>
|
||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||
{newProduct.endDate ? format(newProduct.endDate, "PPP", { locale: zhCN }) : "选择日期"}
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0">
|
||
<Calendar
|
||
mode="single"
|
||
selected={newProduct.endDate || undefined}
|
||
onSelect={(date) => setNewProduct({ ...newProduct, endDate: date || null })}
|
||
initialFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
<Button variant="secondary" onClick={handleUseCurrentTime}>
|
||
现在
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="currentNetValue">当前净值</Label>
|
||
<Input
|
||
id="currentNetValue"
|
||
type="number"
|
||
value={newProduct.currentNetValue !== null ? newProduct.currentNetValue : ""}
|
||
onChange={(e) =>
|
||
setNewProduct({
|
||
...newProduct,
|
||
currentNetValue: e.target.value ? Number.parseFloat(e.target.value) : null,
|
||
})
|
||
}
|
||
placeholder="输入当前净值"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="annualRate">年化利率 (%)</Label>
|
||
<Input
|
||
id="annualRate"
|
||
type="number"
|
||
value={newProduct.annualRate !== null ? newProduct.annualRate : ""}
|
||
onChange={(e) =>
|
||
setNewProduct({
|
||
...newProduct,
|
||
annualRate: e.target.value ? Number.parseFloat(e.target.value) : null,
|
||
})
|
||
}
|
||
placeholder="输入年化利率(可自动计算)"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="currency">货币</Label>
|
||
<Select value={newProduct.currency} onValueChange={(value) => setNewProduct({ ...newProduct, currency: value })}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择货币" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="RMB">人民币 (RMB)</SelectItem>
|
||
<SelectItem value="USD">美元 (USD)</SelectItem>
|
||
<SelectItem value="EUR">欧元 (EUR)</SelectItem>
|
||
<SelectItem value="JPY">日元 (JPY)</SelectItem>
|
||
<SelectItem value="GBP">英镑 (GBP)</SelectItem>
|
||
<SelectItem value="HKD">港币 (HKD)</SelectItem>
|
||
<SelectItem value="SGD">新加坡元 (SGD)</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="notes">备注</Label>
|
||
<Input
|
||
id="notes"
|
||
type="text"
|
||
value={newProduct.notes || ""}
|
||
onChange={(e) => setNewProduct({ ...newProduct, notes: e.target.value || null })}
|
||
placeholder="输入备注"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-end">
|
||
<Button onClick={handleAddProduct} className="w-full" disabled={isSubmitting}>
|
||
<PlusCircle className="mr-2 h-4 w-4" />
|
||
{isSubmitting ? "添加中..." : "添加产品"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<Alert variant="destructive" className="mt-4">
|
||
<AlertDescription>{error}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>理财产品对比</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="overflow-x-auto">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>本金</TableHead>
|
||
<TableHead>存入时间</TableHead>
|
||
<TableHead>截止时间</TableHead>
|
||
<TableHead>当前净值</TableHead>
|
||
<TableHead>年化利率 (%)</TableHead>
|
||
<TableHead>货币</TableHead>
|
||
<TableHead>备注</TableHead>
|
||
<TableHead>收益</TableHead>
|
||
<TableHead>平均日收益</TableHead>
|
||
<TableHead>平均月收益</TableHead>
|
||
<TableHead>平均年收益</TableHead>
|
||
<TableHead className="w-[120px]">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{products.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={12} className="text-center">
|
||
暂无数据
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
products.map((product) => (
|
||
<TableRow key={product.id}>
|
||
{editingProductId === product.id ? (
|
||
// 编辑模式的行
|
||
<>
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={editingProduct.principal || ""}
|
||
onChange={(e) => setEditingProduct({
|
||
...editingProduct,
|
||
principal: Number.parseFloat(e.target.value) || 0
|
||
})}
|
||
className="w-20"
|
||
/>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button variant="outline" size="sm">
|
||
<CalendarIcon className="h-4 w-4" />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0">
|
||
<Calendar
|
||
mode="single"
|
||
selected={editingProduct.depositDate || undefined}
|
||
onSelect={(date) => setEditingProduct({
|
||
...editingProduct,
|
||
depositDate: date || null
|
||
})}
|
||
initialFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex gap-1">
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button variant="outline" size="sm">
|
||
<CalendarIcon className="h-4 w-4" />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-auto p-0">
|
||
<Calendar
|
||
mode="single"
|
||
selected={editingProduct.endDate || undefined}
|
||
onSelect={(date) => setEditingProduct({
|
||
...editingProduct,
|
||
endDate: date || null
|
||
})}
|
||
initialFocus
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={handleUseCurrentTimeForEdit}
|
||
>
|
||
现在
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={editingProduct.currentNetValue !== null ? editingProduct.currentNetValue : ""}
|
||
onChange={(e) => setEditingProduct({
|
||
...editingProduct,
|
||
currentNetValue: e.target.value ? Number.parseFloat(e.target.value) : null
|
||
})}
|
||
className="w-24"
|
||
/>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={editingProduct.annualRate !== null ? editingProduct.annualRate : ""}
|
||
onChange={(e) => setEditingProduct({
|
||
...editingProduct,
|
||
annualRate: e.target.value ? Number.parseFloat(e.target.value) : null
|
||
})}
|
||
className="w-20"
|
||
placeholder="自动计算"
|
||
/>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Select value={editingProduct.currency} onValueChange={(value) => setEditingProduct({ ...editingProduct, currency: value })}>
|
||
<SelectTrigger className="w-24">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="RMB">RMB</SelectItem>
|
||
<SelectItem value="USD">USD</SelectItem>
|
||
<SelectItem value="EUR">EUR</SelectItem>
|
||
<SelectItem value="JPY">JPY</SelectItem>
|
||
<SelectItem value="GBP">GBP</SelectItem>
|
||
<SelectItem value="HKD">HKD</SelectItem>
|
||
<SelectItem value="SGD">SGD</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Input
|
||
type="text"
|
||
value={editingProduct.notes || ""}
|
||
onChange={(e) => setEditingProduct({
|
||
...editingProduct,
|
||
notes: e.target.value || null
|
||
})}
|
||
className="w-32"
|
||
placeholder="备注"
|
||
/>
|
||
</TableCell>
|
||
<TableCell>-</TableCell>
|
||
<TableCell>-</TableCell>
|
||
<TableCell>-</TableCell>
|
||
<TableCell>
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={handleSaveEdit}
|
||
disabled={isUpdating}
|
||
>
|
||
<Check className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={handleCancelEdit}
|
||
disabled={isUpdating}
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</>
|
||
) : (
|
||
// 普通显示模式的行
|
||
<>
|
||
<TableCell>{product.principal.toFixed(2)}</TableCell>
|
||
<TableCell>
|
||
{product.depositDate ? format(product.depositDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
|
||
</TableCell>
|
||
<TableCell>
|
||
{product.endDate ? format(product.endDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
|
||
</TableCell>
|
||
<TableCell>
|
||
{product.currentNetValue !== null ? product.currentNetValue.toFixed(2) : "-"}
|
||
</TableCell>
|
||
<TableCell>{product.annualRate !== null ? product.annualRate.toFixed(2) : "-"}</TableCell>
|
||
<TableCell>{product.currency}</TableCell>
|
||
<TableCell>{product.notes}</TableCell>
|
||
<TableCell
|
||
className={cn(
|
||
product.profit && product.profit > 0
|
||
? "text-green-600"
|
||
: product.profit && product.profit < 0
|
||
? "text-red-600"
|
||
: "",
|
||
)}
|
||
>
|
||
{product.profit !== null ? product.profit.toFixed(2) : "-"}
|
||
</TableCell>
|
||
<TableCell>{product.dailyProfit !== null ? product.dailyProfit.toFixed(2) : "-"}</TableCell>
|
||
<TableCell>{product.monthlyProfit !== null ? product.monthlyProfit.toFixed(2) : "-"}</TableCell>
|
||
<TableCell>{product.averageAnnualProfit !== null ? product.averageAnnualProfit.toFixed(2) : "-"}</TableCell>
|
||
<TableCell>
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleEditProduct(product)}
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleRemoveProduct(product.id)}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</>
|
||
)}
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|