This commit is contained in:
freedakgmail
2025-12-10 15:21:27 +08:00
parent e7167f4340
commit 1ae59960e8
148 changed files with 681 additions and 477 deletions
+6
View File
@@ -16,6 +16,12 @@ export const metadata: Metadata = {
description: "现代化的中国家族树管理系统,支持多用户协作、权限管理、数据导入导出等功能",
generator: "v0.app",
manifest: "/manifest.json",
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
appleWebApp: {
capable: true,
statusBarStyle: "default",
+65 -76
View File
@@ -273,17 +273,17 @@ export default function MemberProfilePage() {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6">
<Tabs value={editActiveTab} onValueChange={setEditActiveTab} className="flex flex-col h-[calc(100vh-8rem)]">
<main className="container mx-auto py-4 md:py-8 px-3 md:px-6">
<Tabs value={editActiveTab} onValueChange={setEditActiveTab} className="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-8rem)]">
{/* 固定头部区域 */}
<div className="flex-shrink-0 pb-4 border-b border-border">
{/* 返回按钮 */}
<BackButton onClick={() => setIsEditing(false)} label="返回" />
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
<div className="flex items-start gap-6">
<div className="h-24 w-24 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
<div className="flex flex-col md:flex-row justify-between items-start gap-3 md:gap-6 pb-3 md:pb-4">
<div className="flex items-start gap-3 md:gap-6">
<div className="h-16 w-16 md:h-24 md:w-24 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden flex-shrink-0">
{member.avatarUrl ? (
<img
src={member.avatarUrl}
@@ -292,15 +292,15 @@ export default function MemberProfilePage() {
/>
) : (
member.gender === 'FEMALE' ? (
<CircleUserRound className="h-16 w-16 text-pink-400" />
<CircleUserRound className="h-10 w-10 md:h-16 md:w-16 text-pink-400" />
) : (
<CircleUser className="h-16 w-16 text-blue-400" />
<CircleUser className="h-10 w-10 md:h-16 md:w-16 text-blue-400" />
)
)}
</div>
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold font-serif text-foreground flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 md:gap-3 mb-1 md:mb-2 flex-wrap">
<h1 className="text-xl md:text-3xl font-bold font-serif text-foreground flex items-center gap-1 md:gap-2">
{member.isFounder && <span className="text-amber-600 text-2xl">👑</span>}
<MemberNameWithStatus
name={member.fullName}
@@ -308,16 +308,16 @@ export default function MemberProfilePage() {
/>
</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">
<span className="px-2 md:px-3 py-0.5 md:py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-xs md: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">
<span className="px-1.5 md:px-2 py-0.5 md:py-1 bg-primary/10 text-primary text-xs md:text-sm font-medium rounded-full border border-primary/20">
{member.generation}
</span>
)}
</div>
<p className="text-muted-foreground">
<p className="text-sm md:text-base text-muted-foreground">
{member.isFounder
? '修改始祖信息将影响整个家族树'
: '更新成员信息'}
@@ -345,11 +345,11 @@ export default function MemberProfilePage() {
</div>
{/* Tab 导航栏 */}
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
<TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger>
<TabsList className="grid grid-cols-4 md:flex md:flex-wrap gap-1 md:gap-2 bg-muted/70 p-1 w-full md:w-fit h-auto">
<TabsTrigger value="info" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="details" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="photos" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="stories" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
</TabsList>
{/* 始祖特殊提示框 */}
@@ -396,10 +396,10 @@ export default function MemberProfilePage() {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6">
<Tabs defaultValue="info" className="flex flex-col h-[calc(100vh-8rem)]">
<main className="container mx-auto py-4 md:py-8 px-3 md:px-6">
<Tabs defaultValue="info" className="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-8rem)]">
{/* 固定头部区域 */}
<div className="flex-shrink-0 pb-4 border-b border-border">
<div className="flex-shrink-0 pb-3 md:pb-4 border-b border-border">
{/* 返回按钮 */}
<BackButton
onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}
@@ -407,10 +407,10 @@ export default function MemberProfilePage() {
/>
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
<div className="flex items-start gap-6">
<div className="relative group">
<div className="h-24 w-24 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
<div className="flex flex-col md:flex-row justify-between items-start gap-3 md:gap-6 pb-3 md:pb-4">
<div className="flex items-start gap-3 md:gap-6">
<div className="relative group flex-shrink-0">
<div className="h-16 w-16 md:h-24 md:w-24 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
{avatarBlobUrl ? (
<img
src={avatarBlobUrl}
@@ -419,15 +419,15 @@ export default function MemberProfilePage() {
/>
) : (
member.gender === 'FEMALE' ? (
<CircleUserRound className="h-16 w-16 text-pink-400" />
<CircleUserRound className="h-10 w-10 md:h-16 md:w-16 text-pink-400" />
) : (
<CircleUser className="h-16 w-16 text-blue-400" />
<CircleUser className="h-10 w-10 md:h-16 md:w-16 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-6 w-6 text-white" />
<Camera className="h-5 w-5 md:h-6 md:w-6 text-white" />
<input
type="file"
accept="image/*"
@@ -443,10 +443,10 @@ export default function MemberProfilePage() {
</div>
)}
</div>
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold font-serif text-foreground flex items-center gap-2">
{member.isFounder && <span className="text-amber-600 text-2xl">👑</span>}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 md:gap-3 mb-1 md:mb-2 flex-wrap">
<h1 className="text-xl md:text-3xl font-bold font-serif text-foreground flex items-center gap-1 md:gap-2">
{member.isFounder && <span className="text-amber-600 text-lg md:text-2xl">👑</span>}
<MemberNameWithStatus
name={member.fullName}
isDead={!!member.deathDate}
@@ -455,16 +455,16 @@ export default function MemberProfilePage() {
/>
</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">
<span className="px-2 md:px-3 py-0.5 md:py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-xs md: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">
<span className="px-1.5 md:px-2 py-0.5 md:py-1 bg-primary/10 text-primary text-xs md:text-sm font-medium rounded-full border border-primary/20">
{member.generation}
</span>
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
<div className="flex flex-wrap gap-x-2 md:gap-x-4 gap-y-1 text-xs md:text-sm text-muted-foreground">
{member.courtesyName && <span>{member.courtesyName}</span>}
{member.artName && <span>{member.artName}</span>}
{member.generationName && <span>{member.generationName}</span>}
@@ -473,71 +473,60 @@ export default function MemberProfilePage() {
</div>
</div>
<div className="flex gap-2 flex-wrap">
<div className="flex gap-1.5 md:gap-2 flex-wrap">
{permissions.canCreate(role) && (
<>
{!member.isFounder && !member.fatherId && (
<Button variant="outline" size="sm" onClick={handleQuickAddFather} className="gap-1">
<UserPlus className="h-4 w-4" />
</Button>
)}
{!member.isFounder && !member.motherId && (
<Button variant="outline" size="sm" onClick={handleQuickAddMother} className="gap-1">
<UserPlus className="h-4 w-4" />
</Button>
)}
<Button variant="default" size="sm" onClick={handleQuickAddChild} className="gap-1">
<UserPlus className="h-4 w-4" />
<Button variant="default" size="sm" onClick={handleQuickAddChild} className="gap-1 text-xs md:text-sm h-7 md:h-8 px-2 md:px-3">
<UserPlus className="h-3 w-3 md:h-4 md:w-4" /> <span className="hidden sm:inline"></span>
</Button>
<Button variant="secondary" size="sm" onClick={handleQuickAddSpouse} className="gap-1">
<UserPlus className="h-4 w-4" />
<Button variant="secondary" size="sm" onClick={handleQuickAddSpouse} className="gap-1 text-xs md:text-sm h-7 md:h-8 px-2 md:px-3">
<UserPlus className="h-3 w-3 md:h-4 md:w-4" /> <span className="hidden sm:inline"></span>
</Button>
</>
)}
<Button variant="outline" size="icon" onClick={() => window.print()} title="打印">
<Printer className="h-4 w-4" />
<Button variant="outline" size="icon" onClick={() => window.print()} title="打印" className="h-7 w-7 md:h-8 md:w-8">
<Printer className="h-3 w-3 md:h-4 md:w-4" />
</Button>
<Button variant="outline" size="icon" title="分享">
<Share2 className="h-4 w-4" />
<Button variant="outline" size="icon" title="分享" className="h-7 w-7 md:h-8 md:w-8">
<Share2 className="h-3 w-3 md:h-4 md:w-4" />
</Button>
{permissions.canUpdate(role) && (
<Button variant="outline" size="sm" onClick={() => setIsEditing(true)} className="gap-1">
<Edit className="h-4 w-4" />
<Button variant="outline" size="sm" onClick={() => setIsEditing(true)} className="gap-1 text-xs md:text-sm h-7 md:h-8 px-2 md:px-3">
<Edit className="h-3 w-3 md:h-4 md:w-4" />
</Button>
)}
{permissions.canDeleteMember(role) && (
<Button variant="destructive" size="icon" onClick={handleDelete} title="删除">
<Trash className="h-4 w-4" />
<Button variant="destructive" size="icon" onClick={handleDelete} title="删除" className="h-7 w-7 md:h-8 md:w-8">
<Trash className="h-3 w-3 md:h-4 md:w-4" />
</Button>
)}
</div>
</div>
{/* Tab 导航栏 */}
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
<TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger>
<TabsTrigger value="history">线</TabsTrigger>
<TabsList className="grid grid-cols-5 md:flex md:flex-wrap gap-1 md:gap-2 bg-muted/70 p-1 w-full md:w-fit h-auto">
<TabsTrigger value="info" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="details" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="photos" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="stories" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="history" className="text-[11px] md:text-sm px-1.5 md:px-3 py-1.5">线</TabsTrigger>
</TabsList>
{/* 始祖特殊提示框 */}
{/* 始祖特殊提示框 - 移动端简化 */}
{member.isFounder && (
<div className="mt-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 className="mt-3 md:mt-6 p-3 md: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-2 md:gap-4">
<div className="flex-shrink-0 w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
<span className="text-xl md:text-3xl">👑</span>
</div>
<div className="flex-1">
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
<div className="flex-1 min-w-0">
<h2 className="text-lg md:text-2xl font-serif font-bold text-amber-900 mb-1 md:mb-3">
</h2>
<p className="text-base text-amber-800 leading-relaxed font-serif">
<p className="text-sm md: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 />
<span className="hidden md:inline"><br /></span>
</p>
</div>
</div>
@@ -548,10 +537,10 @@ export default function MemberProfilePage() {
{/* Tab 内容区域 - 可滚动 */}
<div className="flex-1 overflow-y-auto">
{/* 基本信息Tab - 包含基本信息和家庭关系 */}
<TabsContent value="info" className="mt-6 space-y-6">
<TabsContent value="info" className="mt-3 md:mt-6 space-y-4 md:space-y-6">
{/* 基本信息 */}
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<ChineseCardContent className="p-6">
<ChineseCardContent className="p-3 md:p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4"></ChineseCardTitle>
<dl className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-x-8 gap-y-4 text-sm">
<div>
+34 -35
View File
@@ -157,50 +157,50 @@ export default function NewMemberPage() {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col h-[calc(100vh-8rem)]">
<main className="container mx-auto py-4 md:py-8 px-3 md:px-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-8rem)]">
{/* 固定头部区域 */}
<div className="flex-shrink-0 pb-4 border-b border-border">
<div className="flex-shrink-0 pb-3 md:pb-4 border-b border-border">
{/* 返回按钮 */}
<BackButton onClick={() => router.back()} />
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
<div className="flex items-start gap-6">
<div className={`h-24 w-24 rounded-lg bg-muted flex items-center justify-center border-2 ${isFirstMember ? 'border-amber-400 bg-amber-50/30' : 'border-primary/20'} overflow-hidden`}>
<div className="flex flex-col md:flex-row justify-between items-start gap-3 md:gap-6 pb-3 md:pb-4">
<div className="flex items-start gap-3 md:gap-6">
<div className={`h-14 w-14 md:h-24 md:w-24 rounded-lg bg-muted flex items-center justify-center border-2 ${isFirstMember ? 'border-amber-400 bg-amber-50/30' : 'border-primary/20'} overflow-hidden flex-shrink-0`}>
{isFirstMember ? (
<span className="text-4xl">👑</span>
<span className="text-2xl md:text-4xl">👑</span>
) : (
<UserPlus className="h-16 w-16 text-muted-foreground" />
<UserPlus className="h-8 w-8 md:h-16 md:w-16 text-muted-foreground" />
)}
</div>
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold font-serif text-foreground flex items-center gap-2">
{isFirstMember && <span className="text-amber-600 text-2xl">👑</span>}
{isFirstMember ? '添加家族始祖' : `添加家族${memberType}`}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 md:gap-3 mb-1 md:mb-2 flex-wrap">
<h1 className="text-lg md:text-3xl font-bold font-serif text-foreground flex items-center gap-1 md:gap-2">
{isFirstMember && <span className="text-amber-600 text-lg md:text-2xl">👑</span>}
{isFirstMember ? '添加家族始祖' : `添加${memberType}`}
</h1>
{isFirstMember && (
<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">
<span className="px-2 md:px-3 py-0.5 md:py-1 bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 text-xs md:text-sm font-semibold rounded-full border-2 border-amber-300 shadow-sm">
· 1
</span>
)}
</div>
<p className="text-muted-foreground">
<p className="text-xs md:text-base text-muted-foreground">
{isFirstMember
? '请认真填写始祖信息,这将是家族历史的起点'
: `填写${memberType}信息,完善家族树`}
? '请认真填写始祖信息'
: `填写${memberType}信息`}
</p>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<Button type="button" variant="outline" onClick={() => router.back()}>
<div className="flex gap-1.5 md:gap-2 flex-wrap">
<Button type="button" variant="outline" onClick={() => router.back()} className="text-xs md:text-sm h-7 md:h-9 px-2 md:px-4">
</Button>
<Button
type="button"
className="bg-primary text-primary-foreground hover:bg-primary/90"
className="bg-primary text-primary-foreground hover:bg-primary/90 text-xs md:text-sm h-7 md:h-9 px-2 md:px-4"
onClick={() => {
const form = document.querySelector('form')
if (form) {
@@ -214,29 +214,28 @@ export default function NewMemberPage() {
</div>
{/* Tab 导航栏 */}
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
<TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger>
<TabsList className="grid grid-cols-4 md:flex md:flex-wrap gap-1 md:gap-2 bg-muted/70 p-1 w-full md:w-fit h-auto">
<TabsTrigger value="info" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="details" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="photos" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
<TabsTrigger value="stories" className="text-xs md:text-sm px-2 md:px-3 py-1.5"></TabsTrigger>
</TabsList>
{/* 始祖特殊提示框 */}
{/* 始祖特殊提示框 - 移动端简化 */}
{isFirstMember && (
<div className="mt-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 className="mt-3 md:mt-6 p-3 md: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-2 md:gap-4">
<div className="flex-shrink-0 w-10 h-10 md:w-14 md:h-14 bg-gradient-to-br from-amber-200 to-orange-200 rounded-full flex items-center justify-center shadow-sm">
<span className="text-xl md:text-3xl">👑</span>
</div>
<div className="flex-1">
<h2 className="text-2xl font-serif font-bold text-amber-900 mb-3">
<div className="flex-1 min-w-0">
<h2 className="text-lg md:text-2xl font-serif font-bold text-amber-900 mb-1 md:mb-3">
</h2>
<p className="text-base text-amber-800 leading-relaxed font-serif">
<p className="text-sm md: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 />
<span className="hidden md:inline"><br /></span>
</p>
</div>
</div>
+5 -13
View File
@@ -38,33 +38,25 @@ interface PlaceholderMember {
type MemberOrPlaceholder = FamilyMember | PlaceholderMember
// 提取占位符生成逻辑为独立函数
function addPlaceholdersToFamilyUnit(
// 提取配偶添加逻辑为独立函数(移除占位符,让卡片自然流动排列)
function addSpousesToFamilyUnit(
member: FamilyMember,
spouses: FamilyMember[],
processedSpouses: Set<string>
): MemberOrPlaceholder[] {
const result: MemberOrPlaceholder[] = [member]
let familyUnitSize = 1 // 主成员自己
// 添加配偶
// 添加配偶(紧跟在主成员后面)
if (member.spouseIds && member.spouseIds.length > 0) {
member.spouseIds.forEach(spouseId => {
const spouse = spouses.find(m => m.id === spouseId)
if (spouse && !processedSpouses.has(spouseId)) {
result.push(spouse)
processedSpouses.add(spouseId)
familyUnitSize++
}
})
}
// 计算需要添加的占位符数量,确保填满4的倍数
const placeholdersNeeded = (4 - (familyUnitSize % 4)) % 4
for (let i = 0; i < placeholdersNeeded; i++) {
result.push({ id: `placeholder-${member.id}-${i}`, isPlaceholder: true })
}
return result
}
@@ -679,12 +671,12 @@ export default function MembersPage() {
return aDate.localeCompare(bDate)
})
// 使用优化后的函数添加配偶和占位符
// 添加配偶(紧跟在主成员后面,不使用占位符
const result: MemberOrPlaceholder[] = []
const processedSpouses = new Set<string>()
sortedFamilyMembers.forEach(member => {
const familyUnit = addPlaceholdersToFamilyUnit(member, members, processedSpouses)
const familyUnit = addSpousesToFamilyUnit(member, members, processedSpouses)
result.push(...familyUnit)
})
+39 -39
View File
@@ -430,11 +430,11 @@ export default function DashboardPage() {
<div className="flex h-screen flex-col bg-background font-sans overflow-hidden">
<SiteHeader />
<main className="flex-1 container mx-auto py-8 px-4 md:px-6 flex flex-col overflow-hidden">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8 flex-shrink-0">
<main className="flex-1 container mx-auto py-4 md:py-8 px-3 md:px-6 flex flex-col overflow-y-auto overflow-x-hidden">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-4 mb-4 md:mb-8 flex-shrink-0">
<div>
<div className="flex items-center gap-3">
<h1 className="text-3xl font-serif font-bold text-foreground">
<div className="flex items-center gap-2 md:gap-3 flex-wrap">
<h1 className="text-2xl md:text-3xl font-serif font-bold text-foreground">
{currentTree?.name || '家族族谱'}
</h1>
{currentTree?.currentUserRole && (
@@ -470,7 +470,7 @@ export default function DashboardPage() {
</div>
</div>
<Collapsible open={statsExpanded} onOpenChange={setStatsExpanded} className="mb-8 flex-shrink-0">
<Collapsible open={statsExpanded} onOpenChange={setStatsExpanded} className="mb-4 md:mb-8 flex-shrink-0">
<div className="flex items-center justify-between mb-3">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2 text-muted-foreground hover:text-foreground -ml-2">
@@ -480,21 +480,21 @@ export default function DashboardPage() {
</CollapsibleTrigger>
</div>
<CollapsibleContent className="data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 md:gap-4">
{/* 第一个卡片:家族成员 + 在世/已故 */}
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}>
<ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
<ChineseCardContent className="p-5 pt-6">
<div className="flex items-center justify-between mb-3">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<ChineseCardContent className="p-4 md:p-5 pt-5 md:pt-6">
<div className="flex items-center justify-between mb-2 md:mb-3">
<p className="text-xs md:text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="chinese-seal chinese-seal-sm">
<Users className="h-4 w-4" />
</div>
</div>
<div className="flex flex-col space-y-1.5">
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{stats.totalMembers} </span>
<ChineseDivider variant="simple" className="my-2" />
<div className="flex items-center gap-3 text-sm text-muted-foreground/80 font-medium">
<div className="flex flex-col space-y-1">
<span className="text-2xl md:text-3xl font-serif font-bold text-foreground tracking-tight">{stats.totalMembers} </span>
<ChineseDivider variant="simple" className="my-1.5 md:my-2" />
<div className="flex items-center gap-2 md:gap-3 text-xs md:text-sm text-muted-foreground/80 font-medium">
<span className="text-green-600"> {stats.livingMembers}</span>
<span className="text-muted-foreground">·</span>
<span className="text-gray-600"> {stats.deceasedMembers}</span>
@@ -507,17 +507,17 @@ export default function DashboardPage() {
{/* 第二个卡片:繁衍代数 + 记录年代 + 平均寿命 */}
<Link href={currentTree?.id ? `/timeline?treeId=${currentTree.id}` : '#'}>
<ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
<ChineseCardContent className="p-5 pt-6">
<div className="flex items-center justify-between mb-3">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<ChineseCardContent className="p-4 md:p-5 pt-5 md:pt-6">
<div className="flex items-center justify-between mb-2 md:mb-3">
<p className="text-xs md:text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="chinese-seal chinese-seal-sm">
<Clock className="h-4 w-4" />
</div>
</div>
<div className="flex flex-col space-y-1.5">
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{stats.maxGeneration} </span>
<ChineseDivider variant="simple" className="my-2" />
<div className="flex items-center gap-3 text-sm text-muted-foreground/80 font-medium">
<div className="flex flex-col space-y-1">
<span className="text-2xl md:text-3xl font-serif font-bold text-foreground tracking-tight">{stats.maxGeneration} </span>
<ChineseDivider variant="simple" className="my-1.5 md:my-2" />
<div className="flex items-center gap-2 md:gap-3 text-xs md:text-sm text-muted-foreground/80 font-medium flex-wrap">
<span> {stats.yearsSpan} </span>
<span className="text-muted-foreground">·</span>
<span>寿 {stats.averageLifespan > 0 ? `${stats.averageLifespan}` : '-'}</span>
@@ -530,25 +530,25 @@ export default function DashboardPage() {
{/* 第三个卡片:性别比例 */}
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}>
<ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
<ChineseCardContent className="p-5 pt-6">
<div className="flex items-center justify-between mb-3">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<ChineseCardContent className="p-4 md:p-5 pt-5 md:pt-6">
<div className="flex items-center justify-between mb-2 md:mb-3">
<p className="text-xs md:text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="chinese-seal chinese-seal-sm">
<Users className="h-4 w-4" />
</div>
</div>
<div className="flex flex-col space-y-1.5">
<div className="flex items-center justify-center gap-2.5">
<span className="text-3xl font-serif font-bold text-primary tracking-tight">
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-center gap-2">
<span className="text-2xl md:text-3xl font-serif font-bold text-primary tracking-tight">
{stats.maleCount > 0 ? '1' : '0'}
</span>
<span className="text-2xl font-bold text-muted-foreground">:</span>
<span className="text-3xl font-serif font-bold text-primary tracking-tight">
<span className="text-xl md:text-2xl font-bold text-muted-foreground">:</span>
<span className="text-2xl md:text-3xl font-serif font-bold text-primary tracking-tight">
{stats.maleCount > 0 ? (stats.femaleCount / stats.maleCount).toFixed(2) : '0'}
</span>
</div>
<ChineseDivider variant="simple" className="my-2" />
<div className="text-center text-sm text-muted-foreground/80 font-medium">
<ChineseDivider variant="simple" className="my-1.5 md:my-2" />
<div className="text-center text-xs md:text-sm text-muted-foreground/80 font-medium">
{stats.maleCount} · {stats.femaleCount}
</div>
</div>
@@ -559,17 +559,17 @@ export default function DashboardPage() {
</CollapsibleContent>
</Collapsible>
<Tabs defaultValue="photos" className="w-full flex-1 flex flex-col overflow-hidden">
<div className="flex items-center justify-between mb-4 flex-shrink-0">
<TabsList className="bg-muted/50">
<TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="recent"></TabsTrigger>
<TabsTrigger value="statistics"></TabsTrigger>
<TabsTrigger value="migration"></TabsTrigger>
<Tabs defaultValue="photos" className="w-full flex-1 flex flex-col min-h-0">
<div className="flex items-center justify-between mb-3 md:mb-4 flex-shrink-0 gap-2">
<TabsList className="bg-muted/50 h-8 md:h-9">
<TabsTrigger value="photos" className="text-[11px] md:text-sm px-2 md:px-3 h-7 md:h-8"></TabsTrigger>
<TabsTrigger value="recent" className="text-[11px] md:text-sm px-2 md:px-3 h-7 md:h-8"></TabsTrigger>
<TabsTrigger value="statistics" className="text-[11px] md:text-sm px-2 md:px-3 h-7 md:h-8"></TabsTrigger>
<TabsTrigger value="migration" className="text-[11px] md:text-sm px-2 md:px-3 h-7 md:h-8"></TabsTrigger>
</TabsList>
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '/members'}>
<Button variant="ghost" size="sm" className="text-muted-foreground gap-1">
<ArrowRight className="h-3 w-3" />
<Button variant="ghost" size="sm" className="text-muted-foreground gap-1 text-[11px] md:text-sm h-8 px-2 md:px-3">
<ArrowRight className="h-3 w-3" />
</Button>
</Link>
</div>
+20 -20
View File
@@ -1103,19 +1103,19 @@ export default function TimelinePage() {
return (
<div className="h-screen bg-background flex flex-col font-sans overflow-hidden">
<SiteHeader />
<main className="container mx-auto py-8 px-4 md:px-6 flex-1 flex flex-col overflow-hidden">
<div className="mb-8 flex-shrink-0">
<h1 className="text-3xl font-serif font-bold mb-2"></h1>
<p className="text-muted-foreground"></p>
<main className="container mx-auto py-4 md:py-8 px-3 md:px-6 flex-1 flex flex-col overflow-y-auto overflow-x-hidden">
<div className="mb-4 md:mb-8 flex-shrink-0">
<h1 className="text-2xl md:text-3xl font-serif font-bold mb-1 md:mb-2"></h1>
<p className="text-sm md:text-base text-muted-foreground"></p>
</div>
<Tabs defaultValue="timeline" className="w-full flex-1 flex flex-col overflow-hidden">
<TabsList className="mb-6 w-full md:w-auto flex-shrink-0">
<TabsTrigger value="timeline" className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<TabsList className="mb-4 md:mb-6 w-full md:w-auto flex-shrink-0 h-8 md:h-9">
<TabsTrigger value="timeline" className="flex items-center gap-1 md:gap-2 text-xs md:text-sm">
<Clock className="h-3 w-3 md:h-4 md:w-4" />
</TabsTrigger>
<TabsTrigger value="migration" className="flex items-center gap-2">
<MapIcon className="h-4 w-4" />
<TabsTrigger value="migration" className="flex items-center gap-1 md:gap-2 text-xs md:text-sm">
<MapIcon className="h-3 w-3 md:h-4 md:w-4" />
</TabsTrigger>
</TabsList>
@@ -1237,13 +1237,13 @@ export default function TimelinePage() {
</TabsContent>
<TabsContent value="migration" className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<ChineseCard variant="bamboo" className="shadow-sm">
<ChineseCardHeader>
<ChineseCardTitle className="font-serif"></ChineseCardTitle>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8">
<ChineseCard variant="bamboo" className="shadow-sm overflow-hidden">
<ChineseCardHeader className="px-3 md:px-5">
<ChineseCardTitle className="font-serif text-base md:text-lg"></ChineseCardTitle>
</ChineseCardHeader>
<ChineseCardContent>
<ScrollArea className="h-[600px] pr-4">
<ChineseCardContent className="px-2 md:px-5 pb-2 md:pb-5">
<ScrollArea className="h-[400px] md:h-[600px] pr-2 md:pr-4">
<div className="space-y-6">
{migrations.map((mig, idx) => {
const isSelfMigration = mig.father.id === mig.person.id
@@ -1299,12 +1299,12 @@ export default function TimelinePage() {
</ChineseCardContent>
</ChineseCard>
<ChineseCard variant="default" cornerOrnament className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20 shadow-sm">
<ChineseCardHeader>
<ChineseCardTitle className="font-serif"></ChineseCardTitle>
<ChineseCard variant="default" cornerOrnament className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20 shadow-sm overflow-hidden">
<ChineseCardHeader className="px-3 md:px-5">
<ChineseCardTitle className="font-serif text-base md:text-lg"></ChineseCardTitle>
</ChineseCardHeader>
<ChineseCardContent>
<div className="relative h-[600px] overflow-hidden rounded-lg bg-background/50 backdrop-blur border border-border/50">
<ChineseCardContent className="px-2 md:px-5 pb-2 md:pb-5">
<div className="relative h-[400px] md:h-[600px] overflow-hidden rounded-lg bg-background/50 backdrop-blur border border-border/50">
{!mapLoaded ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<div className="text-center">
+163 -55
View File
@@ -51,6 +51,7 @@ export default function TreePage() {
const router = useRouter()
const searchParams = useSearchParams()
const [viewMode, setViewMode] = useState<ViewMode>("traditional")
const [isIOSDevice, setIsIOSDevice] = useState(false)
// 关系查询模式
const [relationMode, setRelationMode] = useState(false)
@@ -81,12 +82,28 @@ export default function TreePage() {
}
}, [currentTree?.id, refreshData])
// 从 URL 参数读取视图模式
// 从 URL 参数读取视图模式iOS 触摸设备默认使用传统族谱
useEffect(() => {
if (!searchParams) return
const view = searchParams.get('view')
if (view === 'd3-org-chart' || view === 'traditional') {
// 检测 iOS 触摸设备(iPad/iPhone 上的所有浏览器)
const isTouchDevice = typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1
const isIOS = typeof navigator !== 'undefined' && (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && isTouchDevice) ||
/CriOS/.test(navigator.userAgent) // iOS Chrome
)
// 设置 iOS 设备状态
setIsIOSDevice(isIOS)
if (view === 'd3-org-chart' && !isIOS) {
// 非 iOS 设备可以使用 D3
setViewMode(view)
} else if (view === 'traditional' || isIOS) {
// iOS 设备强制使用传统族谱
setViewMode('traditional')
}
}, [searchParams])
const [scale, setScale] = useState(1)
@@ -261,12 +278,91 @@ export default function TreePage() {
setIsDragging(false)
}
// 鼠标滚轮缩放
const handleWheel = (e: React.WheelEvent) => {
e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1
setScale((s) => Math.min(Math.max(s + delta, 0.1), 2))
}
// 触摸事件状态
const touchStartRef = useRef<{ x: number; y: number; distance?: number } | null>(null)
const positionRef = useRef(position)
const scaleRef = useRef(scale)
// 保持 ref 同步
useEffect(() => {
positionRef.current = position
}, [position])
useEffect(() => {
scaleRef.current = scale
}, [scale])
// 使用原生事件监听器处理触摸和滚轮事件(避免 passive 事件问题)
useEffect(() => {
const container = containerRef.current
if (!container) return
// 鼠标滚轮缩放
const handleWheel = (e: WheelEvent) => {
e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1
setScale((s) => Math.min(Math.max(s + delta, 0.1), 2))
}
// 计算两指间距离
const getTouchDistance = (touches: TouchList) => {
if (touches.length < 2) return 0
const dx = touches[0].clientX - touches[1].clientX
const dy = touches[0].clientY - touches[1].clientY
return Math.sqrt(dx * dx + dy * dy)
}
// 触摸开始
const handleTouchStart = (e: globalThis.TouchEvent) => {
if (e.touches.length === 1) {
touchStartRef.current = {
x: e.touches[0].clientX - positionRef.current.x,
y: e.touches[0].clientY - positionRef.current.y
}
} else if (e.touches.length === 2) {
touchStartRef.current = {
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
distance: getTouchDistance(e.touches)
}
}
}
// 触摸移动
const handleTouchMove = (e: globalThis.TouchEvent) => {
if (!touchStartRef.current) return
e.preventDefault()
if (e.touches.length === 1 && !touchStartRef.current.distance) {
const newX = e.touches[0].clientX - touchStartRef.current.x
const newY = e.touches[0].clientY - touchStartRef.current.y
setPosition({ x: newX, y: newY })
} else if (e.touches.length === 2 && touchStartRef.current.distance) {
const newDistance = getTouchDistance(e.touches)
const scaleFactor = newDistance / touchStartRef.current.distance
setScale(s => Math.min(Math.max(s * scaleFactor, 0.1), 2))
touchStartRef.current.distance = newDistance
}
}
// 触摸结束
const handleTouchEnd = () => {
touchStartRef.current = null
}
// 添加事件监听器,设置 passive: false
container.addEventListener('wheel', handleWheel, { passive: false })
container.addEventListener('touchstart', handleTouchStart, { passive: true })
container.addEventListener('touchmove', handleTouchMove, { passive: false })
container.addEventListener('touchend', handleTouchEnd, { passive: true })
return () => {
container.removeEventListener('wheel', handleWheel)
container.removeEventListener('touchstart', handleTouchStart)
container.removeEventListener('touchmove', handleTouchMove)
container.removeEventListener('touchend', handleTouchEnd)
}
}, [viewMode])
// 处理成员选择(关系查询模式)
const handleMemberClick = (memberId: string) => {
@@ -589,28 +685,36 @@ export default function TreePage() {
<div className="h-screen flex flex-col bg-muted/30 overflow-hidden">
<SiteHeader />
{/* 视图切换和关系查询按钮 */}
<div className="absolute top-20 right-4 z-30 flex gap-2">
{/* 视图切换和关系查询按钮 - 移动端自适应 */}
<div className="absolute top-16 md:top-20 right-2 md:right-4 z-30 flex gap-1 md:gap-2">
{/* 关系查询按钮 */}
<Button
variant={relationMode ? "default" : "outline"}
className="gap-2 shadow-lg"
className="gap-1 md:gap-2 shadow-lg text-xs md:text-sm px-2 md:px-4 h-8 md:h-10"
onClick={toggleRelationMode}
>
<Users className="h-4 w-4" />
{relationMode ? (
selectedMembers.length === 0 ? '选择第一个成员' : '选择第二个成员'
) : '关系查询'}
<Users className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">
{relationMode ? (
selectedMembers.length === 0 ? '选择第一个成员' : '选择第二个成员'
) : '关系查询'}
</span>
<span className="sm:hidden">
{relationMode ? '选择' : '查询'}
</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2 shadow-lg">
<LayoutGrid className="h-4 w-4" />
{viewMode === 'traditional' ? '传统族谱' : 'D3族谱'}
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
{/* iOS 设备只显示传统族谱,不显示切换按钮 */}
{!isIOSDevice && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-1 md:gap-2 shadow-lg text-xs md:text-sm px-2 md:px-4 h-8 md:h-10">
<LayoutGrid className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">{viewMode === 'traditional' ? '传统族谱' : 'D3族谱'}</span>
<span className="sm:hidden">{viewMode === 'traditional' ? '传统' : 'D3'}</span>
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={() => {
@@ -625,44 +729,48 @@ export default function TreePage() {
{viewMode === 'traditional' && <span className="text-primary"></span>}
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const params = new URLSearchParams(searchParams.toString())
params.set('view', 'd3-org-chart')
router.push(`/tree?${params.toString()}`)
}}
className={viewMode === 'd3-org-chart' ? 'bg-accent' : ''}
>
<div className="flex items-center justify-between w-full">
<span>D3族谱</span>
{viewMode === 'd3-org-chart' && <span className="text-primary"></span>}
</div>
</DropdownMenuItem>
{/* iOS 设备不显示 D3 选项(foreignObject 渲染问题) */}
{!isIOSDevice && (
<DropdownMenuItem
onClick={() => {
const params = new URLSearchParams(searchParams.toString())
params.set('view', 'd3-org-chart')
router.push(`/tree?${params.toString()}`)
}}
className={viewMode === 'd3-org-chart' ? 'bg-accent' : ''}
>
<div className="flex items-center justify-between w-full">
<span>D3族谱</span>
{viewMode === 'd3-org-chart' && <span className="text-primary"></span>}
</div>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
{viewMode === 'traditional' ? (
<div className="flex-1 relative min-h-0">
{/* Toolbar */}
<div className="absolute top-4 left-4 z-20 flex flex-col gap-2 bg-background/80 backdrop-blur p-2 rounded-lg border shadow-sm">
<Button variant="outline" size="icon" onClick={() => setScale((s) => Math.min(s + 0.1, 2))}>
{/* Toolbar - 移动端底部横向,桌面端左侧纵向 */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 md:bottom-auto md:top-4 md:left-4 md:translate-x-0 z-20 flex flex-row md:flex-col gap-1 md:gap-2 bg-background/90 backdrop-blur p-1.5 md:p-2 rounded-lg border shadow-lg">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => setScale((s) => Math.min(s + 0.1, 2))}>
<ZoomIn className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => setScale((s) => Math.max(s - 0.1, 0.1))}>
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => setScale((s) => Math.max(s - 0.1, 0.1))}>
<ZoomOut className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleTraditionalFitView} title="自适应视图">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={handleTraditionalFitView} title="自适应视图">
<Move className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => setExpandAll(true)} title="完全展开">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => setExpandAll(true)} title="完全展开">
<Maximize2 className="h-4 w-4" />
</Button>
<div className="h-px bg-border my-1" />
<Button variant="outline" size="icon" onClick={handleExportImage} title="导出为图片">
<div className="w-px md:w-auto h-6 md:h-px bg-border mx-1 md:mx-0 md:my-1" />
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={handleExportImage} title="导出为图片">
<Download className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleExportPDF} title="导出族谱PDF">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={handleExportPDF} title="导出族谱PDF">
<FileText className="h-4 w-4" />
</Button>
</div>
@@ -675,7 +783,6 @@ export default function TreePage() {
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
>
<div
style={{
@@ -699,22 +806,22 @@ export default function TreePage() {
</div>
) : (
<div className="flex-1 relative min-h-0">
{/* 统一的控制按钮 */}
<div className="absolute top-4 left-4 z-20 flex flex-col gap-2 bg-background/80 backdrop-blur p-2 rounded-lg border shadow-sm">
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.expandAll()} title="完全展开">
{/* 统一的控制按钮 - 移动端底部横向,桌面端左侧纵向 */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 md:bottom-auto md:top-4 md:left-4 md:translate-x-0 z-20 flex flex-row md:flex-col gap-1 md:gap-2 bg-background/90 backdrop-blur p-1.5 md:p-2 rounded-lg border shadow-lg">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => d3ChartRef.current?.expandAll()} title="完全展开">
<Maximize2 className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.collapseAll()} title="折叠全部">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => d3ChartRef.current?.collapseAll()} title="折叠全部">
<ZoomOut className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.fitView()} title="适应视图">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => d3ChartRef.current?.fitView()} title="适应视图">
<Move className="h-4 w-4" />
</Button>
<div className="h-px bg-border my-1" />
<Button variant="outline" size="icon" onClick={() => d3ChartRef.current?.exportPNG()} title="导出PNG">
<div className="w-px md:w-auto h-6 md:h-px bg-border mx-1 md:mx-0 md:my-1" />
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={() => d3ChartRef.current?.exportPNG()} title="导出PNG">
<Download className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleExportPDF} title="导出族谱PDF">
<Button variant="outline" size="icon" className="h-9 w-9 md:h-10 md:w-10" onClick={handleExportPDF} title="导出族谱PDF">
<FileText className="h-4 w-4" />
</Button>
</div>
@@ -734,10 +841,11 @@ export default function TreePage() {
{/* 关系查询结果对话框 */}
<Dialog open={showRelationDialog} onOpenChange={handleDialogClose}>
<DialogContent className="max-w-2xl" aria-describedby="relation-description">
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<p id="relation-description" className="sr-only"></p>
{relationResult && (
<div className="space-y-4">