0.0.0.2
This commit is contained in:
@@ -8,17 +8,34 @@ import { SiteHeader } from "@/components/site-header"
|
||||
import { MemberForm } from "@/components/members/member-form"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ArrowLeft, Edit, Trash, Share2, Printer } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import { db } from "@/lib/db"
|
||||
|
||||
export default function MemberProfilePage() {
|
||||
const { id } = useParams()
|
||||
const { getMember, updateMember, deleteMember } = useFamily()
|
||||
const router = useRouter()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | undefined>(undefined)
|
||||
|
||||
const member = getMember(id as string)
|
||||
|
||||
// Load avatar blob if exists
|
||||
useEffect(() => {
|
||||
if (member?.avatarImageId) {
|
||||
db.images.get(member.avatarImageId).then((image) => {
|
||||
if (image) {
|
||||
const url = URL.createObjectURL(image.blob)
|
||||
setAvatarBlobUrl(url)
|
||||
return () => URL.revokeObjectURL(url)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setAvatarBlobUrl(undefined)
|
||||
}
|
||||
}, [member?.avatarImageId])
|
||||
|
||||
if (!member) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
@@ -47,7 +64,7 @@ export default function MemberProfilePage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<div className="container py-8">
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Button variant="ghost" onClick={() => setIsEditing(false)}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> 返回
|
||||
@@ -63,14 +80,14 @@ export default function MemberProfilePage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<main className="container py-8 px-4 md:px-6">
|
||||
<main className="container mx-auto py-8 px-4 md:px-6">
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start gap-6 mb-8 border-b border-border pb-8">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="h-32 w-32 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
|
||||
{member.avatarUrl ? (
|
||||
{avatarBlobUrl || member.avatarUrl ? (
|
||||
<img
|
||||
src={member.avatarUrl || "/placeholder.svg"}
|
||||
src={avatarBlobUrl || member.avatarUrl || "/placeholder.svg"}
|
||||
alt={member.fullName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function NewMemberPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
<SiteHeader />
|
||||
<div className="container py-8">
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Button variant="ghost" onClick={() => router.back()}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" /> 返回
|
||||
|
||||
+11
-8
@@ -9,6 +9,7 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Search, Plus, Filter } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { AvatarDisplay } from "@/components/ui/avatar-display"
|
||||
|
||||
export default function MembersPage() {
|
||||
const { treeData, searchResults, searchMembers } = useFamily()
|
||||
@@ -17,13 +18,13 @@ export default function MembersPage() {
|
||||
const displayMembers = query
|
||||
? searchResults
|
||||
: Object.values(treeData.members).sort(
|
||||
(a, b) => a.generation - b.generation || (a.birthDate || "").localeCompare(b.birthDate || ""),
|
||||
)
|
||||
(a, b) => a.generation - b.generation || (a.birthDate || "").localeCompare(b.birthDate || ""),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col font-sans">
|
||||
<SiteHeader />
|
||||
<main className="container py-8 px-4 md:px-6 flex-1">
|
||||
<main className="container mx-auto py-8 px-4 md:px-6 flex-1">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center mb-8 gap-4">
|
||||
<h1 className="text-3xl font-serif font-bold">家族成员列表</h1>
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
@@ -57,10 +58,12 @@ export default function MembersPage() {
|
||||
<Link href={`/members/${member.id}`} key={member.id}>
|
||||
<Card className="hover:border-primary transition-colors cursor-pointer h-full">
|
||||
<CardContent className="p-6 flex items-start gap-4">
|
||||
<Avatar className="h-16 w-16 border-2 border-border">
|
||||
<AvatarImage src={member.avatarUrl || "/placeholder.svg"} />
|
||||
<AvatarFallback className="text-lg font-serif bg-muted">{member.surname}</AvatarFallback>
|
||||
</Avatar>
|
||||
<AvatarDisplay
|
||||
imageId={member.avatarImageId}
|
||||
fallbackUrl={member.avatarUrl}
|
||||
fallbackText={member.surname}
|
||||
className="h-16 w-16 border-2 border-border"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<h3 className="text-xl font-bold font-serif truncate">
|
||||
@@ -80,7 +83,7 @@ export default function MembersPage() {
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{member.ancestralHome || "籍贯未知"}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{member.lifeStatus === "deceased" && (
|
||||
{member.deathDate && (
|
||||
<span className="text-[10px] bg-neutral-100 text-neutral-600 px-1.5 py-0.5 rounded border border-neutral-200">
|
||||
已故
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export default function DashboardPage() {
|
||||
<div className="flex min-h-screen flex-col bg-background font-sans">
|
||||
<SiteHeader />
|
||||
|
||||
<main className="flex-1 container py-8 px-4 md:px-6">
|
||||
<main className="flex-1 container mx-auto py-8 px-4 md:px-6">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-serif font-bold text-foreground">李氏家族族谱</h1>
|
||||
|
||||
+10
-18
@@ -9,23 +9,14 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { useRef, useState } from "react"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { treeData, addMember } = useFamily() // Actually need setTreeData but I didn't expose it directly. I should have.
|
||||
// Wait, I didn't expose setTreeData in the context interface.
|
||||
// I need to update the context to allow full data replacement or add a "loadData" function.
|
||||
// Let me quickly check context/family-context.tsx
|
||||
// ... It has 'addMember', 'updateMember', 'deleteMember'. No 'loadData'.
|
||||
// I will assume I can update the context file first.
|
||||
|
||||
const { treeData, loadData, resetData } = useFamily()
|
||||
const [importStatus, setImportStatus] = useState<string>("")
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Since I can't easily modify the context interface without a separate edit step which I should do properly,
|
||||
// I will modify the context file in this same block to add 'loadData'.
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col font-sans">
|
||||
<SiteHeader />
|
||||
<main className="container py-8 px-4 md:px-6 flex-1 max-w-3xl">
|
||||
<main className="container mx-auto py-8 px-4 md:px-6 flex-1 max-w-3xl">
|
||||
<h1 className="text-3xl font-serif font-bold mb-8">系统设置 (Settings)</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -80,19 +71,19 @@ export default function SettingsPage() {
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
reader.onload = async (event) => {
|
||||
try {
|
||||
const json = JSON.parse(event.target?.result as string)
|
||||
if (json.members && json.rootId) {
|
||||
// Ideally call loadData(json) here
|
||||
// Since I'm in the component, I'll dispatch a custom event or use the context method I'm about to add
|
||||
const event = new CustomEvent("family-data-import", { detail: json })
|
||||
window.dispatchEvent(event)
|
||||
await loadData(json)
|
||||
setImportStatus("导入成功!")
|
||||
// Clear input so same file can be selected again if needed
|
||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||
} else {
|
||||
setImportStatus("错误:无效的数据格式")
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setImportStatus("错误:无法解析文件")
|
||||
}
|
||||
}
|
||||
@@ -117,9 +108,10 @@ export default function SettingsPage() {
|
||||
<CardContent>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
if (confirm("确定要重置吗?所有更改将丢失。")) {
|
||||
window.location.reload() // Simple way to reset state since it's in-memory
|
||||
await resetData()
|
||||
alert("数据已重置为演示状态。")
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function TimelinePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col font-sans">
|
||||
<SiteHeader />
|
||||
<main className="container py-8 px-4 md:px-6 flex-1">
|
||||
<main className="container mx-auto py-8 px-4 md:px-6 flex-1">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-serif font-bold mb-2">家族历史 (History)</h1>
|
||||
<p className="text-muted-foreground">纵览家族发展的时间脉络与迁徙足迹。</p>
|
||||
@@ -89,9 +89,8 @@ export default function TimelinePage() {
|
||||
<div key={idx} className="relative pl-8 md:pl-12 group">
|
||||
{/* Dot */}
|
||||
<div
|
||||
className={`absolute -left-[9px] top-1 h-4 w-4 rounded-full border-2 border-background transition-colors group-hover:scale-110 ${
|
||||
event.type === "birth" ? "bg-primary" : "bg-neutral-400"
|
||||
}`}
|
||||
className={`absolute -left-[9px] top-1 h-4 w-4 rounded-full border-2 border-background transition-colors group-hover:scale-110 ${event.type === "birth" ? "bg-primary" : "bg-neutral-400"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Year Label (Mobile vs Desktop) */}
|
||||
@@ -103,9 +102,8 @@ export default function TimelinePage() {
|
||||
<div className="bg-card border border-border rounded-lg p-4 shadow-sm transition-all hover:shadow-md hover:border-primary/50 max-w-xl">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span
|
||||
className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
event.type === "birth" ? "bg-primary/10 text-primary" : "bg-neutral-100 text-neutral-500"
|
||||
}`}
|
||||
className={`text-xs font-bold px-2 py-0.5 rounded-full ${event.type === "birth" ? "bg-primary/10 text-primary" : "bg-neutral-100 text-neutral-500"
|
||||
}`}
|
||||
>
|
||||
{event.type === "birth" ? "诞生" : "逝世"}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user