support local storage

This commit is contained in:
songtianlun 2025-06-14 20:52:15 +08:00
parent aacc770d59
commit 2a60e35fc5
6 changed files with 767 additions and 294 deletions

View File

@ -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 />

View 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>
)
}

View File

@ -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)
// 本地数据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(): NewProduct {
const lastProduct = products.length > 0 ? products[products.length - 1] : null
// 创建空产品的函数
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,
@ -71,56 +91,71 @@ export default function FinanceCalculator() {
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 loadProducts() {
// 从服务器加载产品
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,65 +168,59 @@ export default function FinanceCalculator() {
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,
}
if (process.env.NODE_ENV === 'development') {
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()
if (process.env.NODE_ENV === 'development') {
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,
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,
}
setProducts([productWithDates, ...products])
setNewProduct(createEmptyProduct())
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}`)
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('添加产品失败')
}
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) {
if (process.env.NODE_ENV === 'development') {
console.error('Request error:', 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,189 +267,88 @@ 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 {
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,
}
if (process.env.NODE_ENV === 'development') {
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()
if (process.env.NODE_ENV === 'development') {
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,
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,
}
// 更新产品列表
setProducts(products.map(p =>
p.id === editingProductId ? updatedProductWithDates : p
))
// 清除编辑状态
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 }
)
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,
}
} else {
setServerProducts(serverProducts.map(product =>
product.id === editingProductId ? updatedProduct : product
))
toast.success('产品更新成功')
} else {
toast.error('更新产品失败')
}
} else {
if (process.env.NODE_ENV === 'development') {
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 || '更新产品失败')
}
// 更新本地数据
updateLocalProduct(editingProductId, editingProduct)
toast.success('产品已在本地更新')
}
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>
)

View 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
View 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
View 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,
}
}