0.0.0.4
This commit is contained in:
+148
-22
@@ -7,10 +7,12 @@ import { useFamily } from "@/context/family-context"
|
||||
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 { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import { db } from "@/lib/db"
|
||||
import { PhotoGallery } from "@/components/members/photo-gallery"
|
||||
import { StoryManager } from "@/components/members/story-manager"
|
||||
|
||||
export default function MemberProfilePage() {
|
||||
const { id } = useParams()
|
||||
@@ -85,14 +87,18 @@ export default function MemberProfilePage() {
|
||||
<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">
|
||||
{avatarBlobUrl || member.avatarUrl ? (
|
||||
{avatarBlobUrl ? (
|
||||
<img
|
||||
src={avatarBlobUrl || member.avatarUrl || "/placeholder.svg"}
|
||||
src={avatarBlobUrl}
|
||||
alt={member.fullName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-4xl font-serif font-bold text-muted-foreground/50">{member.surname}</span>
|
||||
member.gender === 'female' ? (
|
||||
<CircleUserRound className="h-20 w-20 text-pink-400" />
|
||||
) : (
|
||||
<CircleUser className="h-20 w-20 text-blue-400" />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
@@ -152,6 +158,40 @@ export default function MemberProfilePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Photo Gallery */}
|
||||
{member.photoIds && member.photoIds.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<Images className="h-6 w-6 text-primary" />
|
||||
照片墙
|
||||
</h2>
|
||||
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
|
||||
<PhotoGallery
|
||||
photoIds={member.photoIds}
|
||||
onChange={(photoIds) => updateMember(member.id, { photoIds })}
|
||||
readonly={false}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Family Stories */}
|
||||
{member.stories && member.stories.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<BookOpen className="h-6 w-6 text-primary" />
|
||||
家族故事
|
||||
</h2>
|
||||
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
|
||||
<StoryManager
|
||||
stories={member.stories}
|
||||
onChange={(stories) => updateMember(member.id, { stories })}
|
||||
readonly={false}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-1 h-6 bg-secondary rounded-full"></span>
|
||||
@@ -167,12 +207,47 @@ export default function MemberProfilePage() {
|
||||
href={`/members/${member.fatherId}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
查看父亲资料
|
||||
{getMember(member.fatherId)?.fullName || member.fatherId}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
母亲 (Mother)
|
||||
</h3>
|
||||
{member.motherId ? (
|
||||
<Link
|
||||
href={`/members/${member.motherId}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
{getMember(member.motherId)?.fullName || member.motherId}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
配偶 (Spouse)
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.spouseIds?.length > 0 ? (
|
||||
member.spouseIds.map((spouseId) => (
|
||||
<Link
|
||||
href={`/members/${spouseId}`}
|
||||
key={spouseId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
{getMember(spouseId)?.fullName || spouseId}
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg border border-border">
|
||||
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
|
||||
子女 (Children)
|
||||
@@ -185,7 +260,7 @@ export default function MemberProfilePage() {
|
||||
key={childId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
{childId} {/* In real app, fetch name */}
|
||||
{getMember(childId)?.fullName || childId}
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
@@ -206,24 +281,75 @@ export default function MemberProfilePage() {
|
||||
<dt className="text-muted-foreground">出生日期</dt>
|
||||
<dd className="font-medium">{member.birthDate || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">逝世日期</dt>
|
||||
<dd className="font-medium">{member.deathDate || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">字辈</dt>
|
||||
<dd className="font-medium">{member.generationName || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">谥号</dt>
|
||||
<dd className="font-medium">{member.posthumousName || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">墓地</dt>
|
||||
<dd className="font-medium">{member.burialPlace || "-"}</dd>
|
||||
</div>
|
||||
{member.deathDate && (
|
||||
<>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">逝世日期</dt>
|
||||
<dd className="font-medium">{member.deathDate}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">谥号</dt>
|
||||
<dd className="font-medium">{member.posthumousName || "-"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground">墓地</dt>
|
||||
<dd className="font-medium">{member.burialPlace || "-"}</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{member.generationName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">字辈</dt>
|
||||
<dd className="font-medium">{member.generationName}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* 联系方式 */}
|
||||
{(member.phone || member.telephone || member.email || member.address) && (
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm">
|
||||
<h3 className="font-serif font-bold text-lg mb-4 border-b border-border pb-2">联系方式</h3>
|
||||
<dl className="space-y-4 text-sm">
|
||||
{member.phone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">手机号</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`tel:${member.phone}`} className="hover:text-primary">
|
||||
{member.phone}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.telephone && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">固定电话</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`tel:${member.telephone}`} className="hover:text-primary">
|
||||
{member.telephone}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.email && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">邮箱</dt>
|
||||
<dd className="font-medium">
|
||||
<a href={`mailto:${member.email}`} className="hover:text-primary">
|
||||
{member.email}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.address && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground">现居地址</dt>
|
||||
<dd className="font-medium">{member.address}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+115
-58
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useMemo } from "react"
|
||||
import { useState, useMemo, useEffect } from "react"
|
||||
import { SiteHeader } from "@/components/site-header"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
@@ -16,9 +17,19 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
|
||||
export default function MembersPage() {
|
||||
const { treeData, searchMembers } = useFamily()
|
||||
const { treeData, searchMembers, setHighlightedMemberId } = useFamily()
|
||||
const router = useRouter()
|
||||
const [query, setQuery] = useState("")
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
|
||||
// 从 URL 参数读取搜索关键词
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const searchParam = params.get('search')
|
||||
if (searchParam) {
|
||||
setQuery(searchParam)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Filter States
|
||||
const [selectedSurname, setSelectedSurname] = useState<string>("all")
|
||||
@@ -100,6 +111,27 @@ export default function MembersPage() {
|
||||
// Sort
|
||||
return result.sort((a, b) => a.generation - b.generation || (a.birthDate || "").localeCompare(b.birthDate || ""))
|
||||
}, [allMembers, query, selectedSurname, selectedHome, statusFilter, generationRange])
|
||||
|
||||
// 按世代分组
|
||||
const membersByGeneration = useMemo(() => {
|
||||
const groups = new Map<number, typeof filteredMembers>()
|
||||
|
||||
filteredMembers.forEach(member => {
|
||||
const gen = member.generation
|
||||
if (!groups.has(gen)) {
|
||||
groups.set(gen, [])
|
||||
}
|
||||
groups.get(gen)!.push(member)
|
||||
})
|
||||
|
||||
// 转换为数组并排序
|
||||
return Array.from(groups.entries())
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([generation, members]) => ({
|
||||
generation,
|
||||
members
|
||||
}))
|
||||
}, [filteredMembers])
|
||||
|
||||
const clearFilters = () => {
|
||||
setSelectedSurname("all")
|
||||
@@ -233,62 +265,87 @@ export default function MembersPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredMembers.length === 0 ? (
|
||||
<div className="col-span-full text-center py-12 text-muted-foreground">
|
||||
没有找到匹配的成员。
|
||||
</div>
|
||||
) : (
|
||||
filteredMembers.map((member) => (
|
||||
<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">
|
||||
<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">
|
||||
{member.fullName}
|
||||
{member.courtesyName && (
|
||||
<span className="text-sm font-normal text-muted-foreground ml-2">
|
||||
字 {member.courtesyName}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded text-muted-foreground whitespace-nowrap">
|
||||
{member.generation}世
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1 truncate">
|
||||
{member.generationName ? `字辈: ${member.generationName}` : "未录入字辈"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{member.ancestralHome || "籍贯未知"}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{member.deathDate && (
|
||||
<span className="text-[10px] bg-neutral-100 text-neutral-600 px-1.5 py-0.5 rounded border border-neutral-200">
|
||||
已故
|
||||
</span>
|
||||
)}
|
||||
{member.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{filteredMembers.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
没有找到匹配的成员。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{membersByGeneration.map(({ generation, members }) => (
|
||||
<div key={generation}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<h2 className="text-xl font-serif font-bold text-foreground">第 {generation} 世</h2>
|
||||
<span className="text-sm text-muted-foreground">共 {members.length} 人</span>
|
||||
<div className="flex-1 h-px bg-border"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{members.map((member) => (
|
||||
<div key={member.id} className="relative group">
|
||||
<Link href={`/members/${member.id}`}>
|
||||
<Card className="hover:border-primary transition-colors cursor-pointer h-full">
|
||||
<CardContent className="p-3 flex items-start gap-2.5">
|
||||
<AvatarDisplay
|
||||
imageId={member.avatarImageId}
|
||||
fallbackUrl={member.avatarUrl}
|
||||
fallbackText={member.surname}
|
||||
gender={member.gender}
|
||||
className="h-10 w-10 border border-border"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline justify-between gap-1.5">
|
||||
<h3 className="text-base font-bold font-serif truncate">
|
||||
{member.fullName}
|
||||
{member.courtesyName && (
|
||||
<span className="text-[11px] font-normal text-muted-foreground ml-1">
|
||||
字 {member.courtesyName}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground mt-0.5 truncate">
|
||||
{member.generationName ? `字辈: ${member.generationName}` : "未录入字辈"}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground truncate">{member.ancestralHome || "籍贯未知"}</p>
|
||||
{(member.deathDate || member.tags?.length) && (
|
||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||
{member.deathDate && (
|
||||
<span className="text-[9px] bg-neutral-100 text-neutral-600 px-1 py-0.5 rounded border border-neutral-200">
|
||||
已故
|
||||
</span>
|
||||
)}
|
||||
{member.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-[9px] bg-primary/10 text-primary px-1 py-0.5 rounded border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6 p-0"
|
||||
onClick={() => {
|
||||
setHighlightedMemberId(member.id)
|
||||
router.push('/tree')
|
||||
}}
|
||||
title="在族谱图中定位"
|
||||
>
|
||||
<Search className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user