This commit is contained in:
freedakgmail
2025-11-23 09:13:25 +08:00
parent 81dbf709aa
commit bade25ff26
378 changed files with 769815 additions and 2334 deletions
+150 -181
View File
@@ -8,13 +8,12 @@ import { Download, Upload, AlertTriangle, RefreshCw, FileText, Bell, History, Da
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"
import { useDialog } from "@/components/ui/alert-dialog-custom"
interface UserProfile {
id: string
@@ -33,6 +32,7 @@ interface UserProfile {
export default function SettingsPage() {
const { treeData, loadData, resetData } = useFamily()
const { data: session } = useSession()
const { showAlert, showConfirm } = useDialog()
const [importStatus, setImportStatus] = useState<string>("")
const [gedcomStatus, setGedcomStatus] = useState<string>("")
const [userProfile, setUserProfile] = useState<UserProfile | null>(null)
@@ -94,7 +94,7 @@ export default function SettingsPage() {
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserIcon className="h-5 w-5" />
<UserIcon className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
@@ -142,6 +142,26 @@ export default function SettingsPage() {
</div>
</div>
)}
<div className="pt-3 border-t">
<p className="text-sm font-medium text-destructive mb-2 flex items-center gap-2">
<AlertTriangle className="h-4 w-4" />
</p>
<Button
variant="destructive"
size="sm"
className="w-full"
onClick={async () => {
const confirmed = await showConfirm("确定要重置吗?所有更改将丢失。", "重置数据", "destructive")
if (confirmed) {
await resetData()
await showAlert("数据已重置为演示状态。", "成功")
}
}}
>
</Button>
</div>
</CardContent>
</Card>
@@ -149,83 +169,87 @@ export default function SettingsPage() {
</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 - 两列布局 */}
{/* 数据管理和 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)
<Database className="h-5 w-5" /> (Data Management)
</CardTitle>
<CardDescription> JSON </CardDescription>
<CardDescription></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)
<CardContent className="space-y-6">
{/* 备份部分 */}
<div className="space-y-2">
<h3 className="text-sm font-medium flex items-center gap-2">
<Download className="h-4 w-4" />
</h3>
<p className="text-sm text-muted-foreground"> JSON </p>
<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)
}}
/>
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
...
>
(Export JSON)
</Button>
<span className="text-sm text-muted-foreground">{importStatus}</span>
</div>
<div className="border-t" />
{/* 恢复部分 */}
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium flex items-center gap-2">
<Upload className="h-4 w-4" />
</h3>
<p className="text-sm text-muted-foreground mt-1"> JSON </p>
</div>
<div className="flex items-center gap-4">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept=".json"
onChange={async (e) => {
const file = e.target.files?.[0]
if (!file) return
const confirmed = await showConfirm("恢复数据将覆盖当前所有数据,确定继续吗?", "恢复数据", "destructive")
if (!confirmed) {
if (fileInputRef.current) fileInputRef.current.value = ""
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>
</div>
</CardContent>
</Card>
@@ -237,116 +261,61 @@ export default function SettingsPage() {
</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
<CardContent className="space-y-4">
<div className="flex flex-col gap-4">
<Button
variant="outline"
className="justify-start"
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>
<div className="flex items-center gap-2">
<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>
const reader = new FileReader()
reader.onload = async (event) => {
try {
const gedcomText = event.target?.result as string
const importedData = importFromGedcom(gedcomText)
await loadData(importedData)
setGedcomStatus("导入成功!")
if (gedcomInputRef.current) gedcomInputRef.current.value = ""
} catch (err) {
console.error(err)
setGedcomStatus("错误:无法解析文件")
}
}
reader.readAsText(file)
}}
/>
<Button variant="outline" className="justify-start" 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>
</div>
</CardContent>
</Card>
</div>