From 2a60e35fc56f08391e46e7e2249bc513f5a93bd4 Mon Sep 17 00:00:00 2001 From: songtianlun Date: Sat, 14 Jun 2025 20:52:15 +0800 Subject: [PATCH] support local storage --- app/page.tsx | 4 + components/data-sync-dialog.tsx | 150 ++++++++ components/finance-calculator.tsx | 566 ++++++++++++++---------------- components/login-suggestion.tsx | 90 +++++ hooks/use-data-sync.ts | 92 +++++ hooks/use-local-storage.ts | 159 +++++++++ 6 files changed, 767 insertions(+), 294 deletions(-) create mode 100644 components/data-sync-dialog.tsx create mode 100644 components/login-suggestion.tsx create mode 100644 hooks/use-data-sync.ts create mode 100644 hooks/use-local-storage.ts diff --git a/app/page.tsx b/app/page.tsx index c181031..7eb970e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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() { + + {/* 登录建议组件 */} + diff --git a/components/data-sync-dialog.tsx b/components/data-sync-dialog.tsx new file mode 100644 index 0000000..984fdc0 --- /dev/null +++ b/components/data-sync-dialog.tsx @@ -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 + 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 ( + + + + + + 发现本地数据 + + + 检测到您在未登录状态下保存了 {localProducts.length} 条理财产品记录。 + 请选择如何处理这些本地数据: + + + +
+ + + + 本地数据概览: +
    +
  • • 共 {localProducts.length} 条产品记录
  • +
  • • 总本金: ¥{localProducts.reduce((sum, p) => sum + p.principal, 0).toLocaleString()}
  • +
  • • 涉及币种: {Array.from(new Set(localProducts.map(p => p.currency))).join(', ')}
  • +
+
+
+ +
+
setSelectedAction('sync')} + > +
+ setSelectedAction('sync')} + className="mt-1" + /> +
+
+ +

同步到云端

+
+

+ 将本地数据上传到服务器,与云端数据合并。本地数据仍会保留。 +

+
+
+
+ +
setSelectedAction('skip')} + > +
+ setSelectedAction('skip')} + className="mt-1" + /> +
+
+ +

使用云端数据

+
+

+ 直接从服务器加载数据,本地数据不会被修改,但不会显示在界面中。 +

+
+
+
+
+
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/components/finance-calculator.tsx b/components/finance-calculator.tsx index f995209..f9cfc7d 100644 --- a/components/finance-calculator.tsx +++ b/components/finance-calculator.tsx @@ -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([]) - const [newProduct, setNewProduct] = useState(createEmptyProduct()) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [isSubmitting, setIsSubmitting] = useState(false) - // 编辑相关状态 - const [editingProductId, setEditingProductId] = useState(null) - const [editingProduct, setEditingProduct] = useState(createEmptyProduct()) - const [isUpdating, setIsUpdating] = useState(false) + // 服务器数据状态 + const [serverProducts, setServerProducts] = useState([]) + 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(() => createEmptyProduct([])) + const [error, setError] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + + // 编辑相关状态 + const [editingProductId, setEditingProductId] = useState(null) + const [editingProduct, setEditingProduct] = useState(() => 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 ( -
- - - -

需要登录

-

- 请登录以使用理财计算器功能,您的数据将安全保存在云端 -

-
-
- -
- ) - } + }, [currentProducts.length]) // 加载中状态 - if (status === 'loading' || isLoading) { + if (status === 'loading' || isLocalLoading || isServerLoading) { return (
@@ -412,9 +365,26 @@ export default function FinanceCalculator() { return (
+ {/* 数据同步对话框 */} + + - 添加新理财产品 + + 添加新理财产品 + {status === 'unauthenticated' && ( + + 本地存储模式 + + )} +
@@ -565,7 +535,14 @@ export default function FinanceCalculator() { - 理财产品对比 + + 理财产品对比 + {currentProducts.length > 0 && ( + + {status === 'authenticated' ? '云端数据' : '本地数据'} · 共 {currentProducts.length} 条记录 + + )} +
@@ -587,14 +564,14 @@ export default function FinanceCalculator() { - {products.length === 0 ? ( + {currentProducts.length === 0 ? ( - 暂无数据 + {status === 'authenticated' ? '暂无云端数据' : '暂无本地数据'} ) : ( - products.map((product) => ( + currentProducts.map((product) => ( {editingProductId === product.id ? ( // 编辑模式的行 @@ -650,11 +627,7 @@ export default function FinanceCalculator() { /> -
@@ -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" /> @@ -679,12 +652,17 @@ export default function FinanceCalculator() { annualRate: e.target.value ? Number.parseFloat(e.target.value) : null })} className="w-20" - placeholder="自动计算" /> - setEditingProduct({ + ...editingProduct, + currency: value + })} + > + @@ -706,27 +684,22 @@ export default function FinanceCalculator() { ...editingProduct, notes: e.target.value || null })} - className="w-32" + className="w-20" placeholder="备注" /> - - - - - - - - - -
+ +
+
) diff --git a/components/login-suggestion.tsx b/components/login-suggestion.tsx new file mode 100644 index 0000000..64e3b59 --- /dev/null +++ b/components/login-suggestion.tsx @@ -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: , + title: "云端数据存储", + description: "数据安全存储在云端,永不丢失" + }, + { + icon: , + title: "多设备同步", + description: "在不同设备间同步您的理财数据" + }, + { + icon: , + title: "数据备份保护", + description: "专业的数据备份和恢复机制" + } + ] + + return ( +
+ {/* 警告提示 */} + + + + 重要提醒:当前未登录状态下,您的数据仅存储在本地浏览器中。 + 清理浏览器数据或更换设备后,数据将会丢失。建议您登录账号以享用云端存储服务。 + + + + {/* 登录建议卡片 */} + + + + + 建议登录账号 + + + +

+ 登录后可享受以下专业功能,让您的理财管理更安全、更便捷: +

+ +
+ {loginFeatures.map((feature, index) => ( +
+
+ {feature.icon} +
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+
+ ))} +
+ +
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/hooks/use-data-sync.ts b/hooks/use-data-sync.ts new file mode 100644 index 0000000..d6f5f0f --- /dev/null +++ b/hooks/use-data-sync.ts @@ -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 => { + 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, + } +} \ No newline at end of file diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts new file mode 100644 index 0000000..af45728 --- /dev/null +++ b/hooks/use-local-storage.ts @@ -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([]) + 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) => { + 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) => { + 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, + } +} \ No newline at end of file