0.0.1.2
This commit is contained in:
+150
-181
@@ -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 格式,兼容其他家谱软件(如 Ancestry、MyHeritage)。</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user