From 86e889cc2ad2a5d45379077cf421bc8747f105c7 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Sun, 30 Nov 2025 19:10:30 +0800 Subject: [PATCH] 0.3.1.2 --- app/admin/page.tsx | 2 +- app/globals.css | 416 ++++++++++++ app/members/[id]/page.tsx | 630 ++++++++++-------- app/members/new/page.tsx | 19 +- app/members/page.tsx | 25 +- app/page.tsx | 477 ++++++------- app/relationship/page.tsx | 4 +- app/timeline/page.tsx | 38 +- components/dashboard/family-trees-list.tsx | 52 +- components/dashboard/statistics-charts.tsx | 66 +- components/members/member-form.tsx | 326 +++++---- components/members/member-version-history.tsx | 2 +- components/ui/back-button.tsx | 25 + components/ui/chinese-card.tsx | 207 ++++++ context/family-context.tsx | 27 +- next-env.d.ts | 2 +- 16 files changed, 1555 insertions(+), 763 deletions(-) create mode 100644 components/ui/back-button.tsx create mode 100644 components/ui/chinese-card.tsx diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 77df4e9d..134319ba 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -222,7 +222,7 @@ export default function AdminPage() { -

用户管理

+

用户管理

diff --git a/app/globals.css b/app/globals.css index 97d357e5..65e39dca 100644 --- a/app/globals.css +++ b/app/globals.css @@ -188,3 +188,419 @@ @apply font-serif; } } + +/* ============================================ + 中国风卡片样式系统 + Chinese Traditional Card Styles + ============================================ */ + +@layer components { + /* 基础变量 */ + :root { + --chinese-gold: oklch(0.75 0.12 85); + --chinese-gold-dark: oklch(0.55 0.10 50); + --chinese-red: oklch(0.45 0.15 25); + --chinese-red-light: oklch(0.55 0.12 25); + --chinese-ink: oklch(0.2 0.02 265); + --chinese-paper: oklch(0.97 0.01 90); + --corner-size: 12px; + --border-ornament: 2px; + } + + .dark { + --chinese-gold: oklch(0.65 0.10 85); + --chinese-gold-dark: oklch(0.45 0.08 50); + --chinese-paper: oklch(0.15 0.01 265); + } + + /* ============ 默认中国风卡片 ============ */ + .chinese-card-default { + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: + 0 1px 3px rgba(0,0,0,0.06), + 0 1px 2px rgba(0,0,0,0.04); + background: linear-gradient( + to bottom right, + var(--card) 0%, + oklch(from var(--card) l calc(c - 0.005) h) 100% + ); + transition: all 0.3s ease; + } + + .chinese-card-default:hover { + box-shadow: + 0 4px 12px rgba(0,0,0,0.08), + 0 2px 4px rgba(0,0,0,0.04); + border-color: var(--chinese-red-light); + } + + /* ============ 卷轴风格卡片 ============ */ + .chinese-card-scroll { + border: none; + border-radius: 2px; + background: var(--card); + box-shadow: + inset 0 0 0 1px var(--border), + 0 2px 8px rgba(0,0,0,0.06); + position: relative; + padding-top: 8px; + } + + /* 卷轴顶部装饰 */ + .chinese-card-scroll::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 6px; + background: linear-gradient( + to right, + var(--chinese-gold-dark) 0%, + var(--chinese-gold) 15%, + var(--chinese-gold) 85%, + var(--chinese-gold-dark) 100% + ); + border-radius: 2px 2px 0 0; + } + + /* 卷轴底部装饰 */ + .chinese-card-scroll::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient( + to right, + var(--chinese-gold-dark) 0%, + var(--chinese-gold) 15%, + var(--chinese-gold) 85%, + var(--chinese-gold-dark) 100% + ); + border-radius: 0 0 2px 2px; + } + + /* ============ 牌匾风格卡片 ============ */ + .chinese-card-plaque { + border: 3px solid var(--chinese-gold-dark); + border-radius: 2px; + background: linear-gradient( + 135deg, + var(--card) 0%, + oklch(from var(--card) calc(l - 0.02) c h) 100% + ); + box-shadow: + inset 0 0 0 1px var(--chinese-gold), + 0 4px 16px rgba(0,0,0,0.1); + position: relative; + } + + /* 牌匾内边框装饰 */ + .chinese-card-plaque::before { + content: ''; + position: absolute; + inset: 4px; + border: 1px solid oklch(from var(--chinese-gold) l c h / 0.3); + border-radius: 1px; + pointer-events: none; + } + + /* ============ 画像边框风格卡片 ============ */ + .chinese-card-portrait { + border: 2px solid var(--chinese-ink); + border-radius: 0; + background: var(--card); + box-shadow: + 4px 4px 0 0 oklch(from var(--chinese-ink) l c h / 0.15), + inset 0 0 0 4px var(--card), + inset 0 0 0 5px var(--border); + position: relative; + } + + .chinese-card-portrait::before { + content: ''; + position: absolute; + inset: 8px; + border: 1px dashed oklch(from var(--chinese-ink) l c h / 0.2); + pointer-events: none; + } + + /* ============ 竹简风格卡片 ============ */ + .chinese-card-bamboo { + border: none; + border-radius: 0; + background: + repeating-linear-gradient( + to right, + var(--card) 0px, + var(--card) 28px, + oklch(from var(--border) l c h / 0.5) 28px, + oklch(from var(--border) l c h / 0.5) 30px + ); + box-shadow: + inset 0 2px 4px rgba(0,0,0,0.05), + 0 2px 8px rgba(0,0,0,0.08); + position: relative; + padding: 16px 20px; + } + + .chinese-card-bamboo::before, + .chinese-card-bamboo::after { + content: ''; + position: absolute; + left: 8px; + right: 8px; + height: 3px; + background: var(--chinese-gold-dark); + border-radius: 1px; + } + + .chinese-card-bamboo::before { + top: 6px; + } + + .chinese-card-bamboo::after { + bottom: 6px; + } + + /* ============ 角饰样式 ============ */ + .chinese-card-corner-ornament { + position: relative; + } + + .chinese-corner { + position: absolute; + width: var(--corner-size); + height: var(--corner-size); + pointer-events: none; + } + + /* 角饰 - 回纹风格 */ + .chinese-corner::before, + .chinese-corner::after { + content: ''; + position: absolute; + background: var(--chinese-red); + } + + .chinese-corner::before { + width: 100%; + height: 2px; + } + + .chinese-corner::after { + width: 2px; + height: 100%; + } + + /* 四个角的位置 */ + .chinese-corner-tl { + top: 6px; + left: 6px; + } + .chinese-corner-tl::before { top: 0; left: 0; } + .chinese-corner-tl::after { top: 0; left: 0; } + + .chinese-corner-tr { + top: 6px; + right: 6px; + } + .chinese-corner-tr::before { top: 0; right: 0; } + .chinese-corner-tr::after { top: 0; right: 0; } + + .chinese-corner-bl { + bottom: 6px; + left: 6px; + } + .chinese-corner-bl::before { bottom: 0; left: 0; } + .chinese-corner-bl::after { bottom: 0; left: 0; } + + .chinese-corner-br { + bottom: 6px; + right: 6px; + } + .chinese-corner-br::before { bottom: 0; right: 0; } + .chinese-corner-br::after { bottom: 0; right: 0; } + + /* ============ 顶部装饰条 ============ */ + .chinese-card-header-decoration { + height: 3px; + background: linear-gradient( + to right, + transparent 0%, + var(--chinese-red) 10%, + var(--chinese-gold) 50%, + var(--chinese-red) 90%, + transparent 100% + ); + margin-bottom: 8px; + } + + /* ============ 中国风分隔线 ============ */ + .chinese-divider { + height: 1px; + width: 100%; + margin: 12px 0; + position: relative; + } + + /* 波浪分隔线 */ + .chinese-divider-wave { + background: linear-gradient( + 90deg, + transparent, + var(--border) 20%, + var(--border) 80%, + transparent + ); + } + + .chinese-divider-wave::before { + content: '〰'; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background: var(--card); + padding: 0 8px; + color: var(--chinese-red); + font-size: 12px; + letter-spacing: 4px; + } + + /* 云纹分隔线 */ + .chinese-divider-cloud { + background: linear-gradient( + 90deg, + transparent, + var(--border) 15%, + var(--border) 85%, + transparent + ); + } + + .chinese-divider-cloud::before { + content: '☁'; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background: var(--card); + padding: 0 12px; + color: var(--muted-foreground); + font-size: 14px; + } + + /* 回纹分隔线 */ + .chinese-divider-meander { + height: 8px; + background: + repeating-linear-gradient( + 90deg, + var(--chinese-red) 0px, + var(--chinese-red) 4px, + transparent 4px, + transparent 8px + ); + opacity: 0.3; + } + + /* 简约分隔线 */ + .chinese-divider-simple { + background: linear-gradient( + 90deg, + transparent, + var(--chinese-red) 30%, + var(--chinese-red) 70%, + transparent + ); + opacity: 0.4; + } + + /* ============ 悬停装饰效果 ============ */ + .chinese-card-default:hover .chinese-corner::before, + .chinese-card-default:hover .chinese-corner::after { + background: var(--chinese-gold); + } + + /* ============ 统计卡片特殊样式 ============ */ + .chinese-stat-card { + position: relative; + overflow: hidden; + } + + .chinese-stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient( + to right, + var(--chinese-red) 0%, + var(--chinese-gold) 50%, + var(--chinese-red) 100% + ); + } + + .chinese-stat-card:hover::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient( + to right, + transparent 0%, + var(--chinese-red) 20%, + var(--chinese-red) 80%, + transparent 100% + ); + animation: stat-line-in 0.3s ease-out; + } + + @keyframes stat-line-in { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } + } + + /* ============ 印章装饰 ============ */ + .chinese-seal { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 2px solid var(--chinese-red); + border-radius: 4px; + color: var(--chinese-red); + font-family: var(--font-serif); + font-size: 14px; + font-weight: bold; + transform: rotate(-5deg); + opacity: 0.8; + } + + .chinese-seal-sm { + width: 28px; + height: 28px; + font-size: 10px; + border-width: 1.5px; + } + + .chinese-seal-lg { + width: 56px; + height: 56px; + font-size: 18px; + border-width: 3px; + } +} diff --git a/app/members/[id]/page.tsx b/app/members/[id]/page.tsx index b3f109c8..6d3abcdc 100644 --- a/app/members/[id]/page.tsx +++ b/app/members/[id]/page.tsx @@ -1,14 +1,7 @@ "use client" -import { useEffect, useState } from "react" -import { useParams, useRouter } from "next/navigation" -import Link from "next/link" -import { useFamily } from "@/context/family-context" -import { SiteHeader } from "@/components/site-header" -import { MemberForm } from "@/components/members/member-form" -import { Button } from "@/components/ui/button" -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" -import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus, Camera } from "lucide-react" +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card" +import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus, Camera, History } from "lucide-react" import type { FamilyMember } from "@/types/family" import { PhotoGallery } from "@/components/members/photo-gallery" import { StoryManager } from "@/components/members/story-manager" @@ -17,6 +10,15 @@ import { permissions } from "@/lib/permissions" import { MemberNameWithStatus } from "@/components/member-name-with-status" import { MemberVersionHistory } from "@/components/members/member-version-history" import { useSession } from "next-auth/react" +import { useEffect, useState } from "react" +import { useParams, useRouter } from "next/navigation" +import Link from "next/link" +import { useFamily } from "@/context/family-context" +import { BackButton } from "@/components/ui/back-button" +import { SiteHeader } from "@/components/site-header" +import { MemberForm } from "@/components/members/member-form" +import { Button } from "@/components/ui/button" +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" export default function MemberProfilePage() { const { id } = useParams() @@ -40,8 +42,16 @@ export default function MemberProfilePage() { const member = getMember(id as string) const allMembers = Object.values(treeData.members) - // 始祖专用边框样式 - const founderBorderClass = member?.isFounder ? "border-2 border-amber-400 shadow-md" : "border border-border" + // 判断是否已故 + const isDead = !!member?.deathDate + + // 根据在世/已故状态设置卡片样式 + // 优先级:始祖 > 已故 > 在世 + const cardStyle = member?.isFounder + ? '!border-2 !border-amber-400 !bg-amber-50/30 dark:!bg-amber-900/20 shadow-md' // 始祖:金色边框(优先级最高) + : isDead + ? '!border-2 !border-gray-400 dark:!border-gray-600 !bg-gray-50/50 dark:!bg-gray-900/30' // 已故:灰色深边框 + : '!border-2 !border-amber-300/70 !bg-card dark:!border-amber-600/50' // 在世:浅金色边框 // 计算年龄或享年 const calculateAge = () => { @@ -227,16 +237,7 @@ export default function MemberProfilePage() { {/* 固定头部区域 */}
{/* 返回按钮 */} -
- -
+ setIsEditing(false)} label="返回" /> {/* Header Section */}
@@ -304,7 +305,7 @@ export default function MemberProfilePage() { {/* Tab 导航栏 */} - 详细信息 + 基本信息 详细资料 生平影像 家族故事 @@ -359,16 +360,10 @@ export default function MemberProfilePage() { {/* 固定头部区域 */}
{/* 返回按钮 */} -
- -
+ router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)} + label="返回" + /> {/* Header Section */}
@@ -479,7 +474,7 @@ export default function MemberProfilePage() { {/* Tab 导航栏 */} - 详细信息 + 基本信息 详细资料 生平影像 家族故事 @@ -511,304 +506,361 @@ export default function MemberProfilePage() { {/* Tab 内容区域 - 可滚动 */}
- {/* 详细信息Tab - 包含基本信息和家庭关系 */} + {/* 基本信息Tab - 包含基本信息和家庭关系 */} {/* 基本信息 */} -
-

基本信息

-
-
-
姓名
-
{member.fullName}
-
-
-
性别
-
{member.gender === 'MALE' ? '男' : '女'}
-
-
-
世系
-
第 {member.generation} 世
-
- {member.phone && ( + + + 基本信息 +
-
联系电话
-
- {member.phone} -
+
姓名
+
{member.fullName}
- )} - {member.occupation && (
-
职业
-
{member.occupation}
+
性别
+
{member.gender === 'MALE' ? '男' : '女'}
- )} -
-
+
+
世系
+
第 {member.generation} 世
+
+ {member.occupation && ( +
+
职业
+
{member.occupation}
+
+ )} + + + {/* 家庭关系 */}
-
-

- 父母 -

-
-
-

父亲

- {member.fatherId ? ( - - - - ) : member.spouseFatherName ? ( - - {member.spouseFatherName} - - ) : ( - - - )} -
-
-

母亲

- {member.motherId ? ( - - - - ) : member.spouseMotherName ? ( - - {member.spouseMotherName} - - ) : ( - - - )} -
-
-
-
-

- 配偶与子女 -

-
-
-

配偶

-
- {member.spouseIds?.length > 0 ? ( - member.spouseIds.map((spouseId) => ( - - - - )) + + +

+ 父母 +

+
+
+

父亲

+ {member.fatherId ? ( + + + + ) : member.spouseFatherName ? ( + + {member.spouseFatherName} + ) : ( - 无记录 + - + )} +
+
+

母亲

+ {member.motherId ? ( + + + + ) : member.spouseMotherName ? ( + + {member.spouseMotherName} + + ) : ( + - )}
-
-

子女

-
- {member.childrenIds?.length > 0 ? ( - member.childrenIds.map((childId) => ( - - - - )) - ) : ( - 无记录 - )} + + + + +

+ 配偶与子女 +

+
+
+

配偶

+
+ {member.spouseIds?.length > 0 ? ( + member.spouseIds.map((spouseId) => ( + + + + )) + ) : ( + 无记录 + )} +
+
+
+

子女

+
+ {member.childrenIds?.length > 0 ? ( + member.childrenIds.map((childId) => ( + + + + )) + ) : ( + 无记录 + )} +
-
-
+
+
{/* 详细资料Tab */} -
-

传统称谓与生平

-
-
-
生卒
-
-
- 出生:{member.birthDate || "-"} - {member.birthDate && member.isLunarDate && ( - - 农历 - - )} + + + 传统称谓与生平 +
+
+
生卒
+
+
+ 出生:{member.birthDate || "-"} + {member.birthDate && member.isLunarDate && ( + + 农历 + + )} +
+
+ 逝世:{member.deathDate || "-"} + {member.deathDate && member.isLunarDate && ( + + 农历 + + )} +
+
+
+ {ageInfo && ( +
+
{ageInfo.type === 'deceased' ? '享年' : '年龄'}
+
{ageInfo.age} 岁
-
- 逝世:{member.deathDate || "-"} - {member.deathDate && member.isLunarDate && ( - - 农历 - - )} + )} + {member.generationName && ( +
+
字辈
+
{member.generationName}
-
-
- {ageInfo && ( -
-
{ageInfo.type === 'deceased' ? '享年' : '年龄'}
-
{ageInfo.age} 岁
-
- )} - {member.generationName && ( -
-
字辈
-
{member.generationName}
-
- )} - {member.courtesyName && ( -
-
-
{member.courtesyName}
-
- )} - {member.artName && ( -
-
-
{member.artName}
-
- )} - {member.posthumousName && ( -
-
谥号
-
{member.posthumousName}
-
- )} - {member.birthPlace && ( -
-
出生地
-
{member.birthPlace}
-
- )} - {member.deathPlace && ( -
-
逝世地
-
{member.deathPlace}
-
- )} - {member.address && ( -
-
居住地
-
{member.address}
-
- )} -
-
+ )} + {member.courtesyName && ( +
+
+
{member.courtesyName}
+
+ )} + {member.artName && ( +
+
+
{member.artName}
+
+ )} + {member.posthumousName && ( +
+
谥号
+
{member.posthumousName}
+
+ )} + {member.ancestralHome && ( +
+
籍贯
+
{member.ancestralHome}
+
+ )} + {member.birthPlace && ( +
+
出生地
+
{member.birthPlace}
+
+ )} + {member.deathPlace && ( +
+
逝世地
+
{member.deathPlace}
+
+ )} + {member.burialPlace && ( +
+
安葬地
+
{member.burialPlace}
+
+ )} + {member.address && ( +
+
居住地
+
{member.address}
+
+ )} + {member.occupation && ( +
+
职业
+
{member.occupation}
+
+ )} + {member.phone && ( +
+
手机号
+
+ {member.phone} +
+
+ )} + {member.telephone && ( +
+
固定电话
+
+ {member.telephone} +
+
+ )} + {member.email && ( +
+
电子邮箱
+
+ {member.email} +
+
+ )} + + + {/* 个人简介 */} {member.bio && ( -
-

个人简介

-

{member.bio}

-
+ + + 个人简介 +

{member.bio}

+
+
)} {/* 标签 */} {member.tags && member.tags.length > 0 && ( -
-

标签

-
- {member.tags.map((tag) => ( - - {tag} - - ))} -
-
+ + + 标签 +
+ {member.tags.map((tag) => ( + + {tag} + + ))} +
+
+
)}
{/* 生平影像Tab */} -
-

- - 影像记忆 -

- { - await updateMember(member.id, { photos, photoIds: undefined }) - setLastSaveTime(Date.now()) - }} - readonly={false} - /> -
-
- - {/* 家族故事Tab */} - -
-

- - 家族故事 -

- {member.stories && member.stories.length > 0 ? ( - { - await updateMember(member.id, { stories }) + + + + + 影像记忆 + + { + await updateMember(member.id, { photos, photoIds: undefined }) setLastSaveTime(Date.now()) }} readonly={false} /> - ) : ( -
- 暂无故事记录,期待您书写这段家族记忆。 -
- )} -
+ + +
+ + {/* 家族故事Tab */} + + + + + + 家族故事 + + {member.stories && member.stories.length > 0 ? ( + { + await updateMember(member.id, { stories }) + setLastSaveTime(Date.now()) + }} + readonly={false} + /> + ) : ( +
+ 暂无故事记录,期待您书写这段家族记忆。 +
+ )} +
+
{/* 时间线Tab - 仅显示视图 */} -
-

最近更新

-

- 最后保存于 {new Date(lastSaveTime).toLocaleString('zh-CN')} -

-
-
-

版本历史

- -
+ + + 最近更新 +

+ 最后保存于 {new Date(lastSaveTime).toLocaleString('zh-CN')} +

+
+
+ + + + + 版本历史 + + + +
diff --git a/app/members/new/page.tsx b/app/members/new/page.tsx index a30f792a..b7b627f7 100644 --- a/app/members/new/page.tsx +++ b/app/members/new/page.tsx @@ -1,15 +1,17 @@ "use client" +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card" import { useRouter, useSearchParams } from "next/navigation" import { useFamily } from "@/context/family-context" import { SiteHeader } from "@/components/site-header" import { MemberForm } from "@/components/members/member-form" import { Button } from "@/components/ui/button" import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { ArrowLeft, UserPlus } from "lucide-react" +import { UserPlus } from "lucide-react" import type { FamilyMember } from "@/types/family" import { useMemo, useEffect, useState } from "react" import { useSession } from "next-auth/react" +import { BackButton } from "@/components/ui/back-button" export default function NewMemberPage() { const router = useRouter() @@ -146,21 +148,12 @@ export default function NewMemberPage() { {/* 固定头部区域 */}
{/* 返回按钮 */} -
- -
+ router.back()} /> {/* Header Section */}
-
+
{isFirstMember ? ( 👑 ) : ( @@ -208,7 +201,7 @@ export default function NewMemberPage() { {/* Tab 导航栏 */} - 详细信息 + 基本信息 详细资料 生平影像 家族故事 diff --git a/app/members/page.tsx b/app/members/page.tsx index 6f81e92e..d23e2ae5 100644 --- a/app/members/page.tsx +++ b/app/members/page.tsx @@ -6,7 +6,7 @@ import { useFamily } from "@/context/family-context" import { useRouter } from "next/navigation" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" -import { Card, CardContent } from "@/components/ui/card" +import { ChineseCard, ChineseCardContent } from "@/components/ui/chinese-card" import { Search, Plus, Filter, X, Users, User } from "lucide-react" import Link from "next/link" import { AvatarDisplay } from "@/components/ui/avatar-display" @@ -90,15 +90,22 @@ const MemberCard = memo(function MemberCard({ const father = member.fatherId ? allMembers.find(m => m.id === member.fatherId) : null const mother = member.motherId ? allMembers.find(m => m.id === member.motherId) : null + // 判断是否已故 + const isDead = !!member.deathDate + + // 根据在世/已故状态设置卡片样式 + // 优先级:始祖 > 已故 > 在世 + const cardStyle = isFounder + ? '!border-2 !border-amber-400 !bg-amber-50/30 dark:!bg-amber-900/20 shadow-md' // 始祖:金色边框(优先级最高) + : isDead + ? '!border-2 !border-gray-400 dark:!border-gray-600 !bg-gray-50/50 dark:!bg-gray-900/30' // 已故:灰色深边框 + : '!border-2 !border-amber-300/70 !bg-card dark:!border-amber-600/50' // 在世:浅金色边框 + return (
- - + +
)}
-
-
+ +
- {/* 近期纪念日标签页 */} - - - - 近期纪念日 - 未来三个月内的生日和忌日 - - -
- {(() => { - const now = new Date() - const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate()) - const members = Object.values(treeData.members) - const upcomingEvents: Array<{ - member: any - type: 'birth' | 'death' - date: Date - originalDate: string - isLunar: boolean - lunarDisplay?: string - month: number - day: number - }> = [] - - members.forEach(member => { - // 生日 - if (member.birthDate) { - const birthDate = new Date(member.birthDate) - let thisYearBirth: Date - let lunarDisplay: string | undefined - - // 检查是否按农历计算 - if (member.isLunarDate) { - // 农历生日:计算今年对应的公历日期 - const lunarInfo = solar2lunar(birthDate) - if (lunarInfo) { - const thisYearLunar = lunar2solar( - now.getFullYear(), - lunarInfo.lunarMonth, - lunarInfo.lunarDay, - lunarInfo.isLeap - ) - if (thisYearLunar) { - thisYearBirth = thisYearLunar - lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}` - } else { - thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate()) - } - } else { - thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate()) - } - } else { - // 公历生日 - thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate()) - } - - if (thisYearBirth >= now && thisYearBirth <= threeMonthsLater) { - upcomingEvents.push({ - member, - type: 'birth', - date: thisYearBirth, - originalDate: member.birthDate, - isLunar: member.isLunarDate || false, - lunarDisplay, - month: thisYearBirth.getMonth() + 1, - day: thisYearBirth.getDate() - }) - } - } - - // 忌日 - if (member.deathDate) { - const deathDate = new Date(member.deathDate) - let thisYearDeath: Date - let lunarDisplay: string | undefined - - // 检查是否按农历计算 - if (member.isLunarDate) { - // 农历忌日:计算今年对应的公历日期 - const lunarInfo = solar2lunar(deathDate) - if (lunarInfo) { - const thisYearLunar = lunar2solar( - now.getFullYear(), - lunarInfo.lunarMonth, - lunarInfo.lunarDay, - lunarInfo.isLeap - ) - if (thisYearLunar) { - thisYearDeath = thisYearLunar - lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}` - } else { - thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate()) - } - } else { - thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate()) - } - } else { - // 公历忌日 - thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate()) - } - - if (thisYearDeath >= now && thisYearDeath <= threeMonthsLater) { - upcomingEvents.push({ - member, - type: 'death', - date: thisYearDeath, - originalDate: member.deathDate, - isLunar: member.isLunarDate || false, - lunarDisplay, - month: thisYearDeath.getMonth() + 1, - day: thisYearDeath.getDate() - }) - } - } - }) - - upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime()) - - return upcomingEvents.length > 0 ? ( - upcomingEvents.map((event, index) => { - const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear() - return ( - - - -
- {event.isLunar && event.lunarDisplay ? ( - // 农历日期框 - 红色主题 -
- - {event.lunarDisplay} - -
- ) : ( - // 公历日期框 - 蓝色主题 -
- - {event.month}月 - - {event.day} -
- )} -
-

- {event.type === 'birth' ? '诞辰' : '忌日'} - {event.isLunar && ( - - 农历 - - )} -

-

- 第 {event.member.generation} 世 · {yearsAgo} 年 - {event.type === 'death' && ' · 已故'} - {event.isLunar && event.lunarDisplay && ( - · {event.lunarDisplay} - )} -

-
-
-
-
- - ) - }) - ) : ( -
-

来日无期

-
- ) - })()} -
-
-
-
{/* 统计图表标签页 */} @@ -995,12 +998,12 @@ export default function DashboardPage() { {/* 迁徙记录标签页 */} - - - 家族迁徙记录 - 记录家族成员的籍贯分布 - - + + + 家族籍贯记录 + 记录家族成员的籍贯分布 + +
{locationGroups.length > 0 ? ( locationGroups.map(([location, locationMembers]) => ( @@ -1025,12 +1028,12 @@ export default function DashboardPage() { )) ) : (
-

未有迁徙

+

未有籍贯记录

)}
-
-
+ +
diff --git a/app/relationship/page.tsx b/app/relationship/page.tsx index 2302b84b..fe9f6561 100644 --- a/app/relationship/page.tsx +++ b/app/relationship/page.tsx @@ -83,8 +83,8 @@ export default function RelationshipTestPage() {
{/* 标题 */}
-

家族关系计算器

-

+

家族关系计算器

+

选择两个家族成员,自动计算他们之间的亲属关系

diff --git a/app/timeline/page.tsx b/app/timeline/page.tsx index cb73b8f9..5c27329d 100644 --- a/app/timeline/page.tsx +++ b/app/timeline/page.tsx @@ -2,7 +2,7 @@ import { SiteHeader } from "@/components/site-header" import { useFamily } from "@/context/family-context" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card" import { ScrollArea } from "@/components/ui/scroll-area" import { Clock, Map as MapIcon, ArrowRight, Calendar, User, Loader2 } from 'lucide-react' import { useMemo, useEffect, useState, useRef, useCallback } from 'react' @@ -601,8 +601,8 @@ export default function TimelinePage() { - - + +
{events.map((event, idx) => (
@@ -713,17 +713,17 @@ export default function TimelinePage() {
)}
-
-
+ +
- - - 迁徙记录 - - + + + 迁徙记录 + +
{migrations.map((mig, idx) => ( @@ -770,14 +770,14 @@ export default function TimelinePage() { )}
-
-
+ + - - - 迁徙路径图 - - + + + 迁徙路径图 + +
{!mapLoaded ? (
@@ -793,8 +793,8 @@ export default function TimelinePage() { /> )}
- - + +
diff --git a/components/dashboard/family-trees-list.tsx b/components/dashboard/family-trees-list.tsx index c139dae0..e41deecc 100644 --- a/components/dashboard/family-trees-list.tsx +++ b/components/dashboard/family-trees-list.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react" import { useSession } from "next-auth/react" import Link from "next/link" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription } from "@/components/ui/chinese-card" import { Button } from "@/components/ui/button" import { Users, Calendar, Plus, ArrowRight } from "lucide-react" import { format } from "date-fns" @@ -47,41 +47,41 @@ export function FamilyTreesList() { if (!session) { return ( - - - 我的家族树 - 请先登录查看您的家族树 - - + + + 我的家族树 + 请先登录查看您的家族树 + + - - +
+
) } if (loading) { return ( - - - 我的家族树 - - + + + 我的家族树 + +

加载中...

-
-
+ + ) } return ( - - + +
- 我的家族树 - + 我的家族树 + {trees.length > 0 ? `共 ${trees.length} 个家族树` : '还没有家族树'} - +
-
- + + {trees.length === 0 ? (

您还没有创建任何家族树

@@ -106,7 +106,7 @@ export function FamilyTreesList() { {trees.map((tree) => (

{tree.name}

@@ -134,7 +134,7 @@ export function FamilyTreesList() { ))}
)} - - + + ) } diff --git a/components/dashboard/statistics-charts.tsx b/components/dashboard/statistics-charts.tsx index 4de93b0a..281dafb4 100644 --- a/components/dashboard/statistics-charts.tsx +++ b/components/dashboard/statistics-charts.tsx @@ -13,7 +13,7 @@ import { import { CanvasRenderer } from "echarts/renderers" import { graphic } from "echarts/core" import type { FamilyMember } from "@/types/family" -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription } from "@/components/ui/chinese-card" // 注册必要的组件 echarts.use([ @@ -330,45 +330,45 @@ export function StatisticsCharts({ members }: StatisticsChartsProps) { return (
- - - 性别分布 - 家族成员性别比例 - - + + + 性别分布 + 家族成员性别比例 + +
- - + + - - - 世代分布 - 各世代人数统计 - - + + + 世代分布 + 各世代人数统计 + +
- - + + - - - 年龄分布 - 按年龄段划分的人数 - - + + + 年龄分布 + 按年龄段划分的人数 + +
- - + + - - - 出生年代分布 - 各年代出生人数趋势 - - + + + 出生年代分布 + 各年代出生人数趋势 + +
- - + +
) } diff --git a/components/members/member-form.tsx b/components/members/member-form.tsx index 5e5ef49d..06ef7c13 100644 --- a/components/members/member-form.tsx +++ b/components/members/member-form.tsx @@ -11,7 +11,7 @@ import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { CalendarIcon, User, Scroll, Users, Images, BookOpen } from "lucide-react" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription } from "@/components/ui/chinese-card" import { Switch } from "@/components/ui/switch" import { ImageUpload } from "@/components/ui/image-upload" import { PhotoGallery } from "./photo-gallery" @@ -290,6 +290,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc spouseFatherName: formData.spouseFatherName, spouseMotherName: formData.spouseMotherName, phone: formData.phone, + telephone: formData.telephone, + email: formData.email, occupation: formData.occupation, courtesyName: formData.courtesyName, artName: formData.artName, @@ -298,8 +300,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc birthDate: formData.birthDate, deathDate: formData.deathDate, isLunarDate: formData.isLunarDate || false, + ancestralHome: formData.ancestralHome, birthPlace: formData.birthPlace, deathPlace: formData.deathPlace, + burialPlace: formData.burialPlace, address: formData.address, bio: formData.bio, avatarUrl: formData.avatarUrl, @@ -337,7 +341,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc {!hideTabs && (
- 详细信息 + 基本信息 详细资料 生平影像 家族故事 @@ -346,17 +350,17 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc )}
- {/* 详细信息Tab - 包含基本信息和家庭关系 */} + {/* 基本信息Tab - 包含基本信息和家庭关系 */} {/* 基本信息卡片 */} - - - + + + 基本信息 - - - + + + {/* 头像和基本信息两栏布局 */}
{/* 左侧:头像 */} @@ -367,89 +371,88 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc />
- {/* 右侧:基本表单字段 */} -
-
+ {/* 右侧:表单 */} +
+
- + handleChange("surname", e.target.value)} + placeholder="例如:李" + className={errors.includes("surname") ? "border-red-500" : ""} />
- + handleChange("givenName", e.target.value)} - /> -
-
- - -
-
- - handleChange("generation", Number.parseInt(e.target.value))} + placeholder="例如:明" + className={errors.includes("givenName") ? "border-red-500" : ""} />
-
-
- +
+ + handleChange("gender", val)} + className="flex space-x-4" + > +
+ + +
+
+ + +
+
+
+ +
+ +
handleChange("phone", e.target.value)} - placeholder="请输入联系电话" - /> -
-
- - handleChange("occupation", e.target.value)} - placeholder="请输入职业" + id="generation" + type="number" + value={formData.generation || ""} + onChange={(e) => handleChange("generation", parseInt(e.target.value) || 0)} + className="w-32" + min={1} /> +
- - + + {/* 家庭关系 - 左右两个独立卡片 */}
{/* 左栏:父母 */} - - - + + + 父母 - + {!isFounder && ( - + 需关联父母或配偶以连接至家族树 - + )} - - + +
@@ -558,23 +561,23 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc )}
-
-
+ + {/* 右栏:配偶和子女 */} - - - + + + 配偶与子女 - + {!isFounder && ( - + 需关联父母或配偶以连接至家族树 - + )} - - + +
@@ -697,8 +700,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc )}
-
-
+ +
@@ -706,14 +709,14 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc {/* 使用两列布局放置两个卡片 */}
- - - + + + 传统称谓 - - - + + +
@@ -752,17 +755,17 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc />
-
-
+ + - - - + + + 生平日期 - - - + + +
-
-
+ +
{/* 地点和简介卡片 */} - - - + + + 地点与简介 - - - + + +
+
+ + handleChange("ancestralHome", e.target.value)} + placeholder="请输入籍贯" + /> +
+
+ + handleChange("address", e.target.value)} + placeholder="请输入居住地" + /> +
+
+ +
- + handleChange("address", e.target.value)} - placeholder="请输入居住地" + id="burialPlace" + value={formData.burialPlace || ""} + onChange={(e) => handleChange("burialPlace", e.target.value)} + placeholder="请输入安葬地" + /> +
+
+ + handleChange("occupation", e.target.value)} + placeholder="请输入职业" + /> +
+
+ +
+
+ + handleChange("phone", e.target.value)} + placeholder="请输入手机号" + /> +
+
+ + handleChange("telephone", e.target.value)} + placeholder="请输入固定电话" + /> +
+
+ + handleChange("email", e.target.value)} + placeholder="请输入电子邮箱" />
@@ -834,57 +898,57 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc rows={4} />
- - + + {/* 生平影像Tab */} - - - + + + 照片墙 - - 上传多张照片记录珍贵时刻 - - + + 上传多张照片记录珍贵时刻 + + - - + + {/* 家族故事Tab */} - - - + + + 家族故事 - - 记录与此成员相关的家族故事 - - + + 记录与此成员相关的家族故事 + + handleChange("stories", stories)} /> - - + + - - - + + + 标签 - - 添加标签以便分类和搜索(按回车添加) - - + + 添加标签以便分类和搜索(按回车添加) + +
)}
- - + +
diff --git a/components/members/member-version-history.tsx b/components/members/member-version-history.tsx index 3afea5f6..07eb6995 100644 --- a/components/members/member-version-history.tsx +++ b/components/members/member-version-history.tsx @@ -146,7 +146,7 @@ export function MemberVersionHistory({ memberId, treeId, onVersionSelect, lastUp {/* 其他信息变更 */} {changedOthers.length > 0 && (
-

详细信息变更

+

基本信息变更

{changedOthers.map(([field, change]) => (
diff --git a/components/ui/back-button.tsx b/components/ui/back-button.tsx new file mode 100644 index 00000000..00be17f0 --- /dev/null +++ b/components/ui/back-button.tsx @@ -0,0 +1,25 @@ +"use client" + +import { ArrowLeft } from "lucide-react" +import { Button } from "@/components/ui/button" + +interface BackButtonProps { + onClick: () => void + label?: string + className?: string +} + +export function BackButton({ onClick, label = "返回", className = "" }: BackButtonProps) { + return ( +
+ +
+ ) +} diff --git a/components/ui/chinese-card.tsx b/components/ui/chinese-card.tsx new file mode 100644 index 00000000..84479e9b --- /dev/null +++ b/components/ui/chinese-card.tsx @@ -0,0 +1,207 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +/** + * 中国风卡片组件 + * 提供多种传统中国设计风格变体 + */ + +type ChineseCardVariant = 'default' | 'scroll' | 'plaque' | 'portrait' | 'bamboo' + +interface ChineseCardProps extends React.ComponentProps<'div'> { + variant?: ChineseCardVariant + /** 是否显示角饰 */ + cornerOrnament?: boolean + /** 是否显示顶部装饰条 */ + headerDecoration?: boolean +} + +function ChineseCard({ + className, + variant = 'default', + cornerOrnament = false, + headerDecoration = false, + children, + ...props +}: ChineseCardProps) { + return ( +
+ {/* 顶部装饰条 */} + {headerDecoration && ( +
+ )} + + {/* 角饰元素 */} + {cornerOrnament && ( + <> + + + + + + )} + + {children} +
+ ) +} + +function ChineseCardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function ChineseCardTitle({ className, ...props }: React.ComponentProps<'h3'>) { + return ( +

+ ) +} + +function ChineseCardDescription({ className, ...props }: React.ComponentProps<'p'>) { + return ( +

+ ) +} + +function ChineseCardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +

+ ) +} + +function ChineseCardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +/** + * 中国风分隔线 + */ +function ChineseDivider({ + className, + variant = 'wave', + ...props +}: React.ComponentProps<'div'> & { + variant?: 'wave' | 'cloud' | 'meander' | 'simple' +}) { + return ( +
+ ) +} + +/** + * 统计数字组件 - 用于展示大数字 + */ +function ChineseStatNumber({ + className, + children, + ...props +}: React.ComponentProps<'span'>) { + return ( + + {children} + + ) +} + +/** + * 统计标签组件 + */ +function ChineseStatLabel({ + className, + children, + ...props +}: React.ComponentProps<'span'>) { + return ( + + {children} + + ) +} + +export { + ChineseCard, + ChineseCardHeader, + ChineseCardTitle, + ChineseCardDescription, + ChineseCardContent, + ChineseCardFooter, + ChineseDivider, + ChineseStatNumber, + ChineseStatLabel, + type ChineseCardVariant, +} diff --git a/context/family-context.tsx b/context/family-context.tsx index 488bcb0c..37091030 100644 --- a/context/family-context.tsx +++ b/context/family-context.tsx @@ -237,7 +237,32 @@ function FamilyProviderInner({ children }: { children: React.ReactNode }) { const data = await response.json() - // 更新本地状态 + // 由于后端会维护双向关系,需要重新加载所有成员以保持数据一致性 + // 特别是当修改了关系字段(spouseIds, childrenIds, fatherId, motherId)时 + const hasRelationshipChanges = + updates.spouseIds !== undefined || + updates.childrenIds !== undefined || + updates.fatherId !== undefined || + updates.motherId !== undefined + + if (hasRelationshipChanges) { + // 重新加载所有成员数据以同步双向关系 + const membersResponse = await fetch(`/api/trees/${currentTree.id}/members`) + if (membersResponse.ok) { + const membersData = await membersResponse.json() + const membersMap: Record = {} + membersData.members.forEach((m: FamilyMember) => { + membersMap[m.id] = m + }) + setTreeData(prev => ({ + ...prev, + members: membersMap, + })) + return + } + } + + // 如果没有关系变更,只更新当前成员 setTreeData(prev => ({ ...prev, members: { diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.