778 lines
30 KiB
TypeScript
778 lines
30 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, 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"
|
|
import AdBanner from "@/components/AdBanner"
|
|
import { useLocalStorage, LocalFinanceProduct } from "@/hooks/use-local-storage"
|
|
import { useDataSync } from "@/hooks/use-data-sync"
|
|
import { DataSyncDialog } from "@/components/data-sync-dialog"
|
|
|
|
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 [serverProducts, setServerProducts] = useState<FinanceProduct[]>([])
|
|
const [isServerLoading, setIsServerLoading] = useState(false)
|
|
|
|
// 本地数据hooks
|
|
const {
|
|
products: localProducts,
|
|
isLoading: isLocalLoading,
|
|
addProduct: addLocalProduct,
|
|
updateProduct: updateLocalProduct,
|
|
removeProduct: removeLocalProduct,
|
|
hasLocalData,
|
|
getAllLocalData
|
|
} = useLocalStorage()
|
|
|
|
// 数据同步hooks
|
|
const { isSyncing, syncLocalToServer, fetchServerData } = useDataSync()
|
|
|
|
// 同步对话框状态
|
|
const [showSyncDialog, setShowSyncDialog] = useState(false)
|
|
const [hasCheckedSync, setHasCheckedSync] = useState(false)
|
|
|
|
// 获取当前使用的产品列表
|
|
const currentProducts = status === 'authenticated' ? serverProducts : localProducts
|
|
|
|
// 创建空产品的函数
|
|
function createEmptyProduct(products?: FinanceProduct[]): NewProduct {
|
|
const productList = products || currentProducts
|
|
const lastProduct = productList.length > 0 ? productList[productList.length - 1] : null
|
|
return {
|
|
principal: lastProduct ? lastProduct.principal : 0,
|
|
depositDate: null,
|
|
endDate: null,
|
|
currentNetValue: null,
|
|
annualRate: null,
|
|
currency: lastProduct ? lastProduct.currency : "RMB",
|
|
notes: null,
|
|
}
|
|
}
|
|
|
|
// 表单状态
|
|
const [newProduct, setNewProduct] = useState<NewProduct>(() => createEmptyProduct([]))
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
|
|
// 编辑相关状态
|
|
const [editingProductId, setEditingProductId] = useState<string | null>(null)
|
|
const [editingProduct, setEditingProduct] = useState<NewProduct>(() => createEmptyProduct([]))
|
|
const [isUpdating, setIsUpdating] = useState(false)
|
|
|
|
// 从服务器加载产品
|
|
async function loadServerProducts() {
|
|
if (!session?.user?.id) return
|
|
|
|
setIsServerLoading(true)
|
|
try {
|
|
const data = await fetchServerData()
|
|
setServerProducts(data)
|
|
} catch (error) {
|
|
console.error('Failed to load server products:', error)
|
|
} finally {
|
|
setIsServerLoading(false)
|
|
}
|
|
}
|
|
|
|
// 当用户登录状态改变时处理数据加载和同步
|
|
useEffect(() => {
|
|
if (status === 'authenticated') {
|
|
loadServerProducts()
|
|
|
|
// 检查是否有本地数据需要同步
|
|
if (!hasCheckedSync && hasLocalData()) {
|
|
setShowSyncDialog(true)
|
|
setHasCheckedSync(true)
|
|
}
|
|
} else if (status === 'unauthenticated') {
|
|
setServerProducts([])
|
|
setHasCheckedSync(false)
|
|
}
|
|
}, [status, session?.user?.id])
|
|
|
|
// 数据同步处理
|
|
const handleSyncConfirm = async () => {
|
|
const localData = getAllLocalData()
|
|
const success = await syncLocalToServer(localData)
|
|
if (success) {
|
|
// 重新加载服务器数据
|
|
await loadServerProducts()
|
|
}
|
|
}
|
|
|
|
const handleSyncCancel = async () => {
|
|
// 直接从服务器加载数据,不同步本地数据
|
|
await loadServerProducts()
|
|
}
|
|
|
|
// 添加产品
|
|
async function handleAddProduct() {
|
|
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 {
|
|
if (status === 'authenticated') {
|
|
// 已登录:保存到服务器
|
|
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,
|
|
}
|
|
|
|
const response = await fetch('/api/products', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestData),
|
|
})
|
|
|
|
if (response.ok) {
|
|
const responseData = await response.json()
|
|
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,
|
|
}
|
|
setServerProducts([productWithDates, ...serverProducts])
|
|
toast.success('产品添加成功')
|
|
} else {
|
|
const responseData = await response.json()
|
|
setError(responseData.error || '添加产品失败')
|
|
toast.error('添加产品失败')
|
|
}
|
|
} else {
|
|
// 未登录:保存到本地存储
|
|
addLocalProduct({
|
|
principal: newProduct.principal,
|
|
depositDate: newProduct.depositDate,
|
|
endDate: newProduct.endDate,
|
|
currentNetValue: newProduct.currentNetValue,
|
|
annualRate: newProduct.annualRate,
|
|
currency: newProduct.currency,
|
|
notes: newProduct.notes,
|
|
})
|
|
toast.success('产品已保存到本地')
|
|
}
|
|
|
|
setNewProduct(createEmptyProduct([]))
|
|
} catch (error) {
|
|
console.error('Request error:', error)
|
|
setError('网络请求失败,请重试')
|
|
toast.error('网络请求失败,请重试')
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
// 删除产品
|
|
async function handleRemoveProduct(id: string) {
|
|
if (status === 'authenticated') {
|
|
// 从服务器删除
|
|
try {
|
|
const response = await fetch(`/api/products/${id}`, {
|
|
method: 'DELETE',
|
|
})
|
|
|
|
if (response.ok) {
|
|
setServerProducts(serverProducts.filter(product => product.id !== id))
|
|
toast.success('产品删除成功')
|
|
} else {
|
|
toast.error('删除产品失败')
|
|
}
|
|
} catch (error) {
|
|
toast.error('网络请求失败')
|
|
}
|
|
} else {
|
|
// 从本地存储删除
|
|
removeLocalProduct(id)
|
|
toast.success('产品已从本地删除')
|
|
}
|
|
}
|
|
|
|
// 编辑产品
|
|
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 (!editingProductId) return
|
|
|
|
setIsUpdating(true)
|
|
try {
|
|
if (status === 'authenticated') {
|
|
// 更新服务器数据
|
|
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,
|
|
}
|
|
|
|
const response = await fetch(`/api/products/${editingProductId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestData),
|
|
})
|
|
|
|
if (response.ok) {
|
|
const responseData = await response.json()
|
|
const updatedProduct = {
|
|
...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,
|
|
}
|
|
|
|
setServerProducts(serverProducts.map(product =>
|
|
product.id === editingProductId ? updatedProduct : product
|
|
))
|
|
toast.success('产品更新成功')
|
|
} else {
|
|
toast.error('更新产品失败')
|
|
}
|
|
} else {
|
|
// 更新本地数据
|
|
updateLocalProduct(editingProductId, editingProduct)
|
|
toast.success('产品已在本地更新')
|
|
}
|
|
|
|
setEditingProductId(null)
|
|
setEditingProduct(createEmptyProduct([]))
|
|
} catch (error) {
|
|
toast.error('网络请求失败')
|
|
} finally {
|
|
setIsUpdating(false)
|
|
}
|
|
}
|
|
|
|
function handleUseCurrentTime() {
|
|
setNewProduct({ ...newProduct, endDate: new Date() })
|
|
}
|
|
|
|
function handleUseCurrentTimeForEdit() {
|
|
setEditingProduct({ ...editingProduct, endDate: new Date() })
|
|
}
|
|
|
|
// 更新空产品状态
|
|
useEffect(() => {
|
|
if (currentProducts.length > 0) {
|
|
setNewProduct((prev) => ({
|
|
...prev,
|
|
principal: currentProducts[0].principal,
|
|
}))
|
|
}
|
|
}, [currentProducts.length])
|
|
|
|
// 加载中状态
|
|
if (status === 'loading' || isLocalLoading || isServerLoading) {
|
|
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">
|
|
{/* 数据同步对话框 */}
|
|
<DataSyncDialog
|
|
open={showSyncDialog}
|
|
onOpenChange={setShowSyncDialog}
|
|
localProducts={getAllLocalData()}
|
|
onSyncConfirm={handleSyncConfirm}
|
|
onSyncCancel={handleSyncCancel}
|
|
isSyncing={isSyncing}
|
|
/>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<span>添加新理财产品</span>
|
|
{status === 'unauthenticated' && (
|
|
<span className="text-sm font-normal text-orange-600 bg-orange-50 px-2 py-1 rounded">
|
|
本地存储模式
|
|
</span>
|
|
)}
|
|
</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 className="flex items-center justify-between">
|
|
<span>理财产品对比</span>
|
|
{currentProducts.length > 0 && (
|
|
<span className="text-sm font-normal text-muted-foreground">
|
|
{status === 'authenticated' ? '云端数据' : '本地数据'} · 共 {currentProducts.length} 条记录
|
|
</span>
|
|
)}
|
|
</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>
|
|
{currentProducts.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={12} className="text-center">
|
|
{status === 'authenticated' ? '暂无云端数据' : '暂无本地数据'}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
currentProducts.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="secondary" 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-20"
|
|
/>
|
|
</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"
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Select
|
|
value={editingProduct.currency}
|
|
onValueChange={(value) => setEditingProduct({
|
|
...editingProduct,
|
|
currency: value
|
|
})}
|
|
>
|
|
<SelectTrigger className="w-20">
|
|
<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-20"
|
|
placeholder="备注"
|
|
/>
|
|
</TableCell>
|
|
<TableCell colSpan={4}>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSaveEdit}
|
|
disabled={isUpdating}
|
|
>
|
|
<Check className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={handleCancelEdit}
|
|
disabled={isUpdating}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</>
|
|
) : (
|
|
// 显示模式的行
|
|
<>
|
|
<TableCell>{product.principal.toLocaleString()}</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.toLocaleString() : "-"}
|
|
</TableCell>
|
|
<TableCell>
|
|
{product.calculatedAnnualRate !== null
|
|
? `${product.calculatedAnnualRate.toFixed(2)}%`
|
|
: product.annualRate !== null
|
|
? `${product.annualRate.toFixed(2)}%`
|
|
: "-"}
|
|
</TableCell>
|
|
<TableCell>{product.currency}</TableCell>
|
|
<TableCell>{product.notes || "-"}</TableCell>
|
|
<TableCell className={product.profit && product.profit > 0 ? "text-green-600" : product.profit && product.profit < 0 ? "text-red-600" : ""}>
|
|
{product.profit !== null ? product.profit.toLocaleString() : "-"}
|
|
</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-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleEditProduct(product)}
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => handleRemoveProduct(product.id)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</>
|
|
)}
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AdBanner />
|
|
</div>
|
|
)
|
|
}
|