Files
chinese-family-tree-2/app/settings/page.tsx
T
freedakgmail e92884ae53 0.0.1.0
2025-11-23 03:07:39 +08:00

362 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { SiteHeader } from "@/components/site-header"
import { useFamily } from "@/context/family-context"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Download, Upload, AlertTriangle, RefreshCw, FileText, Bell, History, Database, User as UserIcon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { useRef, useState, useEffect } from "react"
import { exportToGedcom, importFromGedcom } from "@/lib/gedcom"
import { NotificationSettings } from "@/components/settings/notification-settings"
import { ActivityLogViewer } from "@/components/settings/activity-log-viewer"
import { upgradeDatabase } from "@/lib/db-upgrade"
import { ErrorBoundary } from "@/components/error-boundary"
import { useSession } from "next-auth/react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { ChangePassword } from "@/components/settings/change-password"
interface UserProfile {
id: string
email: string
name: string | null
createdAt: string
updatedAt: string
ownedTreesCount: number
collaborations: Array<{
treeId: string
treeName: string
role: string
}>
}
export default function SettingsPage() {
const { treeData, loadData, resetData } = useFamily()
const { data: session } = useSession()
const [importStatus, setImportStatus] = useState<string>("")
const [gedcomStatus, setGedcomStatus] = useState<string>("")
const [userProfile, setUserProfile] = useState<UserProfile | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const gedcomInputRef = useRef<HTMLInputElement>(null)
const getUserInitial = () => {
if (session?.user?.name) {
return session.user.name.charAt(0).toUpperCase()
}
if (session?.user?.email) {
return session.user.email.charAt(0).toUpperCase()
}
return "U"
}
// 获取用户资料
useEffect(() => {
if (session?.user?.id) {
fetch('/api/user/profile')
.then(res => res.json())
.then(data => {
if (data.user) {
setUserProfile(data.user)
}
})
.catch(err => console.error('获取用户资料失败:', err))
}
}, [session?.user?.id])
const getRoleText = (role: string) => {
const roleMap: Record<string, string> = {
'OWNER': '所有者',
'EDITOR': '编辑者',
'VIEWER': '查看者'
}
return roleMap[role] || role
}
const getRoleBadgeColor = (role: string) => {
const colorMap: Record<string, string> = {
'OWNER': 'bg-purple-100 text-purple-700 border-purple-200',
'EDITOR': 'bg-blue-100 text-blue-700 border-blue-200',
'VIEWER': 'bg-gray-100 text-gray-700 border-gray-200'
}
return colorMap[role] || 'bg-gray-100 text-gray-700'
}
return (
<div className="min-h-screen bg-background flex flex-col font-sans">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6 flex-1 max-w-6xl">
<h1 className="text-3xl font-serif font-bold mb-8"> (Settings)</h1>
<div className="space-y-6">
{/* 用户信息和密码修改 - 两列布局 */}
{session && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserIcon className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<Avatar className="h-16 w-16 border-2 border-border">
<AvatarFallback className="text-2xl">{getUserInitial()}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="text-lg font-semibold">{session.user?.name || "用户"}</h3>
<p className="text-sm text-muted-foreground">{session.user?.email}</p>
<p className="text-xs text-muted-foreground mt-1">
ID: {session.user?.id}
</p>
</div>
</div>
{userProfile && (userProfile.ownedTreesCount > 0 || userProfile.collaborations.length > 0) && (
<div className="space-y-3 pt-3 border-t">
<p className="text-sm font-medium"></p>
<div className="space-y-2">
{/* 显示拥有的家族树(所有者) */}
{userProfile.ownedTreesCount > 0 && (
<div className="text-sm text-muted-foreground">
<span className={`inline-flex items-center px-2 py-1 rounded-full border text-xs ${getRoleBadgeColor('OWNER')}`}>
{getRoleText('OWNER')}
</span>
<span className="ml-2">{userProfile.ownedTreesCount} </span>
</div>
)}
{/* 显示协作的家族树 */}
{userProfile.collaborations.length > 0 && (
<div className="space-y-2">
{userProfile.collaborations.map((collab) => (
<div key={collab.treeId} className="flex items-center justify-between text-sm p-2 rounded-md bg-muted/50">
<span className="text-muted-foreground truncate flex-1">{collab.treeName}</span>
<span className={`text-xs px-2 py-1 rounded-full border ${getRoleBadgeColor(collab.role)}`}>
{getRoleText(collab.role)}
</span>
</div>
))}
</div>
)}
</div>
</div>
)}
</CardContent>
</Card>
<ChangePassword />
</div>
)}
{/* 通知设置和数据备份 - 两列布局 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<NotificationSettings />
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Download className="h-5 w-5" /> (Backup)
</CardTitle>
<CardDescription> JSON </CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={() => {
const dataStr = JSON.stringify(treeData, null, 2)
const blob = new Blob([dataStr], { type: "application/json" })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = `family_tree_backup_${new Date().toISOString().split("T")[0]}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}}
>
(Export JSON)
</Button>
</CardContent>
</Card>
</div>
{/* 数据恢复和 GEDCOM - 两列布局 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" /> (Restore)
</CardTitle>
<CardDescription> JSON </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription></AlertDescription>
</Alert>
<div className="flex items-center gap-4">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept=".json"
onChange={(e) => {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = async (event) => {
try {
const jsonStr = event.target?.result as string
const data = JSON.parse(jsonStr)
await loadData(data)
setImportStatus("导入成功!")
if (fileInputRef.current) fileInputRef.current.value = ""
} catch (err) {
console.error(err)
setImportStatus("错误:无法解析 JSON 文件")
}
}
reader.readAsText(file)
}}
/>
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
...
</Button>
<span className="text-sm text-muted-foreground">{importStatus}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" /> GEDCOM
</CardTitle>
<CardDescription>/ GEDCOM AncestryMyHeritage</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => {
const gedcomText = exportToGedcom(treeData)
const blob = new Blob([gedcomText], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = `family_tree_${new Date().toISOString().split("T")[0]}.ged`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}}
>
<Download className="mr-2 h-4 w-4" />
GEDCOM
</Button>
<input
type="file"
ref={gedcomInputRef}
className="hidden"
accept=".ged,.gedcom"
onChange={(e) => {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = async (event) => {
try {
const gedcomText = event.target?.result as string
const importedData = importFromGedcom(gedcomText)
await loadData(importedData)
setGedcomStatus("GEDCOM 导入成功!")
if (gedcomInputRef.current) gedcomInputRef.current.value = ""
} catch (err) {
console.error(err)
setGedcomStatus("错误:无法解析 GEDCOM 文件")
}
}
reader.readAsText(file)
}}
/>
<Button variant="outline" onClick={() => gedcomInputRef.current?.click()}>
<Upload className="mr-2 h-4 w-4" />
GEDCOM
</Button>
{gedcomStatus && <span className="text-sm text-muted-foreground">{gedcomStatus}</span>}
</div>
</CardContent>
</Card>
</div>
{/* 数据库和系统管理 - 两列布局 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertDescription>
</AlertDescription>
</Alert>
<Button
variant="outline"
onClick={async () => {
if (confirm("确定要升级数据库吗?这会保留所有现有数据。")) {
try {
await upgradeDatabase()
alert("数据库升级成功!请刷新页面。")
window.location.reload()
} catch (error) {
alert("数据库升级失败:" + (error as Error).message)
}
}
}}
>
<Database className="mr-2 h-4 w-4" />
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" /> (Danger Zone)
</CardTitle>
<CardDescription>使</CardDescription>
</CardHeader>
<CardContent>
<Button
variant="destructive"
onClick={async () => {
if (confirm("确定要重置吗?所有更改将丢失。")) {
await resetData()
alert("数据已重置为演示状态。")
}
}}
>
</Button>
</CardContent>
</Card>
</div>
{/* 操作日志 - 全宽 */}
<ErrorBoundary>
<ActivityLogViewer />
</ErrorBoundary>
</div>
</main>
</div>
)
}