0.7.0.0
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user