This commit is contained in:
freedakgmail
2025-11-24 14:02:34 +08:00
parent b7a8c9ee6e
commit 3d075c6076
941 changed files with 25613 additions and 27641 deletions
+426 -199
View File
@@ -7,29 +7,59 @@ 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, CircleUser, CircleUserRound, Images, BookOpen, UserPlus } from "lucide-react"
import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus, Camera } from "lucide-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"
import { useDialog } from "@/components/ui/alert-dialog-custom"
import { permissions } from "@/lib/permissions"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { MemberVersionHistory } from "@/components/members/member-version-history"
export default function MemberProfilePage() {
const { id } = useParams()
const { treeData, currentTree, getMember, updateMember, deleteMember } = useFamily()
const { treeData, currentTree, getMember, updateMember, deleteMember, isLoading } = useFamily()
const role = currentTree?.currentUserRole
const { showConfirm } = useDialog()
const router = useRouter()
const [isEditing, setIsEditing] = useState(false)
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | undefined>(undefined)
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
const member = getMember(id as string)
const allMembers = Object.values(treeData.members)
// 始祖专用边框样式
const founderBorderClass = member?.isFounder ? "border-2 border-amber-400 shadow-md" : "border border-border"
// 计算年龄或享年
const calculateAge = () => {
if (!member?.birthDate) return null
const birthYear = new Date(member.birthDate).getFullYear()
if (member.deathDate) {
// 已故,计算享年
const deathYear = new Date(member.deathDate).getFullYear()
return { age: deathYear - birthYear, type: 'deceased' }
} else {
// 在世,计算年龄
const currentYear = new Date().getFullYear()
return { age: currentYear - birthYear, type: 'living' }
}
}
const ageInfo = calculateAge()
// Load avatar blob if exists
// Load avatar
useEffect(() => {
if (member?.avatarImageId) {
// 优先使用 avatarUrl(服务器存储)
if (member?.avatarUrl) {
setAvatarBlobUrl(member.avatarUrl)
}
// 兼容旧的 IndexedDB 存储
else if (member?.avatarImageId) {
db.images.get(member.avatarImageId).then((image) => {
if (image) {
const url = URL.createObjectURL(image.blob)
@@ -40,15 +70,28 @@ export default function MemberProfilePage() {
} else {
setAvatarBlobUrl(undefined)
}
}, [member?.avatarImageId])
}, [member?.avatarUrl, member?.avatarImageId])
// 显示加载状态
if (isLoading) {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<div className="flex-1 flex flex-col items-center justify-center gap-4">
<p className="text-muted-foreground">...</p>
</div>
</div>
)
}
// 成员未找到
if (!member) {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<div className="flex-1 flex flex-col items-center justify-center gap-4">
<p className="text-muted-foreground">Member not found</p>
<Button onClick={() => router.push("/members")}>Back to List</Button>
<p className="text-muted-foreground"></p>
<Button onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}></Button>
</div>
</div>
)
@@ -63,13 +106,14 @@ export default function MemberProfilePage() {
const confirmed = await showConfirm("确定要删除这位成员吗?这将同时删除其关联关系。", "删除成员", "destructive")
if (confirmed) {
deleteMember(member.id)
router.push("/members")
const params = new URLSearchParams(window.location.search)
router.push(`/members?${params.toString()}`)
}
}
const handleQuickAddChild = () => {
// 跳转到新增成员页面,并预填父母信息
const params = new URLSearchParams()
const params = new URLSearchParams(window.location.search)
if (member.gender === 'MALE') {
params.set('fatherId', member.id)
} else {
@@ -81,7 +125,7 @@ export default function MemberProfilePage() {
const handleQuickAddSpouse = () => {
// 跳转到新增成员页面,并预填配偶信息
const params = new URLSearchParams()
const params = new URLSearchParams(window.location.search)
params.set('spouseId', member.id)
params.set('generation', member.generation.toString())
// 配偶性别与当前成员相反
@@ -91,7 +135,7 @@ export default function MemberProfilePage() {
const handleQuickAddFather = () => {
// 跳转到新增成员页面,并预填父亲信息
const params = new URLSearchParams()
const params = new URLSearchParams(window.location.search)
params.set('childId', member.id)
params.set('generation', (member.generation - 1).toString())
params.set('gender', 'MALE')
@@ -102,9 +146,47 @@ export default function MemberProfilePage() {
router.push(`/members/new?${params.toString()}`)
}
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
try {
setIsUploadingAvatar(true)
// 上传到服务器
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || '上传失败')
}
const { url } = await response.json()
// 更新成员头像 URL
await updateMember(member.id, {
avatarUrl: url,
})
// 显示新头像
setAvatarBlobUrl(url)
} catch (error) {
console.error('上传头像失败:', error)
alert('上传头像失败: ' + (error instanceof Error ? error.message : '未知错误'))
} finally {
setIsUploadingAvatar(false)
}
}
const handleQuickAddMother = () => {
// 跳转到新增成员页面,并预填母亲信息
const params = new URLSearchParams()
const params = new URLSearchParams(window.location.search)
params.set('childId', member.id)
params.set('generation', (member.generation - 1).toString())
params.set('gender', 'FEMALE')
@@ -120,17 +202,47 @@ export default function MemberProfilePage() {
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<div className="container mx-auto py-8">
<div className="mb-6 flex items-center gap-2">
<Button variant="ghost" onClick={() => setIsEditing(false)}>
<div className="mb-6">
<Button variant="ghost" onClick={() => setIsEditing(false)} className="mb-4">
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold font-serif">: {member.fullName}</h1>
{member.isFounder && (
<div className="mb-6 p-6 bg-gradient-to-br from-amber-50 via-orange-50 to-amber-50 border-2 border-amber-400 rounded-xl shadow-md">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
<span className="text-3xl">👑</span>
</div>
<div className="flex-1">
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
</h2>
<p className="text-base text-amber-800 leading-relaxed font-serif">
<span className="font-bold text-amber-900"></span>
<span className="font-bold text-amber-900"></span>
<br />
</p>
</div>
</div>
</div>
)}
<h1 className="text-3xl font-serif font-bold mb-2">
{member.isFounder ? '编辑家族始祖' : `编辑成员: ${member.fullName}`}
</h1>
<p className="text-muted-foreground">
{member.isFounder
? '修改始祖信息将影响整个家族树'
: '更新成员信息'}
</p>
</div>
<MemberForm
initialData={member}
existingMembers={allMembers}
onSubmit={handleSave}
onCancel={() => setIsEditing(false)}
onCancel={() => setIsEditing(false)}
isFounder={member.isFounder}
/>
</div>
</div>
@@ -141,32 +253,92 @@ export default function MemberProfilePage() {
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6">
{/* 始祖特殊提示框 */}
{member.isFounder && (
<div className="mb-6 p-6 bg-gradient-to-br from-amber-50 via-orange-50 to-amber-50 border-2 border-amber-400 rounded-xl shadow-md">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-14 h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
<span className="text-3xl">👑</span>
</div>
<div className="flex-1">
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
</h2>
<p className="text-base text-amber-800 leading-relaxed font-serif">
<span className="font-bold text-amber-900"></span>
<span className="font-bold text-amber-900"></span>
<br />
</p>
</div>
</div>
</div>
)}
{/* 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">
{avatarBlobUrl ? (
<img
src={avatarBlobUrl}
alt={member.fullName}
className="h-full w-full object-cover"
/>
) : (
member.gender === 'FEMALE' ? (
<CircleUserRound className="h-20 w-20 text-pink-400" />
<div className="relative group">
<div className="h-32 w-32 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
{avatarBlobUrl ? (
<img
src={avatarBlobUrl}
alt={member.fullName}
className="h-full w-full object-cover"
/>
) : (
<CircleUser className="h-20 w-20 text-blue-400" />
)
member.gender === 'FEMALE' ? (
<CircleUserRound className="h-20 w-20 text-pink-400" />
) : (
<CircleUser className="h-20 w-20 text-blue-400" />
)
)}
</div>
{permissions.canUpdate(role) && (
<label className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer rounded-lg">
<Camera className="h-8 w-8 text-white" />
<input
type="file"
accept="image/*"
className="hidden"
onChange={handleAvatarUpload}
disabled={isUploadingAvatar}
/>
</label>
)}
{isUploadingAvatar && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-lg">
<div className="text-white text-xs">...</div>
</div>
)}
</div>
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-4xl font-bold font-serif text-foreground">{member.fullName}</h1>
<span className="px-2 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full border border-primary/20">
{member.generation}
</span>
<h1 className="text-4xl font-bold font-serif text-foreground flex items-center gap-2">
{member.isFounder && <span className="text-amber-600 text-3xl">👑</span>}
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
/>
</h1>
{member.isFounder ? (
<span className="px-3 py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-sm font-semibold rounded-full border-2 border-amber-300 shadow-sm">
· {member.generation}
</span>
) : (
<span className="px-2 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full border border-primary/20">
{member.generation}
</span>
)}
</div>
<div className="space-y-1 text-muted-foreground">
{member.isFounder && (
<p className="text-amber-700 font-semibold text-sm flex items-center gap-1">
<span></span>
<span className="text-amber-600"></span>
<span></span>
</p>
)}
{member.courtesyName && (
<p>
<span className="text-foreground">{member.courtesyName}</span>
@@ -197,12 +369,12 @@ export default function MemberProfilePage() {
<div className="flex gap-2 flex-wrap">
{permissions.canCreate(role) && (
<>
{!member.fatherId && (
{!member.isFounder && !member.fatherId && (
<Button variant="outline" onClick={handleQuickAddFather} className="gap-2">
<UserPlus className="h-4 w-4" />
</Button>
)}
{!member.motherId && (
{!member.isFounder && !member.motherId && (
<Button variant="outline" onClick={handleQuickAddMother} className="gap-2">
<UserPlus className="h-4 w-4" />
</Button>
@@ -236,14 +408,222 @@ export default function MemberProfilePage() {
{/* Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Biography */}
{/* Left Column - Details and Biography */}
<div className="lg:col-span-2 space-y-8">
{/* 家族关系 */}
<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>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
</h3>
{member.fatherId ? (
<Link
href={`/members/${member.fatherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
className="text-lg font-serif font-bold hover:underline text-primary"
>
<MemberNameWithStatus
name={getMember(member.fatherId)?.fullName || member.fatherId}
isDead={!!getMember(member.fatherId)?.deathDate}
/>
</Link>
) : member.spouseFatherName ? (
<span className="text-lg font-serif font-bold text-foreground">
{member.spouseFatherName}
</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</div>
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
</h3>
{member.motherId ? (
<Link
href={`/members/${member.motherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
className="text-lg font-serif font-bold hover:underline text-primary"
>
<MemberNameWithStatus
name={getMember(member.motherId)?.fullName || member.motherId}
isDead={!!getMember(member.motherId)?.deathDate}
/>
</Link>
) : member.spouseMotherName ? (
<span className="text-lg font-serif font-bold text-foreground">
{member.spouseMotherName}
</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</div>
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
</h3>
<div className="flex flex-wrap gap-2">
{member.spouseIds?.length > 0 ? (
member.spouseIds.map((spouseId) => (
<Link
href={`/members/${spouseId}${typeof window !== 'undefined' ? window.location.search : ''}`}
key={spouseId}
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
>
<MemberNameWithStatus
name={getMember(spouseId)?.fullName || spouseId}
isDead={!!getMember(spouseId)?.deathDate}
/>
</Link>
))
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
</h3>
<div className="flex flex-wrap gap-2">
{member.childrenIds?.length > 0 ? (
member.childrenIds.map((childId) => (
<Link
href={`/members/${childId}${typeof window !== 'undefined' ? window.location.search : ''}`}
key={childId}
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
>
<MemberNameWithStatus
name={getMember(childId)?.fullName || childId}
isDead={!!getMember(childId)?.deathDate}
/>
</Link>
))
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
</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>
</h2>
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}>
<dl className="grid grid-cols-2 gap-x-8 gap-y-4 text-sm">
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium flex items-center gap-2">
{member.birthDate || "-"}
{member.birthDate && member.isLunarDate && (
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
</span>
)}
</dd>
</div>
{ageInfo && (
<div>
<dt className="text-muted-foreground mb-1">{ageInfo.type === 'deceased' ? '享年' : '年龄'}</dt>
<dd className="font-medium text-primary">{ageInfo.age} </dd>
</div>
)}
{member.deathDate && (
<>
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium flex items-center gap-2">
{member.deathDate}
{member.isLunarDate && (
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
</span>
)}
</dd>
</div>
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.posthumousName || "-"}</dd>
</div>
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.burialPlace || "-"}</dd>
</div>
</>
)}
{member.generationName && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.generationName}</dd>
</div>
)}
</dl>
</div>
</section>
{/* 联系方式 */}
{(member.phone || member.telephone || member.email || member.address) && (
<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>
</h2>
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}>
<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>
</section>
)}
<section>
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
<span className="w-1 h-6 bg-primary rounded-full"></span>
</h2>
<div className="prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg border border-border shadow-sm">
<div className={`prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm`}>
{member.bio ? (
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
) : (
@@ -259,7 +639,7 @@ export default function MemberProfilePage() {
<Images className="h-6 w-6 text-primary" />
</h2>
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm`}>
<PhotoGallery
photoIds={member.photoIds}
onChange={(photoIds) => updateMember(member.id, { photoIds })}
@@ -276,7 +656,7 @@ export default function MemberProfilePage() {
<BookOpen className="h-6 w-6 text-primary" />
</h2>
<div className="bg-card p-6 rounded-lg border border-border shadow-sm">
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm`}>
<StoryManager
stories={member.stories}
onChange={(stories) => updateMember(member.id, { stories })}
@@ -285,174 +665,21 @@ export default function MemberProfilePage() {
</div>
</section>
)}
</div>
{/* Right Column - Version History */}
<div className="space-y-8">
<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>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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">
(Father)
</h3>
{member.fatherId ? (
<Link
href={`/members/${member.fatherId}`}
className="text-lg font-serif font-bold hover:underline text-primary"
>
{getMember(member.fatherId)?.fullName || member.fatherId}
</Link>
) : member.spouseFatherName ? (
<span className="text-lg font-serif font-bold text-foreground">
{member.spouseFatherName}
</span>
) : (
<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>
) : member.spouseMotherName ? (
<span className="text-lg font-serif font-bold text-foreground">
{member.spouseMotherName}
</span>
) : (
<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)
</h3>
<div className="flex flex-wrap gap-2">
{member.childrenIds?.length > 0 ? (
member.childrenIds.map((childId) => (
<Link
href={`/members/${childId}`}
key={childId}
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
>
{getMember(childId)?.fullName || childId}
</Link>
))
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
</div>
<MemberVersionHistory
memberId={member.id}
treeId={currentTree?.id || ''}
/>
</section>
</div>
{/* Sidebar Info */}
<div className="space-y-6">
<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">
<div>
<dt className="text-muted-foreground"></dt>
<dd className="font-medium">{member.birthDate || "-"}</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>
</div>