support local storage
This commit is contained in:
parent
aacc770d59
commit
2a60e35fc5
@ -1,5 +1,6 @@
|
||||
import FinanceCalculator from "@/components/finance-calculator"
|
||||
import { UserNav } from "@/components/auth/user-nav"
|
||||
import { LoginSuggestion } from "@/components/login-suggestion"
|
||||
import { Metadata } from "next"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -49,6 +50,9 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 登录建议组件 */}
|
||||
<LoginSuggestion />
|
||||
</div>
|
||||
|
||||
<FinanceCalculator />
|
||||
|
150
components/data-sync-dialog.tsx
Normal file
150
components/data-sync-dialog.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { Upload, Download, AlertCircle, Loader2 } from "lucide-react"
|
||||
import { LocalFinanceProduct } from "@/hooks/use-local-storage"
|
||||
|
||||
interface DataSyncDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
localProducts: LocalFinanceProduct[]
|
||||
onSyncConfirm: () => Promise<void>
|
||||
onSyncCancel: () => void
|
||||
isSyncing: boolean
|
||||
}
|
||||
|
||||
export function DataSyncDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
localProducts,
|
||||
onSyncConfirm,
|
||||
onSyncCancel,
|
||||
isSyncing
|
||||
}: DataSyncDialogProps) {
|
||||
const [selectedAction, setSelectedAction] = useState<'sync' | 'skip' | null>(null)
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (selectedAction === 'sync') {
|
||||
await onSyncConfirm()
|
||||
} else {
|
||||
onSyncCancel()
|
||||
}
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-amber-500" />
|
||||
发现本地数据
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
检测到您在未登录状态下保存了 {localProducts.length} 条理财产品记录。
|
||||
请选择如何处理这些本地数据:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Alert className="border-blue-200 bg-blue-50">
|
||||
<Upload className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>本地数据概览:</strong>
|
||||
<ul className="mt-2 text-sm space-y-1">
|
||||
<li>• 共 {localProducts.length} 条产品记录</li>
|
||||
<li>• 总本金: ¥{localProducts.reduce((sum, p) => sum + p.principal, 0).toLocaleString()}</li>
|
||||
<li>• 涉及币种: {Array.from(new Set(localProducts.map(p => p.currency))).join(', ')}</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
||||
selectedAction === 'sync'
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300'
|
||||
}`}
|
||||
onClick={() => setSelectedAction('sync')}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="syncAction"
|
||||
checked={selectedAction === 'sync'}
|
||||
onChange={() => setSelectedAction('sync')}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Upload className="h-4 w-4 text-blue-600" />
|
||||
<h4 className="font-medium text-blue-800">同步到云端</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
将本地数据上传到服务器,与云端数据合并。本地数据仍会保留。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-all ${
|
||||
selectedAction === 'skip'
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300'
|
||||
}`}
|
||||
onClick={() => setSelectedAction('skip')}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
name="syncAction"
|
||||
checked={selectedAction === 'skip'}
|
||||
onChange={() => setSelectedAction('skip')}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Download className="h-4 w-4 text-gray-600" />
|
||||
<h4 className="font-medium text-gray-800">使用云端数据</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
直接从服务器加载数据,本地数据不会被修改,但不会显示在界面中。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSyncing}
|
||||
>
|
||||
稍后处理
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedAction || isSyncing}
|
||||
>
|
||||
{isSyncing && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isSyncing ? '处理中...' : '确认'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -7,7 +7,7 @@ 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 { 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"
|
||||
@ -17,6 +17,9 @@ 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
|
||||
@ -48,19 +51,36 @@ interface NewProduct {
|
||||
|
||||
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)
|
||||
// 服务器数据状态
|
||||
const [serverProducts, setServerProducts] = useState<FinanceProduct[]>([])
|
||||
const [isServerLoading, setIsServerLoading] = useState(false)
|
||||
|
||||
function createEmptyProduct(): NewProduct {
|
||||
const lastProduct = products.length > 0 ? products[products.length - 1] : null
|
||||
// 本地数据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,
|
||||
@ -72,55 +92,70 @@ export default function FinanceCalculator() {
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库加载产品
|
||||
async function loadProducts() {
|
||||
// 表单状态
|
||||
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
|
||||
|
||||
setIsLoading(true)
|
||||
setIsServerLoading(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('加载产品数据失败')
|
||||
}
|
||||
const data = await fetchServerData()
|
||||
setServerProducts(data)
|
||||
} catch (error) {
|
||||
toast.error('加载产品数据失败')
|
||||
console.error('Failed to load server products:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsServerLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 当用户登录状态改变时加载产品
|
||||
// 当用户登录状态改变时处理数据加载和同步
|
||||
useEffect(() => {
|
||||
if (status === 'authenticated') {
|
||||
loadProducts()
|
||||
loadServerProducts()
|
||||
|
||||
// 检查是否有本地数据需要同步
|
||||
if (!hasCheckedSync && hasLocalData()) {
|
||||
setShowSyncDialog(true)
|
||||
setHasCheckedSync(true)
|
||||
}
|
||||
} else if (status === 'unauthenticated') {
|
||||
setProducts([])
|
||||
setServerProducts([])
|
||||
setHasCheckedSync(false)
|
||||
}
|
||||
}, [status, session?.user?.id])
|
||||
|
||||
async function handleAddProduct() {
|
||||
if (!session?.user?.id) {
|
||||
toast.error('请先登录')
|
||||
return
|
||||
// 数据同步处理
|
||||
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
|
||||
|
||||
@ -133,6 +168,8 @@ export default function FinanceCalculator() {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
if (status === 'authenticated') {
|
||||
// 已登录:保存到服务器
|
||||
const requestData = {
|
||||
principal: newProduct.principal,
|
||||
depositDate: newProduct.depositDate?.toISOString() || null,
|
||||
@ -143,10 +180,6 @@ export default function FinanceCalculator() {
|
||||
notes: newProduct.notes,
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Sending data to API:', requestData)
|
||||
}
|
||||
|
||||
const response = await fetch('/api/products', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -155,13 +188,8 @@ export default function FinanceCalculator() {
|
||||
body: JSON.stringify(requestData),
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('API response:', responseData)
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// 转换日期字符串为Date对象
|
||||
const responseData = await response.json()
|
||||
const productWithDates = {
|
||||
...responseData,
|
||||
depositDate: responseData.depositDate ? new Date(responseData.depositDate) : null,
|
||||
@ -169,29 +197,30 @@ export default function FinanceCalculator() {
|
||||
createdAt: responseData.createdAt ? new Date(responseData.createdAt) : undefined,
|
||||
updatedAt: responseData.updatedAt ? new Date(responseData.updatedAt) : undefined,
|
||||
}
|
||||
setProducts([productWithDates, ...products])
|
||||
setNewProduct(createEmptyProduct())
|
||||
setServerProducts([productWithDates, ...serverProducts])
|
||||
toast.success('产品添加成功')
|
||||
} else {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
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 {
|
||||
const responseData = await response.json()
|
||||
setError(responseData.error || '添加产品失败')
|
||||
}
|
||||
toast.error('添加产品失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Request error:', 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 {
|
||||
@ -199,7 +228,32 @@ export default function FinanceCalculator() {
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑产品
|
||||
// 删除产品
|
||||
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({
|
||||
@ -213,27 +267,18 @@ export default function FinanceCalculator() {
|
||||
})
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
function handleCancelEdit() {
|
||||
setEditingProductId(null)
|
||||
setEditingProduct(createEmptyProduct())
|
||||
setEditingProduct(createEmptyProduct([]))
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
async function handleSaveEdit() {
|
||||
if (!session?.user?.id || !editingProductId) {
|
||||
toast.error('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
if (editingProduct.principal <= 0) {
|
||||
toast.error("本金必须大于0")
|
||||
return
|
||||
}
|
||||
if (!editingProductId) return
|
||||
|
||||
setIsUpdating(true)
|
||||
|
||||
try {
|
||||
if (status === 'authenticated') {
|
||||
// 更新服务器数据
|
||||
const requestData = {
|
||||
principal: editingProduct.principal,
|
||||
depositDate: editingProduct.depositDate?.toISOString() || null,
|
||||
@ -244,10 +289,6 @@ export default function FinanceCalculator() {
|
||||
notes: editingProduct.notes,
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Updating product with data:', requestData)
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/products/${editingProductId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
@ -256,14 +297,9 @@ export default function FinanceCalculator() {
|
||||
body: JSON.stringify(requestData),
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Update API response:', responseData)
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// 转换日期字符串为Date对象
|
||||
const updatedProductWithDates = {
|
||||
const responseData = await response.json()
|
||||
const updatedProduct = {
|
||||
...responseData,
|
||||
depositDate: responseData.depositDate ? new Date(responseData.depositDate) : null,
|
||||
endDate: responseData.endDate ? new Date(responseData.endDate) : null,
|
||||
@ -271,131 +307,48 @@ export default function FinanceCalculator() {
|
||||
updatedAt: responseData.updatedAt ? new Date(responseData.updatedAt) : undefined,
|
||||
}
|
||||
|
||||
// 更新产品列表
|
||||
setProducts(products.map(p =>
|
||||
p.id === editingProductId ? updatedProductWithDates : p
|
||||
setServerProducts(serverProducts.map(product =>
|
||||
product.id === editingProductId ? updatedProduct : product
|
||||
))
|
||||
|
||||
// 清除编辑状态
|
||||
setEditingProductId(null)
|
||||
setEditingProduct(createEmptyProduct())
|
||||
|
||||
// 显示智能计算状态toast
|
||||
if (responseData.smartCalculation) {
|
||||
const { hasSmartCalculation, calculationType, message, recalculatedFields } = responseData.smartCalculation
|
||||
|
||||
if (hasSmartCalculation) {
|
||||
if (calculationType === 'time_profit_changed') {
|
||||
toast.success(
|
||||
`🤖 智能计算完成!检测到时间或利润相关字段变化,已自动重新计算年化利率和相关收益。已更新:${recalculatedFields.join('、')}`,
|
||||
{ duration: 4000 }
|
||||
)
|
||||
} else if (calculationType === 'annual_rate_changed') {
|
||||
toast.success(
|
||||
`🤖 智能计算完成!检测到年化利率变化,已自动重新计算当前净值和利润。已更新:${recalculatedFields.join('、')}`,
|
||||
{ duration: 4000 }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
toast.info(
|
||||
`ℹ️ ${message}`,
|
||||
{ duration: 3000 }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
toast.success('产品更新成功')
|
||||
} else {
|
||||
toast.error('更新产品失败')
|
||||
}
|
||||
} else {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Update API error response:', responseData)
|
||||
// 更新本地数据
|
||||
updateLocalProduct(editingProductId, editingProduct)
|
||||
toast.success('产品已在本地更新')
|
||||
}
|
||||
|
||||
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 || '更新产品失败')
|
||||
}
|
||||
}
|
||||
setEditingProductId(null)
|
||||
setEditingProduct(createEmptyProduct([]))
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Update request error:', error)
|
||||
}
|
||||
toast.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(),
|
||||
})
|
||||
setNewProduct({ ...newProduct, endDate: new Date() })
|
||||
}
|
||||
|
||||
function handleUseCurrentTimeForEdit() {
|
||||
setEditingProduct({
|
||||
...editingProduct,
|
||||
endDate: new Date(),
|
||||
})
|
||||
setEditingProduct({ ...editingProduct, endDate: new Date() })
|
||||
}
|
||||
|
||||
// 当产品列表变化时,更新新产品的默认本金
|
||||
// 更新空产品状态
|
||||
useEffect(() => {
|
||||
if (products.length > 0) {
|
||||
if (currentProducts.length > 0) {
|
||||
setNewProduct((prev) => ({
|
||||
...prev,
|
||||
principal: products[0].principal,
|
||||
principal: currentProducts[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>
|
||||
<AdBanner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}, [currentProducts.length])
|
||||
|
||||
// 加载中状态
|
||||
if (status === 'loading' || isLoading) {
|
||||
if (status === 'loading' || isLocalLoading || isServerLoading) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
@ -412,9 +365,26 @@ export default function FinanceCalculator() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* 数据同步对话框 */}
|
||||
<DataSyncDialog
|
||||
open={showSyncDialog}
|
||||
onOpenChange={setShowSyncDialog}
|
||||
localProducts={getAllLocalData()}
|
||||
onSyncConfirm={handleSyncConfirm}
|
||||
onSyncCancel={handleSyncCancel}
|
||||
isSyncing={isSyncing}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>添加新理财产品</CardTitle>
|
||||
<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">
|
||||
@ -565,7 +535,14 @@ export default function FinanceCalculator() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>理财产品对比</CardTitle>
|
||||
<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">
|
||||
@ -587,14 +564,14 @@ export default function FinanceCalculator() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products.length === 0 ? (
|
||||
{currentProducts.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={12} className="text-center">
|
||||
暂无数据
|
||||
{status === 'authenticated' ? '暂无云端数据' : '暂无本地数据'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
products.map((product) => (
|
||||
currentProducts.map((product) => (
|
||||
<TableRow key={product.id}>
|
||||
{editingProductId === product.id ? (
|
||||
// 编辑模式的行
|
||||
@ -650,11 +627,7 @@ export default function FinanceCalculator() {
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleUseCurrentTimeForEdit}
|
||||
>
|
||||
<Button variant="secondary" size="sm" onClick={handleUseCurrentTimeForEdit}>
|
||||
现在
|
||||
</Button>
|
||||
</div>
|
||||
@ -667,7 +640,7 @@ export default function FinanceCalculator() {
|
||||
...editingProduct,
|
||||
currentNetValue: e.target.value ? Number.parseFloat(e.target.value) : null
|
||||
})}
|
||||
className="w-24"
|
||||
className="w-20"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@ -679,12 +652,17 @@ export default function FinanceCalculator() {
|
||||
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">
|
||||
<Select
|
||||
value={editingProduct.currency}
|
||||
onValueChange={(value) => setEditingProduct({
|
||||
...editingProduct,
|
||||
currency: value
|
||||
})}
|
||||
>
|
||||
<SelectTrigger className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -706,27 +684,22 @@ export default function FinanceCalculator() {
|
||||
...editingProduct,
|
||||
notes: e.target.value || null
|
||||
})}
|
||||
className="w-32"
|
||||
className="w-20"
|
||||
placeholder="备注"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<TableCell colSpan={4}>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size="sm"
|
||||
onClick={handleSaveEdit}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCancelEdit}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
@ -736,9 +709,9 @@ export default function FinanceCalculator() {
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
// 普通显示模式的行
|
||||
// 显示模式的行
|
||||
<>
|
||||
<TableCell>{product.principal.toFixed(2)}</TableCell>
|
||||
<TableCell>{product.principal.toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
{product.depositDate ? format(product.depositDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
|
||||
</TableCell>
|
||||
@ -746,37 +719,41 @@ export default function FinanceCalculator() {
|
||||
{product.endDate ? format(product.endDate, "yyyy-MM-dd", { locale: zhCN }) : "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{product.currentNetValue !== null ? product.currentNetValue.toFixed(2) : "-"}
|
||||
{product.currentNetValue !== null ? product.currentNetValue.toLocaleString() : "-"}
|
||||
</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">
|
||||
{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
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEditProduct(product)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRemoveProduct(product.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@ -793,6 +770,7 @@ export default function FinanceCalculator() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<AdBanner />
|
||||
</div>
|
||||
)
|
||||
|
90
components/login-suggestion.tsx
Normal file
90
components/login-suggestion.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { useSession, signIn } from "next-auth/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { LogIn, AlertTriangle, Cloud, Shield, RefreshCw } from "lucide-react"
|
||||
|
||||
export function LoginSuggestion() {
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
// 如果已登录,不显示此组件
|
||||
if (status === "authenticated" || status === "loading") {
|
||||
return null
|
||||
}
|
||||
|
||||
const loginFeatures = [
|
||||
{
|
||||
icon: <Cloud className="h-4 w-4" />,
|
||||
title: "云端数据存储",
|
||||
description: "数据安全存储在云端,永不丢失"
|
||||
},
|
||||
{
|
||||
icon: <RefreshCw className="h-4 w-4" />,
|
||||
title: "多设备同步",
|
||||
description: "在不同设备间同步您的理财数据"
|
||||
},
|
||||
{
|
||||
icon: <Shield className="h-4 w-4" />,
|
||||
title: "数据备份保护",
|
||||
description: "专业的数据备份和恢复机制"
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 警告提示 */}
|
||||
<Alert className="border-orange-200 bg-orange-50 text-orange-800">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>重要提醒:</strong>当前未登录状态下,您的数据仅存储在本地浏览器中。
|
||||
清理浏览器数据或更换设备后,数据将会丢失。建议您登录账号以享用云端存储服务。
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* 登录建议卡片 */}
|
||||
<Card className="border-blue-200 bg-blue-50/50">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg text-blue-800">
|
||||
<LogIn className="h-5 w-5" />
|
||||
建议登录账号
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-blue-700 mb-4">
|
||||
登录后可享受以下专业功能,让您的理财管理更安全、更便捷:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{loginFeatures.map((feature, index) => (
|
||||
<div key={index} className="flex items-start space-x-2 p-3 bg-white rounded-lg border border-blue-100">
|
||||
<div className="text-blue-600 mt-0.5">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-blue-800 mb-1">
|
||||
{feature.title}
|
||||
</h4>
|
||||
<p className="text-xs text-blue-600">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex justify-center">
|
||||
<Button
|
||||
onClick={() => signIn()}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6"
|
||||
>
|
||||
<LogIn className="h-4 w-4 mr-2" />
|
||||
立即登录
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
92
hooks/use-data-sync.ts
Normal file
92
hooks/use-data-sync.ts
Normal file
@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { toast } from "sonner"
|
||||
import { LocalFinanceProduct } from "./use-local-storage"
|
||||
|
||||
export function useDataSync() {
|
||||
const { data: session } = useSession()
|
||||
const [isSyncing, setIsSyncing] = useState(false)
|
||||
|
||||
// 将本地数据同步到服务器
|
||||
const syncLocalToServer = async (localProducts: LocalFinanceProduct[]): Promise<boolean> => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('用户未登录')
|
||||
}
|
||||
|
||||
setIsSyncing(true)
|
||||
|
||||
try {
|
||||
const syncPromises = localProducts.map(async (product) => {
|
||||
const requestData = {
|
||||
principal: product.principal,
|
||||
depositDate: product.depositDate?.toISOString() || null,
|
||||
endDate: product.endDate?.toISOString() || null,
|
||||
currentNetValue: product.currentNetValue,
|
||||
annualRate: product.annualRate,
|
||||
currency: product.currency,
|
||||
notes: product.notes,
|
||||
}
|
||||
|
||||
const response = await fetch('/api/products', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(`同步产品失败: ${errorData.error || '未知错误'}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
|
||||
await Promise.all(syncPromises)
|
||||
toast.success(`成功同步 ${localProducts.length} 条本地数据到服务器`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('数据同步失败:', error)
|
||||
toast.error('数据同步失败,请重试')
|
||||
return false
|
||||
} finally {
|
||||
setIsSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 从服务器获取数据
|
||||
const fetchServerData = async () => {
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('用户未登录')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/products')
|
||||
if (!response.ok) {
|
||||
throw new Error('获取服务器数据失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return 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,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取服务器数据失败:', error)
|
||||
toast.error('获取服务器数据失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isSyncing,
|
||||
syncLocalToServer,
|
||||
fetchServerData,
|
||||
}
|
||||
}
|
159
hooks/use-local-storage.ts
Normal file
159
hooks/use-local-storage.ts
Normal file
@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
export interface LocalFinanceProduct {
|
||||
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
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'finance-calculator-products'
|
||||
|
||||
export function useLocalStorage() {
|
||||
const [products, setProducts] = useState<LocalFinanceProduct[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
// 从本地存储加载数据
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
const parsedProducts = JSON.parse(stored).map((product: any) => ({
|
||||
...product,
|
||||
depositDate: product.depositDate ? new Date(product.depositDate) : null,
|
||||
endDate: product.endDate ? new Date(product.endDate) : null,
|
||||
createdAt: new Date(product.createdAt),
|
||||
updatedAt: new Date(product.updatedAt),
|
||||
}))
|
||||
setProducts(parsedProducts)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load local storage data:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 保存数据到本地存储
|
||||
const saveToLocal = (newProducts: LocalFinanceProduct[]) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newProducts))
|
||||
setProducts(newProducts)
|
||||
} catch (error) {
|
||||
console.error('Failed to save to local storage:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 添加产品到本地存储
|
||||
const addProduct = (productData: Omit<LocalFinanceProduct, 'id' | 'createdAt' | 'updatedAt' | 'profit' | 'dailyProfit' | 'monthlyProfit' | 'calculatedAnnualRate' | 'averageAnnualProfit'>) => {
|
||||
const now = new Date()
|
||||
const newProduct: LocalFinanceProduct = {
|
||||
...productData,
|
||||
id: `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
profit: null,
|
||||
dailyProfit: null,
|
||||
monthlyProfit: null,
|
||||
calculatedAnnualRate: null,
|
||||
averageAnnualProfit: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
// 计算收益相关数据
|
||||
const calculatedProduct = calculateProductStats(newProduct)
|
||||
const newProducts = [calculatedProduct, ...products]
|
||||
saveToLocal(newProducts)
|
||||
return calculatedProduct
|
||||
}
|
||||
|
||||
// 更新产品
|
||||
const updateProduct = (id: string, updates: Partial<LocalFinanceProduct>) => {
|
||||
const updatedProducts = products.map(product => {
|
||||
if (product.id === id) {
|
||||
const updatedProduct = { ...product, ...updates, updatedAt: new Date() }
|
||||
return calculateProductStats(updatedProduct)
|
||||
}
|
||||
return product
|
||||
})
|
||||
saveToLocal(updatedProducts)
|
||||
}
|
||||
|
||||
// 删除产品
|
||||
const removeProduct = (id: string) => {
|
||||
const newProducts = products.filter(product => product.id !== id)
|
||||
saveToLocal(newProducts)
|
||||
}
|
||||
|
||||
// 清空本地存储
|
||||
const clearLocal = () => {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
setProducts([])
|
||||
}
|
||||
|
||||
// 获取所有本地数据(用于同步)
|
||||
const getAllLocalData = () => {
|
||||
return products
|
||||
}
|
||||
|
||||
// 检查是否有本地数据
|
||||
const hasLocalData = () => {
|
||||
return products.length > 0
|
||||
}
|
||||
|
||||
return {
|
||||
products,
|
||||
isLoading,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
removeProduct,
|
||||
clearLocal,
|
||||
getAllLocalData,
|
||||
hasLocalData,
|
||||
}
|
||||
}
|
||||
|
||||
// 计算产品统计数据的辅助函数
|
||||
function calculateProductStats(product: LocalFinanceProduct): LocalFinanceProduct {
|
||||
let profit = null
|
||||
let dailyProfit = null
|
||||
let monthlyProfit = null
|
||||
let calculatedAnnualRate = null
|
||||
let averageAnnualProfit = null
|
||||
|
||||
if (product.currentNetValue && product.principal) {
|
||||
profit = product.currentNetValue - product.principal
|
||||
|
||||
if (product.depositDate && product.endDate) {
|
||||
const days = Math.ceil((product.endDate.getTime() - product.depositDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
if (days > 0) {
|
||||
dailyProfit = profit / days
|
||||
monthlyProfit = dailyProfit * 30
|
||||
calculatedAnnualRate = ((product.currentNetValue / product.principal) ** (365 / days) - 1) * 100
|
||||
averageAnnualProfit = profit * (365 / days)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...product,
|
||||
profit,
|
||||
dailyProfit,
|
||||
monthlyProfit,
|
||||
calculatedAnnualRate,
|
||||
averageAnnualProfit,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user