This commit is contained in:
freedakgmail
2025-11-30 19:10:30 +08:00
parent a05fb384a4
commit 86e889cc2a
16 changed files with 1555 additions and 763 deletions
+1 -1
View File
@@ -222,7 +222,7 @@ export default function AdminPage() {
<Button variant="ghost" size="icon" onClick={() => router.push("/")}> <Button variant="ghost" size="icon" onClick={() => router.push("/")}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
<h1 className="text-2xl font-bold"></h1> <h1 className="text-3xl font-serif font-bold"></h1>
</div> </div>
</div> </div>
+416
View File
@@ -188,3 +188,419 @@
@apply font-serif; @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;
}
}
+123 -71
View File
@@ -1,14 +1,7 @@
"use client" "use client"
import { useEffect, useState } from "react" import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card"
import { useParams, useRouter } from "next/navigation" import { ArrowLeft, Edit, Trash, Share2, Printer, CircleUser, CircleUserRound, Images, BookOpen, UserPlus, Camera, History } from "lucide-react"
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 type { FamilyMember } from "@/types/family" import type { FamilyMember } from "@/types/family"
import { PhotoGallery } from "@/components/members/photo-gallery" import { PhotoGallery } from "@/components/members/photo-gallery"
import { StoryManager } from "@/components/members/story-manager" 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 { MemberNameWithStatus } from "@/components/member-name-with-status"
import { MemberVersionHistory } from "@/components/members/member-version-history" import { MemberVersionHistory } from "@/components/members/member-version-history"
import { useSession } from "next-auth/react" 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() { export default function MemberProfilePage() {
const { id } = useParams() const { id } = useParams()
@@ -40,8 +42,16 @@ export default function MemberProfilePage() {
const member = getMember(id as string) const member = getMember(id as string)
const allMembers = Object.values(treeData.members) 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 = () => { const calculateAge = () => {
@@ -227,16 +237,7 @@ export default function MemberProfilePage() {
{/* 固定头部区域 */} {/* 固定头部区域 */}
<div className="flex-shrink-0 pb-4 border-b border-border"> <div className="flex-shrink-0 pb-4 border-b border-border">
{/* 返回按钮 */} {/* 返回按钮 */}
<div className="py-4"> <BackButton onClick={() => setIsEditing(false)} label="返回" />
<Button
variant="ghost"
onClick={() => setIsEditing(false)}
className="gap-2 pl-0 hover:bg-transparent hover:text-primary text-2xl font-bold"
>
<ArrowLeft className="h-6 w-6" />
</Button>
</div>
{/* Header Section */} {/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4"> <div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
@@ -304,7 +305,7 @@ export default function MemberProfilePage() {
{/* Tab 导航栏 */} {/* Tab 导航栏 */}
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit"> <TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
<TabsTrigger value="info"></TabsTrigger> <TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger> <TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger> <TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger> <TabsTrigger value="stories"></TabsTrigger>
@@ -359,16 +360,10 @@ export default function MemberProfilePage() {
{/* 固定头部区域 */} {/* 固定头部区域 */}
<div className="flex-shrink-0 pb-4 border-b border-border"> <div className="flex-shrink-0 pb-4 border-b border-border">
{/* 返回按钮 */} {/* 返回按钮 */}
<div className="py-4"> <BackButton
<Button
variant="ghost"
onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)} onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}
className="gap-2 pl-0 hover:bg-transparent hover:text-primary text-2xl font-bold" label="返回"
> />
<ArrowLeft className="h-6 w-6" />
</Button>
</div>
{/* Header Section */} {/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4"> <div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
@@ -479,7 +474,7 @@ export default function MemberProfilePage() {
{/* Tab 导航栏 */} {/* Tab 导航栏 */}
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit"> <TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
<TabsTrigger value="info"></TabsTrigger> <TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger> <TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger> <TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger> <TabsTrigger value="stories"></TabsTrigger>
@@ -511,11 +506,12 @@ export default function MemberProfilePage() {
{/* Tab 内容区域 - 可滚动 */} {/* Tab 内容区域 - 可滚动 */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{/* 详细信息Tab - 包含基本信息和家庭关系 */} {/* 基本信息Tab - 包含基本信息和家庭关系 */}
<TabsContent value="info" className="mt-6 space-y-6"> <TabsContent value="info" className="mt-6 space-y-6">
{/* 基本信息 */} {/* 基本信息 */}
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-4"></h3> <ChineseCardContent className="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"> <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> <div>
<dt className="text-muted-foreground mb-1"></dt> <dt className="text-muted-foreground mb-1"></dt>
@@ -529,14 +525,6 @@ export default function MemberProfilePage() {
<dt className="text-muted-foreground mb-1"></dt> <dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium"> {member.generation} </dd> <dd className="font-medium"> {member.generation} </dd>
</div> </div>
{member.phone && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">
<a href={`tel:${member.phone}`} className="hover:text-primary">{member.phone}</a>
</dd>
</div>
)}
{member.occupation && ( {member.occupation && (
<div> <div>
<dt className="text-muted-foreground mb-1"></dt> <dt className="text-muted-foreground mb-1"></dt>
@@ -544,11 +532,13 @@ export default function MemberProfilePage() {
</div> </div>
)} )}
</dl> </dl>
</div> </ChineseCardContent>
</ChineseCard>
{/* 家庭关系 */} {/* 家庭关系 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}> <ChineseCard variant="default" cornerOrnament className={`rounded-lg ${cardStyle}`}>
<ChineseCardContent className="p-4">
<h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider"> <h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider">
</h3> </h3>
@@ -594,8 +584,10 @@ export default function MemberProfilePage() {
)} )}
</div> </div>
</div> </div>
</div> </ChineseCardContent>
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}> </ChineseCard>
<ChineseCard variant="default" cornerOrnament className={`rounded-lg ${cardStyle}`}>
<ChineseCardContent className="p-4">
<h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider"> <h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider">
</h3> </h3>
@@ -643,14 +635,16 @@ export default function MemberProfilePage() {
</div> </div>
</div> </div>
</div> </div>
</div> </ChineseCardContent>
</ChineseCard>
</div> </div>
</TabsContent> </TabsContent>
{/* 详细资料Tab */} {/* 详细资料Tab */}
<TabsContent value="details" className="mt-6 space-y-6"> <TabsContent value="details" className="mt-6 space-y-6">
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-4"></h3> <ChineseCardContent className="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-6 gap-x-8 gap-y-4 text-sm"> <dl className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6 gap-x-8 gap-y-4 text-sm">
<div> <div>
<dt className="text-muted-foreground mb-1"></dt> <dt className="text-muted-foreground mb-1"></dt>
@@ -703,6 +697,12 @@ export default function MemberProfilePage() {
<dd className="font-medium">{member.posthumousName}</dd> <dd className="font-medium">{member.posthumousName}</dd>
</div> </div>
)} )}
{member.ancestralHome && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.ancestralHome}</dd>
</div>
)}
{member.birthPlace && ( {member.birthPlace && (
<div> <div>
<dt className="text-muted-foreground mb-1"></dt> <dt className="text-muted-foreground mb-1"></dt>
@@ -715,27 +715,67 @@ export default function MemberProfilePage() {
<dd className="font-medium">{member.deathPlace}</dd> <dd className="font-medium">{member.deathPlace}</dd>
</div> </div>
)} )}
{member.burialPlace && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.burialPlace}</dd>
</div>
)}
{member.address && ( {member.address && (
<div> <div>
<dt className="text-muted-foreground mb-1"></dt> <dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.address}</dd> <dd className="font-medium">{member.address}</dd>
</div> </div>
)} )}
</dl> {member.occupation && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">{member.occupation}</dd>
</div> </div>
)}
{member.phone && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">
<a href={`tel:${member.phone}`} className="hover:text-primary">{member.phone}</a>
</dd>
</div>
)}
{member.telephone && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">
<a href={`tel:${member.telephone}`} className="hover:text-primary">{member.telephone}</a>
</dd>
</div>
)}
{member.email && (
<div>
<dt className="text-muted-foreground mb-1"></dt>
<dd className="font-medium">
<a href={`mailto:${member.email}`} className="hover:text-primary">{member.email}</a>
</dd>
</div>
)}
</dl>
</ChineseCardContent>
</ChineseCard>
{/* 个人简介 */} {/* 个人简介 */}
{member.bio && ( {member.bio && (
<div className={`prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-4"></h3> <ChineseCardContent className="p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4"></ChineseCardTitle>
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p> <p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
</div> </ChineseCardContent>
</ChineseCard>
)} )}
{/* 标签 */} {/* 标签 */}
{member.tags && member.tags.length > 0 && ( {member.tags && member.tags.length > 0 && (
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}> <ChineseCard variant="default" cornerOrnament className={`shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-4"></h3> <ChineseCardContent className="p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4"></ChineseCardTitle>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{member.tags.map((tag) => ( {member.tags.map((tag) => (
<span <span
@@ -746,17 +786,19 @@ export default function MemberProfilePage() {
</span> </span>
))} ))}
</div> </div>
</div> </ChineseCardContent>
</ChineseCard>
)} )}
</TabsContent> </TabsContent>
{/* 生平影像Tab */} {/* 生平影像Tab */}
<TabsContent value="photos" className="mt-6 space-y-6"> <TabsContent value="photos" className="mt-6 space-y-6">
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-4 flex items-center gap-2"> <ChineseCardContent className="p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
<Images className="h-5 w-5 text-primary" /> <Images className="h-5 w-5 text-primary" />
</h3> </ChineseCardTitle>
<PhotoGallery <PhotoGallery
photos={member.photos} photos={member.photos}
photoIds={member.photoIds} photoIds={member.photoIds}
@@ -766,16 +808,18 @@ export default function MemberProfilePage() {
}} }}
readonly={false} readonly={false}
/> />
</div> </ChineseCardContent>
</ChineseCard>
</TabsContent> </TabsContent>
{/* 家族故事Tab */} {/* 家族故事Tab */}
<TabsContent value="stories" className="mt-6 space-y-6"> <TabsContent value="stories" className="mt-6 space-y-6">
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-4 flex items-center gap-2"> <ChineseCardContent className="p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
<BookOpen className="h-5 w-5 text-primary" /> <BookOpen className="h-5 w-5 text-primary" />
</h3> </ChineseCardTitle>
{member.stories && member.stories.length > 0 ? ( {member.stories && member.stories.length > 0 ? (
<StoryManager <StoryManager
stories={member.stories} stories={member.stories}
@@ -790,25 +834,33 @@ export default function MemberProfilePage() {
</div> </div>
)} )}
</div> </ChineseCardContent>
</ChineseCard>
</TabsContent> </TabsContent>
{/* 时间线Tab - 仅显示视图 */} {/* 时间线Tab - 仅显示视图 */}
<TabsContent value="history" className="mt-6 space-y-6"> <TabsContent value="history" className="mt-6 space-y-6">
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<h3 className="text-lg font-serif font-semibold mb-2"></h3> <ChineseCardContent className="p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-2"></ChineseCardTitle>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{new Date(lastSaveTime).toLocaleString('zh-CN')} {new Date(lastSaveTime).toLocaleString('zh-CN')}
</p> </p>
</div> </ChineseCardContent>
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}> </ChineseCard>
<h3 className="text-lg font-serif font-semibold mb-4"></h3> <ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
<ChineseCardContent className="p-6">
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
<History className="h-5 w-5 text-primary" />
</ChineseCardTitle>
<MemberVersionHistory <MemberVersionHistory
memberId={member.id} memberId={member.id}
treeId={currentTree?.id || ''} treeId={currentTree?.id || ''}
lastUpdated={lastSaveTime} lastUpdated={lastSaveTime}
/> />
</div> </ChineseCardContent>
</ChineseCard>
</TabsContent> </TabsContent>
</div> </div>
</Tabs> </Tabs>
+6 -13
View File
@@ -1,15 +1,17 @@
"use client" "use client"
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle } from "@/components/ui/chinese-card"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import { useFamily } from "@/context/family-context" import { useFamily } from "@/context/family-context"
import { SiteHeader } from "@/components/site-header" import { SiteHeader } from "@/components/site-header"
import { MemberForm } from "@/components/members/member-form" import { MemberForm } from "@/components/members/member-form"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" 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 type { FamilyMember } from "@/types/family"
import { useMemo, useEffect, useState } from "react" import { useMemo, useEffect, useState } from "react"
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import { BackButton } from "@/components/ui/back-button"
export default function NewMemberPage() { export default function NewMemberPage() {
const router = useRouter() const router = useRouter()
@@ -146,21 +148,12 @@ export default function NewMemberPage() {
{/* 固定头部区域 */} {/* 固定头部区域 */}
<div className="flex-shrink-0 pb-4 border-b border-border"> <div className="flex-shrink-0 pb-4 border-b border-border">
{/* 返回按钮 */} {/* 返回按钮 */}
<div className="py-4"> <BackButton onClick={() => router.back()} />
<Button
variant="ghost"
onClick={() => router.back()}
className="gap-2 pl-0 hover:bg-transparent hover:text-primary text-2xl font-bold"
>
<ArrowLeft className="h-6 w-6" />
</Button>
</div>
{/* Header Section */} {/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4"> <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="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={`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`}>
{isFirstMember ? ( {isFirstMember ? (
<span className="text-4xl">👑</span> <span className="text-4xl">👑</span>
) : ( ) : (
@@ -208,7 +201,7 @@ export default function NewMemberPage() {
{/* Tab 导航栏 */} {/* Tab 导航栏 */}
<TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit"> <TabsList className="flex flex-wrap gap-2 bg-muted/70 p-1 w-fit">
<TabsTrigger value="info"></TabsTrigger> <TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger> <TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger> <TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger> <TabsTrigger value="stories"></TabsTrigger>
+16 -9
View File
@@ -6,7 +6,7 @@ import { useFamily } from "@/context/family-context"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button" 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 { Search, Plus, Filter, X, Users, User } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { AvatarDisplay } from "@/components/ui/avatar-display" 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 father = member.fatherId ? allMembers.find(m => m.id === member.fatherId) : null
const mother = member.motherId ? allMembers.find(m => m.id === member.motherId) : 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 ( return (
<div className="relative group"> <div className="relative group">
<Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}> <Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}>
<Card className={`hover:border-primary transition-colors cursor-pointer h-full ${ <ChineseCard variant="default" className={`hover:shadow-lg transition-all cursor-pointer h-full ${cardStyle}`}>
isSpouse <ChineseCardContent className="px-4 py-3 flex items-center gap-5">
? 'bg-pink-50/50 dark:bg-pink-950/20 border-pink-200/50 dark:border-pink-800/30'
: 'bg-blue-50/50 dark:bg-blue-950/20 border-blue-200/50 dark:border-blue-800/30'
}`}>
<CardContent className="px-3 py-2 flex items-center gap-5">
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<AvatarDisplay <AvatarDisplay
imageUrl={member.avatarUrl} imageUrl={member.avatarUrl}
@@ -225,8 +232,8 @@ const MemberCard = memo(function MemberCard({
</div> </div>
)} )}
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</Link> </Link>
<Button <Button
size="sm" size="sm"
+193 -190
View File
@@ -6,6 +6,7 @@ import Image from "next/image"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ChineseCard, ChineseCardContent, ChineseCardHeader, ChineseCardTitle, ChineseCardDescription, ChineseDivider } from "@/components/ui/chinese-card"
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { SiteHeader } from "@/components/site-header" import { SiteHeader } from "@/components/site-header"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
@@ -458,58 +459,58 @@ export default function DashboardPage() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 第一个卡片:家族成员 + 在世/已故 */} {/* 第一个卡片:家族成员 + 在世/已故 */}
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}> <Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}>
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group cursor-pointer"> <ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
<CardContent className="p-5"> <ChineseCardContent className="p-5 pt-6">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p> <p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors"> <div className="chinese-seal chinese-seal-sm">
<Users className="h-5 w-5" /> <Users className="h-4 w-4" />
</div> </div>
</div> </div>
<div className="flex flex-col space-y-1.5"> <div className="flex flex-col space-y-1.5">
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{stats.totalMembers} </span> <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 items-center gap-3 text-sm text-muted-foreground/80 font-medium">
<span className="text-green-600"> {stats.livingMembers}</span> <span className="text-green-600"> {stats.livingMembers}</span>
<span className="text-muted-foreground">·</span> <span className="text-muted-foreground">·</span>
<span className="text-gray-600"> {stats.deceasedMembers}</span> <span className="text-gray-600"> {stats.deceasedMembers}</span>
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" /> </ChineseCard>
</Card>
</Link> </Link>
{/* 第二个卡片:繁衍代数 + 记录年代 + 平均寿命 */} {/* 第二个卡片:繁衍代数 + 记录年代 + 平均寿命 */}
<Link href={currentTree?.id ? `/timeline?treeId=${currentTree.id}` : '#'}> <Link href={currentTree?.id ? `/timeline?treeId=${currentTree.id}` : '#'}>
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group cursor-pointer"> <ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
<CardContent className="p-5"> <ChineseCardContent className="p-5 pt-6">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p> <p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors"> <div className="chinese-seal chinese-seal-sm">
<Clock className="h-5 w-5" /> <Clock className="h-4 w-4" />
</div> </div>
</div> </div>
<div className="flex flex-col space-y-1.5"> <div className="flex flex-col space-y-1.5">
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{stats.maxGeneration} </span> <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 items-center gap-3 text-sm text-muted-foreground/80 font-medium">
<span> {stats.yearsSpan} </span> <span> {stats.yearsSpan} </span>
<span className="text-muted-foreground">·</span> <span className="text-muted-foreground">·</span>
<span>寿 {stats.averageLifespan > 0 ? `${stats.averageLifespan}` : '-'}</span> <span>寿 {stats.averageLifespan > 0 ? `${stats.averageLifespan}` : '-'}</span>
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" /> </ChineseCard>
</Card>
</Link> </Link>
{/* 第三个卡片:性别比例 */} {/* 第三个卡片:性别比例 */}
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}> <Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '#'}>
<Card className="relative overflow-hidden border border-border/50 shadow-sm bg-gradient-to-br from-card to-card/50 backdrop-blur transition-all hover:shadow-lg hover:border-primary/30 group cursor-pointer"> <ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
<CardContent className="p-5"> <ChineseCardContent className="p-5 pt-6">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p> <p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide"></p>
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors"> <div className="chinese-seal chinese-seal-sm">
<Users className="h-5 w-5" /> <Users className="h-4 w-4" />
</div> </div>
</div> </div>
<div className="flex flex-col space-y-1.5"> <div className="flex flex-col space-y-1.5">
@@ -522,13 +523,13 @@ export default function DashboardPage() {
{stats.maleCount > 0 ? (stats.femaleCount / stats.maleCount).toFixed(2) : '0'} {stats.maleCount > 0 ? (stats.femaleCount / stats.maleCount).toFixed(2) : '0'}
</span> </span>
</div> </div>
<ChineseDivider variant="simple" className="my-2" />
<div className="text-center text-sm text-muted-foreground/80 font-medium"> <div className="text-center text-sm text-muted-foreground/80 font-medium">
{stats.maleCount} · {stats.femaleCount} {stats.maleCount} · {stats.femaleCount}
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary/50 via-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" /> </ChineseCard>
</Card>
</Link> </Link>
</div> </div>
</CollapsibleContent> </CollapsibleContent>
@@ -538,10 +539,9 @@ export default function DashboardPage() {
<div className="flex items-center justify-between mb-4 flex-shrink-0"> <div className="flex items-center justify-between mb-4 flex-shrink-0">
<TabsList className="bg-muted/50"> <TabsList className="bg-muted/50">
<TabsTrigger value="photos"></TabsTrigger> <TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="recent"></TabsTrigger> <TabsTrigger value="recent"></TabsTrigger>
<TabsTrigger value="anniversaries"></TabsTrigger>
<TabsTrigger value="statistics"></TabsTrigger> <TabsTrigger value="statistics"></TabsTrigger>
<TabsTrigger value="migration"></TabsTrigger> <TabsTrigger value="migration"></TabsTrigger>
</TabsList> </TabsList>
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '/members'}> <Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '/members'}>
<Button variant="ghost" size="sm" className="text-muted-foreground gap-1"> <Button variant="ghost" size="sm" className="text-muted-foreground gap-1">
@@ -552,15 +552,15 @@ export default function DashboardPage() {
{/* 照片展示标签页 */} {/* 照片展示标签页 */}
<TabsContent value="photos" className="mt-0 flex-1 overflow-y-auto"> <TabsContent value="photos" className="mt-0 flex-1 overflow-y-auto">
<Card className="border-none shadow-sm bg-card/50 backdrop-blur"> <ChineseCard variant="scroll" className="shadow-sm">
<CardHeader> <ChineseCardHeader className="pt-6">
<CardTitle className="font-serif flex items-center gap-2"> <ChineseCardTitle className="flex items-center gap-2">
<Images className="h-5 w-5 text-primary" /> <Images className="h-5 w-5 text-primary" />
</CardTitle> </ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
{allPhotos.length > 0 ? ( {allPhotos.length > 0 ? (
<div className="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 space-y-4"> <div className="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 space-y-4">
{allPhotos.map((photo, index) => ( {allPhotos.map((photo, index) => (
@@ -622,21 +622,22 @@ export default function DashboardPage() {
<p className="text-sm text-muted-foreground/70 mt-1"></p> <p className="text-sm text-muted-foreground/70 mt-1"></p>
</div> </div>
)} )}
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</TabsContent> </TabsContent>
<TabsContent value="recent" className="mt-0 flex-1 overflow-y-auto"> <TabsContent value="recent" className="mt-0 flex-1 overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6"> <div className="space-y-6">
<Card className="border-none shadow-sm bg-primary/5"> <ChineseCard variant="plaque" className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-primary font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-primary">
<Calendar className="h-5 w-5" /> <Calendar className="h-5 w-5" />
/ /
</CardTitle> </ChineseCardTitle>
</CardHeader> <ChineseCardDescription></ChineseCardDescription>
<CardContent className="grid gap-4"> </ChineseCardHeader>
<ChineseCardContent className="grid gap-4">
{monthlyAnniversaries.length > 0 ? ( {monthlyAnniversaries.length > 0 ? (
monthlyAnniversaries.map((anniversary, index) => { monthlyAnniversaries.map((anniversary, index) => {
const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear() const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear()
@@ -688,136 +689,19 @@ export default function DashboardPage() {
<p className="text-sm text-foreground font-light tracking-wide"></p> <p className="text-sm text-foreground font-light tracking-wide"></p>
</div> </div>
)} )}
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</div>
<Card className="border-none shadow-sm bg-card/50 backdrop-blur"> <ChineseCard variant="scroll" className="shadow-sm">
<CardHeader> <ChineseCardHeader className="pt-6">
<CardTitle className="font-serif"></CardTitle> <ChineseCardTitle className="flex items-center gap-2">
<CardDescription></CardDescription> <Clock className="h-5 w-5 text-primary" />
</CardHeader>
<CardContent> </ChineseCardTitle>
<ScrollArea className="h-[400px] pr-4"> <ChineseCardDescription></ChineseCardDescription>
<div className="space-y-4"> </ChineseCardHeader>
{recentActivities.length > 0 ? ( <ChineseCardContent>
recentActivities.map((activity, i) => ( <div className="space-y-3">
<div key={activity.id} className="flex gap-3 group hover:bg-muted/30 p-2 rounded-lg transition-colors">
<div className="relative flex flex-col items-center">
<div className="h-full w-px bg-border group-last:hidden"></div>
<div className={`absolute top-2 h-2 w-2 rounded-full ring-4 ring-background ${
activity.action === 'CREATE' ? 'bg-green-500' :
activity.action === 'UPDATE' ? 'bg-blue-500' :
activity.action === 'DELETE' ? 'bg-red-500' :
'bg-purple-500'
}`}></div>
</div>
<div className="pb-2 flex-1">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded-full ${
activity.action === 'CREATE' ? 'bg-green-100 text-green-700' :
activity.action === 'UPDATE' ? 'bg-blue-100 text-blue-700' :
activity.action === 'DELETE' ? 'bg-red-100 text-red-700' :
'bg-purple-100 text-purple-700'
}`}>
{activity.action === 'CREATE' ? '创建' :
activity.action === 'UPDATE' ? '更新' :
activity.action === 'DELETE' ? '删除' : '导入'}
</span>
{activity.entityId && activity.entityType === 'MEMBER' ? (
(() => {
const member = treeData.members[activity.entityId!]
if (member) {
return (
<MemberNameWithStatus
name={activity.entityName || member.fullName || '未知'}
isDead={!!member.deathDate}
memberId={activity.entityId}
treeId={currentTree?.id}
className="font-medium"
/>
)
}
// 成员不存在(可能已删除),不显示状态点,但保留链接(如果不是删除操作)
return activity.action === 'DELETE' ? (
<span className="font-medium">{activity.entityName || '未知'}</span>
) : (
<Link
href={`/members/${activity.entityId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
className="font-medium hover:text-primary hover:underline transition-colors"
>
{activity.entityName || '未知'}
</Link>
)
})()
) : (
<span className="font-medium">{activity.entityName || '未知'}</span>
)}
<span className="text-xs text-muted-foreground">
{format(new Date(activity.timestamp), 'MM-dd HH:mm')}
</span>
</div>
{activity.action === 'UPDATE' && activity.changes ? (
<div className="text-xs text-muted-foreground space-y-0.5">
{(() => {
const changes = activity.changes as Record<string, { old: any, new: any }>
const fieldLabels: Record<string, string> = {
fullName: '姓名', surname: '姓氏', givenName: '名字', gender: '性别',
birthDate: '出生日期', deathDate: '去世日期', birthPlace: '出生地',
ancestralHome: '祖籍', generation: '世代', generationName: '字辈',
courtesyName: '字', artName: '号', posthumousName: '谥号', rank: '排行',
bio: '简介', phone: '手机', telephone: '电话', email: '邮箱',
address: '地址', photoIds: '照片', spouseIds: '配偶', childrenIds: '子女',
motherId: '母亲', fatherId: '父亲', isFounder: '始祖',
isLunarDate: '农历日期', burialPlace: '安葬地', tags: '标签',
}
const entries = Object.entries(changes)
.filter(([key]) => fieldLabels[key])
.slice(0, 2) // 只显示前2个变更
return entries.length > 0 ? (
<>
{entries.map(([key]) => (
<div key={key}> {fieldLabels[key]}</div>
))}
{Object.keys(changes).length > 2 && (
<div className="italic"> {Object.keys(changes).length} </div>
)}
</>
) : <div></div>
})()}
</div>
) : (
<p className="text-xs text-muted-foreground">
{activity.action === 'CREATE' && '创建了成员'}
{activity.action === 'UPDATE' && '更新了成员'}
{activity.action === 'DELETE' && '删除了成员'}
{activity.action === 'IMPORT' && '导入了数据'}
</p>
)}
</div>
</div>
))
) : (
<div className="text-center py-12">
<p className="text-sm text-foreground font-light tracking-wide"></p>
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
</TabsContent>
{/* 近期纪念日标签页 */}
<TabsContent value="anniversaries" className="mt-0 flex-1 overflow-y-auto">
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
<CardHeader>
<CardTitle className="font-serif"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{(() => { {(() => {
const now = new Date() const now = new Date()
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate()) const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
@@ -928,27 +812,26 @@ export default function DashboardPage() {
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime()) upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
return upcomingEvents.length > 0 ? ( return upcomingEvents.length > 0 ? (
upcomingEvents.map((event, index) => { upcomingEvents.slice(0, 5).map((event, index) => {
const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear() const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear()
const daysUntil = Math.ceil((event.date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return ( return (
<Link href={`/members/${event.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${event.member.id}-${event.type}-${index}`}> <Link href={`/members/${event.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${event.member.id}-${event.type}-${index}`}>
<Card className="hover:border-primary transition-colors cursor-pointer"> <div className="flex items-center gap-3 p-2 rounded-lg bg-background/60 border border-border/50 hover:bg-background transition-colors cursor-pointer">
<CardContent className="p-4">
<div className="flex items-center gap-3">
{event.isLunar && event.lunarDisplay ? ( {event.isLunar && event.lunarDisplay ? (
// 农历日期框 - 红色主题 // 农历日期框 - 红色主题
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-gradient-to-br from-red-50 to-amber-50 border-2 border-red-200/50 text-red-800 font-serif shadow-sm"> <div className="flex flex-col items-center justify-center w-12 h-12 rounded-lg bg-gradient-to-br from-red-50 to-amber-50 border border-red-200/50 text-red-800 font-serif shadow-sm">
<span className="text-[10px] leading-tight text-center px-1"> <span className="text-[9px] leading-tight text-center px-1">
{event.lunarDisplay} {event.lunarDisplay}
</span> </span>
</div> </div>
) : ( ) : (
// 公历日期框 - 蓝色主题 // 公历日期框 - 蓝色主题
<div className="flex flex-col items-center justify-center w-14 h-14 rounded-lg bg-gradient-to-br from-blue-50 to-sky-50 border-2 border-blue-200/50 text-blue-800 font-serif shadow-sm"> <div className="flex flex-col items-center justify-center w-12 h-12 rounded-lg bg-gradient-to-br from-blue-50 to-sky-50 border border-blue-200/50 text-blue-800 font-serif shadow-sm">
<span className="text-xs font-medium"> <span className="text-xs font-medium">
{event.month} {event.month}
</span> </span>
<span className="font-bold text-xl leading-tight">{event.day}</span> <span className="font-bold text-sm leading-tight">{event.day}</span>
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1">
@@ -969,25 +852,145 @@ export default function DashboardPage() {
{event.isLunar && event.lunarDisplay && ( {event.isLunar && event.lunarDisplay && (
<span className="ml-1">· {event.lunarDisplay}</span> <span className="ml-1">· {event.lunarDisplay}</span>
)} )}
<span className="ml-1 text-primary">· {daysUntil}</span>
</p> </p>
</div> </div>
</div> </div>
</CardContent>
</Card>
</Link> </Link>
) )
}) })
) : ( ) : (
<div className="col-span-full text-center py-16"> <div className="text-center py-8">
<p className="text-sm text-foreground font-light tracking-wide"></p> <p className="text-sm text-foreground font-light tracking-wide"></p>
</div> </div>
) )
})()} })()}
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</div>
<ChineseCard variant="default" cornerOrnament className="shadow-sm">
<ChineseCardHeader>
<ChineseCardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-primary" />
</ChineseCardTitle>
<ChineseCardDescription></ChineseCardDescription>
</ChineseCardHeader>
<ChineseCardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-4">
{recentActivities.length > 0 ? (
recentActivities.map((activity, i) => (
<div key={activity.id} className="flex gap-3 group hover:bg-muted/30 p-2 rounded-lg transition-colors">
<div className="relative flex flex-col items-center">
<div className="h-full w-px bg-border group-last:hidden"></div>
<div className={`absolute top-2 h-2 w-2 rounded-full ring-4 ring-background ${
activity.action === 'CREATE' ? 'bg-green-500' :
activity.action === 'UPDATE' ? 'bg-blue-500' :
activity.action === 'DELETE' ? 'bg-red-500' :
'bg-purple-500'
}`}></div>
</div>
<div className="pb-2 flex-1">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded-full ${
activity.action === 'CREATE' ? 'bg-green-100 text-green-700' :
activity.action === 'UPDATE' ? 'bg-blue-100 text-blue-700' :
activity.action === 'DELETE' ? 'bg-red-100 text-red-700' :
'bg-purple-100 text-purple-700'
}`}>
{activity.action === 'CREATE' ? '创建' :
activity.action === 'UPDATE' ? '更新' :
activity.action === 'DELETE' ? '删除' : '导入'}
</span>
{activity.entityId && activity.entityType === 'MEMBER' ? (
(() => {
const member = treeData.members[activity.entityId!]
if (member) {
return (
<MemberNameWithStatus
name={activity.entityName || member.fullName || '未知'}
isDead={!!member.deathDate}
memberId={activity.entityId}
treeId={currentTree?.id}
className="font-medium"
/>
)
}
// 成员不存在(可能已删除),不显示状态点,但保留链接(如果不是删除操作)
return activity.action === 'DELETE' ? (
<span className="font-medium">{activity.entityName || '未知'}</span>
) : (
<Link
href={`/members/${activity.entityId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
className="font-medium hover:text-primary hover:underline transition-colors"
>
{activity.entityName || '未知'}
</Link>
)
})()
) : (
<span className="font-medium">{activity.entityName || '未知'}</span>
)}
<span className="text-xs text-muted-foreground">
{format(new Date(activity.timestamp), 'MM-dd HH:mm')}
</span>
</div>
{activity.action === 'UPDATE' && activity.changes ? (
<div className="text-xs text-muted-foreground space-y-0.5">
{(() => {
const changes = activity.changes as Record<string, { old: any, new: any }>
const fieldLabels: Record<string, string> = {
fullName: '姓名', surname: '姓氏', givenName: '名字', gender: '性别',
birthDate: '出生日期', deathDate: '去世日期', birthPlace: '出生地',
ancestralHome: '祖籍', generation: '世代', generationName: '字辈',
courtesyName: '字', artName: '号', posthumousName: '谥号', rank: '排行',
bio: '简介', phone: '手机', telephone: '电话', email: '邮箱',
address: '地址', photoIds: '照片', spouseIds: '配偶', childrenIds: '子女',
motherId: '母亲', fatherId: '父亲', isFounder: '始祖',
isLunarDate: '农历日期', burialPlace: '安葬地', tags: '标签',
}
const entries = Object.entries(changes)
.filter(([key]) => fieldLabels[key])
.slice(0, 2) // 只显示前2个变更
return entries.length > 0 ? (
<>
{entries.map(([key]) => (
<div key={key}> {fieldLabels[key]}</div>
))}
{Object.keys(changes).length > 2 && (
<div className="italic"> {Object.keys(changes).length} </div>
)}
</>
) : <div></div>
})()}
</div>
) : (
<p className="text-xs text-muted-foreground">
{activity.action === 'CREATE' && '创建了成员'}
{activity.action === 'UPDATE' && '更新了成员'}
{activity.action === 'DELETE' && '删除了成员'}
{activity.action === 'IMPORT' && '导入了数据'}
</p>
)}
</div>
</div>
))
) : (
<div className="text-center py-12">
<p className="text-sm text-foreground font-light tracking-wide"></p>
</div>
)}
</div>
</ScrollArea>
</ChineseCardContent>
</ChineseCard>
</div>
</TabsContent> </TabsContent>
{/* 统计图表标签页 */} {/* 统计图表标签页 */}
<TabsContent value="statistics" className="mt-0 flex-1 overflow-y-auto"> <TabsContent value="statistics" className="mt-0 flex-1 overflow-y-auto">
<StatisticsCharts members={Object.values(treeData.members)} /> <StatisticsCharts members={Object.values(treeData.members)} />
@@ -995,12 +998,12 @@ export default function DashboardPage() {
{/* 迁徙记录标签页 */} {/* 迁徙记录标签页 */}
<TabsContent value="migration" className="mt-0 flex-1 overflow-y-auto"> <TabsContent value="migration" className="mt-0 flex-1 overflow-y-auto">
<Card className="border-none shadow-sm bg-card/50 backdrop-blur"> <ChineseCard variant="bamboo" className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle className="font-serif"></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div className="space-y-6"> <div className="space-y-6">
{locationGroups.length > 0 ? ( {locationGroups.length > 0 ? (
locationGroups.map(([location, locationMembers]) => ( locationGroups.map(([location, locationMembers]) => (
@@ -1025,12 +1028,12 @@ export default function DashboardPage() {
)) ))
) : ( ) : (
<div className="text-center py-16"> <div className="text-center py-16">
<p className="text-sm text-foreground font-light tracking-wide"></p> <p className="text-sm text-foreground font-light tracking-wide"></p>
</div> </div>
)} )}
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
+2 -2
View File
@@ -83,8 +83,8 @@ export default function RelationshipTestPage() {
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
{/* 标题 */} {/* 标题 */}
<div> <div>
<h1 className="text-2xl font-serif font-bold mb-1"></h1> <h1 className="text-3xl font-serif font-bold"></h1>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground">
</p> </p>
</div> </div>
+19 -19
View File
@@ -2,7 +2,7 @@
import { SiteHeader } from "@/components/site-header" import { SiteHeader } from "@/components/site-header"
import { useFamily } from "@/context/family-context" import { useFamily } from "@/context/family-context"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 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 { ScrollArea } from "@/components/ui/scroll-area"
import { Clock, Map as MapIcon, ArrowRight, Calendar, User, Loader2 } from 'lucide-react' import { Clock, Map as MapIcon, ArrowRight, Calendar, User, Loader2 } from 'lucide-react'
import { useMemo, useEffect, useState, useRef, useCallback } from 'react' import { useMemo, useEffect, useState, useRef, useCallback } from 'react'
@@ -601,8 +601,8 @@ export default function TimelinePage() {
</TabsList> </TabsList>
<TabsContent value="timeline" className="flex-1 overflow-y-auto"> <TabsContent value="timeline" className="flex-1 overflow-y-auto">
<Card className="border-none shadow-none bg-transparent"> <ChineseCard variant="scroll" className="border-none shadow-none bg-transparent">
<CardContent className="p-0"> <ChineseCardContent className="p-0">
<div className="relative border-l-2 border-primary/20 ml-6 md:ml-12 space-y-8 py-4"> <div className="relative border-l-2 border-primary/20 ml-6 md:ml-12 space-y-8 py-4">
{events.map((event, idx) => ( {events.map((event, idx) => (
<div key={idx} className="relative pl-8 md:pl-12 group"> <div key={idx} className="relative pl-8 md:pl-12 group">
@@ -713,17 +713,17 @@ export default function TimelinePage() {
</div> </div>
)} )}
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</TabsContent> </TabsContent>
<TabsContent value="migration" className="flex-1 overflow-y-auto"> <TabsContent value="migration" className="flex-1 overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<Card> <ChineseCard variant="bamboo" className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle className="font-serif"></CardTitle> <ChineseCardTitle className="font-serif"></ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<ScrollArea className="h-[600px] pr-4"> <ScrollArea className="h-[600px] pr-4">
<div className="space-y-6"> <div className="space-y-6">
{migrations.map((mig, idx) => ( {migrations.map((mig, idx) => (
@@ -770,14 +770,14 @@ export default function TimelinePage() {
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
<Card className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20"> <ChineseCard variant="default" cornerOrnament className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20 shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle className="font-serif"></CardTitle> <ChineseCardTitle className="font-serif"></ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div className="relative h-[600px] overflow-hidden rounded-lg bg-background/50 backdrop-blur border border-border/50"> <div className="relative h-[600px] overflow-hidden rounded-lg bg-background/50 backdrop-blur border border-border/50">
{!mapLoaded ? ( {!mapLoaded ? (
<div className="flex items-center justify-center h-full text-muted-foreground"> <div className="flex items-center justify-center h-full text-muted-foreground">
@@ -793,8 +793,8 @@ export default function TimelinePage() {
/> />
)} )}
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</div> </div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
+26 -26
View File
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import Link from "next/link" 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 { Button } from "@/components/ui/button"
import { Users, Calendar, Plus, ArrowRight } from "lucide-react" import { Users, Calendar, Plus, ArrowRight } from "lucide-react"
import { format } from "date-fns" import { format } from "date-fns"
@@ -47,41 +47,41 @@ export function FamilyTreesList() {
if (!session) { if (!session) {
return ( return (
<Card> <ChineseCard variant="plaque" className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<Link href="/auth/signin"> <Link href="/auth/signin">
<Button></Button> <Button></Button>
</Link> </Link>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
) )
} }
if (loading) { if (loading) {
return ( return (
<Card> <ChineseCard variant="plaque" className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<p className="text-muted-foreground">...</p> <p className="text-muted-foreground">...</p>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
) )
} }
return ( return (
<Card> <ChineseCard variant="plaque" className="shadow-sm">
<CardHeader className="flex flex-row items-center justify-between"> <ChineseCardHeader className="flex flex-row items-center justify-between">
<div> <div>
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription> <ChineseCardDescription>
{trees.length > 0 ? `${trees.length} 个家族树` : '还没有家族树'} {trees.length > 0 ? `${trees.length} 个家族树` : '还没有家族树'}
</CardDescription> </ChineseCardDescription>
</div> </div>
<Link href="/trees/new"> <Link href="/trees/new">
<Button size="sm"> <Button size="sm">
@@ -89,8 +89,8 @@ export function FamilyTreesList() {
</Button> </Button>
</Link> </Link>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
{trees.length === 0 ? ( {trees.length === 0 ? (
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-muted-foreground mb-4"></p> <p className="text-muted-foreground mb-4"></p>
@@ -106,7 +106,7 @@ export function FamilyTreesList() {
{trees.map((tree) => ( {trees.map((tree) => (
<div <div
key={tree.id} key={tree.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors" className="flex items-center justify-between p-4 rounded-lg border bg-card/50 hover:bg-accent/50 transition-colors"
> >
<div className="flex-1"> <div className="flex-1">
<h3 className="font-semibold text-lg">{tree.name}</h3> <h3 className="font-semibold text-lg">{tree.name}</h3>
@@ -134,7 +134,7 @@ export function FamilyTreesList() {
))} ))}
</div> </div>
)} )}
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
) )
} }
+33 -33
View File
@@ -13,7 +13,7 @@ import {
import { CanvasRenderer } from "echarts/renderers" import { CanvasRenderer } from "echarts/renderers"
import { graphic } from "echarts/core" import { graphic } from "echarts/core"
import type { FamilyMember } from "@/types/family" 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([ echarts.use([
@@ -330,45 +330,45 @@ export function StatisticsCharts({ members }: StatisticsChartsProps) {
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card> <ChineseCard variant="default" cornerOrnament className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div ref={genderChartRef} className="h-[300px]" /> <div ref={genderChartRef} className="h-[300px]" />
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
<Card> <ChineseCard variant="default" cornerOrnament className="shadow-sm">
<CardHeader> <ChineseCardHeader>
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div ref={generationChartRef} className="h-[300px]" /> <div ref={generationChartRef} className="h-[300px]" />
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
<Card> <ChineseCard variant="scroll" className="shadow-sm">
<CardHeader> <ChineseCardHeader className="pt-6">
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div ref={ageDistributionRef} className="h-[300px]" /> <div ref={ageDistributionRef} className="h-[300px]" />
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
<Card> <ChineseCard variant="scroll" className="shadow-sm">
<CardHeader> <ChineseCardHeader className="pt-6">
<CardTitle></CardTitle> <ChineseCardTitle></ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div ref={birthYearChartRef} className="h-[300px]" /> <div ref={birthYearChartRef} className="h-[300px]" />
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</div> </div>
) )
} }
+192 -128
View File
@@ -11,7 +11,7 @@ import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { CalendarIcon, User, Scroll, Users, Images, BookOpen } from "lucide-react" 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 { Switch } from "@/components/ui/switch"
import { ImageUpload } from "@/components/ui/image-upload" import { ImageUpload } from "@/components/ui/image-upload"
import { PhotoGallery } from "./photo-gallery" import { PhotoGallery } from "./photo-gallery"
@@ -290,6 +290,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
spouseFatherName: formData.spouseFatherName, spouseFatherName: formData.spouseFatherName,
spouseMotherName: formData.spouseMotherName, spouseMotherName: formData.spouseMotherName,
phone: formData.phone, phone: formData.phone,
telephone: formData.telephone,
email: formData.email,
occupation: formData.occupation, occupation: formData.occupation,
courtesyName: formData.courtesyName, courtesyName: formData.courtesyName,
artName: formData.artName, artName: formData.artName,
@@ -298,8 +300,10 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
birthDate: formData.birthDate, birthDate: formData.birthDate,
deathDate: formData.deathDate, deathDate: formData.deathDate,
isLunarDate: formData.isLunarDate || false, isLunarDate: formData.isLunarDate || false,
ancestralHome: formData.ancestralHome,
birthPlace: formData.birthPlace, birthPlace: formData.birthPlace,
deathPlace: formData.deathPlace, deathPlace: formData.deathPlace,
burialPlace: formData.burialPlace,
address: formData.address, address: formData.address,
bio: formData.bio, bio: formData.bio,
avatarUrl: formData.avatarUrl, avatarUrl: formData.avatarUrl,
@@ -337,7 +341,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
{!hideTabs && ( {!hideTabs && (
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="info"></TabsTrigger> <TabsTrigger value="info"></TabsTrigger>
<TabsTrigger value="details"></TabsTrigger> <TabsTrigger value="details"></TabsTrigger>
<TabsTrigger value="photos"></TabsTrigger> <TabsTrigger value="photos"></TabsTrigger>
<TabsTrigger value="stories"></TabsTrigger> <TabsTrigger value="stories"></TabsTrigger>
@@ -346,17 +350,17 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
)} )}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{/* 详细信息Tab - 包含基本信息和家庭关系 */} {/* 基本信息Tab - 包含基本信息和家庭关系 */}
<TabsContent value="info" className="space-y-6 mt-6 w-full"> <TabsContent value="info" className="space-y-6 mt-6 w-full">
{/* 基本信息卡片 */} {/* 基本信息卡片 */}
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<User className="h-5 w-5" /> <User className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent className="space-y-6"> <ChineseCardContent className="space-y-6">
{/* 头像和基本信息两栏布局 */} {/* 头像和基本信息两栏布局 */}
<div className="grid grid-cols-1 lg:grid-cols-[200px_1fr] gap-8"> <div className="grid grid-cols-1 lg:grid-cols-[200px_1fr] gap-8">
{/* 左侧:头像 */} {/* 左侧:头像 */}
@@ -367,89 +371,88 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
/> />
</div> </div>
{/* 右侧:基本表单字段 */} {/* 右侧:表单 */}
<div className="space-y-4"> <div className="space-y-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="surname"> <span className="text-destructive">*</span></Label> <Label htmlFor="surname">
<span className="text-red-500">*</span>
</Label>
<Input <Input
id="surname" id="surname"
value={formData.surname} value={formData.surname || ""}
onChange={(e) => handleChange("surname", e.target.value)} onChange={(e) => handleChange("surname", e.target.value)}
placeholder="例如:李"
className={errors.includes("surname") ? "border-red-500" : ""}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="givenName"> <span className="text-destructive">*</span></Label> <Label htmlFor="givenName">
<span className="text-red-500">*</span>
</Label>
<Input <Input
id="givenName" id="givenName"
value={formData.givenName} value={formData.givenName || ""}
onChange={(e) => handleChange("givenName", e.target.value)} onChange={(e) => handleChange("givenName", e.target.value)}
/> placeholder="例如:明"
</div> className={errors.includes("givenName") ? "border-red-500" : ""}
<div className="space-y-2">
<Label htmlFor="gender"> <span className="text-destructive">*</span></Label>
<Select value={formData.gender} onValueChange={(val) => handleChange("gender", val)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="MALE"></SelectItem>
<SelectItem value="FEMALE"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="generation"> <span className="text-destructive">*</span></Label>
<Input
id="generation"
type="number"
value={formData.generation}
onChange={(e) => handleChange("generation", Number.parseInt(e.target.value))}
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="phone"></Label> <Label></Label>
<Input <RadioGroup
id="phone" value={formData.gender}
value={formData.phone || ""} onValueChange={(val) => handleChange("gender", val)}
onChange={(e) => handleChange("phone", e.target.value)} className="flex space-x-4"
placeholder="请输入联系电话" >
/> <div className="flex items-center space-x-2">
<RadioGroupItem value="MALE" id="male" />
<Label htmlFor="male"></Label>
</div> </div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="FEMALE" id="female" />
<Label htmlFor="female"></Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="occupation"></Label> <Label htmlFor="generation"> ()</Label>
<div className="flex items-center gap-4">
<Input <Input
id="occupation" id="generation"
value={formData.occupation || ""} type="number"
onChange={(e) => handleChange("occupation", e.target.value)} value={formData.generation || ""}
placeholder="请输入职业" onChange={(e) => handleChange("generation", parseInt(e.target.value) || 0)}
className="w-32"
min={1}
/> />
<span className="text-sm text-muted-foreground"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
{/* 家庭关系 - 左右两个独立卡片 */} {/* 家庭关系 - 左右两个独立卡片 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 左栏:父母 */} {/* 左栏:父母 */}
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<Users className="h-5 w-5" /> <Users className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
{!isFounder && ( {!isFounder && (
<CardDescription className="text-xs"> <ChineseCardDescription className="text-xs">
</CardDescription> </ChineseCardDescription>
)} )}
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="fatherId"></Label> <Label htmlFor="fatherId"></Label>
@@ -558,23 +561,23 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
)} )}
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
{/* 右栏:配偶和子女 */} {/* 右栏:配偶和子女 */}
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<Users className="h-5 w-5" /> <Users className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
{!isFounder && ( {!isFounder && (
<CardDescription className="text-xs"> <ChineseCardDescription className="text-xs">
</CardDescription> </ChineseCardDescription>
)} )}
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="spouseId"></Label> <Label htmlFor="spouseId"></Label>
@@ -697,8 +700,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
)} )}
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</div> </div>
</TabsContent> </TabsContent>
@@ -706,14 +709,14 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
<TabsContent value="details" className="space-y-6 mt-6 w-full"> <TabsContent value="details" className="space-y-6 mt-6 w-full">
{/* 使用两列布局放置两个卡片 */} {/* 使用两列布局放置两个卡片 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" /> <Scroll className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent className="space-y-4"> <ChineseCardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="courtesyName"></Label> <Label htmlFor="courtesyName"></Label>
@@ -752,17 +755,17 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
/> />
</div> </div>
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<CalendarIcon className="h-5 w-5" /> <CalendarIcon className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent className="space-y-4"> <ChineseCardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
<DateInputWithLunar <DateInputWithLunar
id="birthDate" id="birthDate"
@@ -781,20 +784,29 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
showLunarToggle={false} showLunarToggle={false}
/> />
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</div> </div>
{/* 地点和简介卡片 */} {/* 地点和简介卡片 */}
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" /> <Scroll className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
</CardHeader> </ChineseCardHeader>
<CardContent className="space-y-4"> <ChineseCardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="ancestralHome"></Label>
<Input
id="ancestralHome"
value={formData.ancestralHome || ""}
onChange={(e) => handleChange("ancestralHome", e.target.value)}
placeholder="请输入籍贯"
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="birthPlace"></Label> <Label htmlFor="birthPlace"></Label>
<Input <Input
@@ -804,6 +816,18 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
placeholder="请输入出生地" placeholder="请输入出生地"
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="address"></Label>
<Input
id="address"
value={formData.address || ""}
onChange={(e) => handleChange("address", e.target.value)}
placeholder="请输入居住地"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="deathPlace"></Label> <Label htmlFor="deathPlace"></Label>
<Input <Input
@@ -814,12 +838,52 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="address"></Label> <Label htmlFor="burialPlace"></Label>
<Input <Input
id="address" id="burialPlace"
value={formData.address || ""} value={formData.burialPlace || ""}
onChange={(e) => handleChange("address", e.target.value)} onChange={(e) => handleChange("burialPlace", e.target.value)}
placeholder="请输入居住地" placeholder="请输入安葬地"
/>
</div>
<div className="space-y-2">
<Label htmlFor="occupation"></Label>
<Input
id="occupation"
value={formData.occupation || ""}
onChange={(e) => handleChange("occupation", e.target.value)}
placeholder="请输入职业"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={formData.phone || ""}
onChange={(e) => handleChange("phone", e.target.value)}
placeholder="请输入手机号"
/>
</div>
<div className="space-y-2">
<Label htmlFor="telephone"></Label>
<Input
id="telephone"
value={formData.telephone || ""}
onChange={(e) => handleChange("telephone", e.target.value)}
placeholder="请输入固定电话"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email || ""}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="请输入电子邮箱"
/> />
</div> </div>
</div> </div>
@@ -834,57 +898,57 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
rows={4} rows={4}
/> />
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</TabsContent> </TabsContent>
{/* 生平影像Tab */} {/* 生平影像Tab */}
<TabsContent value="photos" className="space-y-6 mt-6 w-full"> <TabsContent value="photos" className="space-y-6 mt-6 w-full">
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<Images className="h-5 w-5" /> <Images className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<PhotoGallery <PhotoGallery
photos={formData.photos} photos={formData.photos}
photoIds={formData.photoIds} photoIds={formData.photoIds}
onChange={handlePhotosChange} onChange={handlePhotosChange}
/> />
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</TabsContent> </TabsContent>
{/* 家族故事Tab */} {/* 家族故事Tab */}
<TabsContent value="stories" className="space-y-6 mt-6 w-full"> <TabsContent value="stories" className="space-y-6 mt-6 w-full">
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<BookOpen className="h-5 w-5" /> <BookOpen className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
<CardDescription></CardDescription> <ChineseCardDescription></ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent> <ChineseCardContent>
<StoryManager <StoryManager
stories={formData.stories || []} stories={formData.stories || []}
onChange={(stories) => handleChange("stories", stories)} onChange={(stories) => handleChange("stories", stories)}
/> />
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
<Card className={`w-full ${founderCardClass}`}> <ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
<CardHeader> <ChineseCardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif"> <ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" /> <Scroll className="h-5 w-5" />
</CardTitle> </ChineseCardTitle>
<CardDescription>便</CardDescription> <ChineseCardDescription>便</ChineseCardDescription>
</CardHeader> </ChineseCardHeader>
<CardContent className="space-y-4"> <ChineseCardContent className="space-y-4">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
@@ -925,8 +989,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
</div> </div>
)} )}
</div> </div>
</CardContent> </ChineseCardContent>
</Card> </ChineseCard>
</TabsContent> </TabsContent>
</div> </div>
</Tabs> </Tabs>
@@ -146,7 +146,7 @@ export function MemberVersionHistory({ memberId, treeId, onVersionSelect, lastUp
{/* 其他信息变更 */} {/* 其他信息变更 */}
{changedOthers.length > 0 && ( {changedOthers.length > 0 && (
<div> <div>
<h4 className="text-xs font-semibold text-foreground mb-2"></h4> <h4 className="text-xs font-semibold text-foreground mb-2"></h4>
<div className="space-y-2 text-xs"> <div className="space-y-2 text-xs">
{changedOthers.map(([field, change]) => ( {changedOthers.map(([field, change]) => (
<div key={field} className="flex items-start gap-2"> <div key={field} className="flex items-start gap-2">
+25
View File
@@ -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 (
<div className="py-4">
<Button
variant="ghost"
onClick={onClick}
className={`gap-2 pl-0 hover:bg-transparent hover:text-primary text-2xl font-bold ${className}`}
>
<ArrowLeft className="h-6 w-6" />
{label}
</Button>
</div>
)
}
+207
View File
@@ -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 (
<div
data-slot="chinese-card"
data-variant={variant}
className={cn(
// 基础样式
'relative bg-card text-card-foreground flex flex-col',
// 变体样式
variant === 'default' && 'chinese-card-default',
variant === 'scroll' && 'chinese-card-scroll',
variant === 'plaque' && 'chinese-card-plaque',
variant === 'portrait' && 'chinese-card-portrait',
variant === 'bamboo' && 'chinese-card-bamboo',
// 角饰
cornerOrnament && 'chinese-card-corner-ornament',
className,
)}
{...props}
>
{/* 顶部装饰条 */}
{headerDecoration && (
<div className="chinese-card-header-decoration" />
)}
{/* 角饰元素 */}
{cornerOrnament && (
<>
<span className="chinese-corner chinese-corner-tl" />
<span className="chinese-corner chinese-corner-tr" />
<span className="chinese-corner chinese-corner-bl" />
<span className="chinese-corner chinese-corner-br" />
</>
)}
{children}
</div>
)
}
function ChineseCardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="chinese-card-header"
className={cn(
'flex flex-col gap-1.5 px-5 pt-5 pb-3',
className,
)}
{...props}
/>
)
}
function ChineseCardTitle({ className, ...props }: React.ComponentProps<'h3'>) {
return (
<h3
data-slot="chinese-card-title"
className={cn(
'font-serif text-lg font-semibold leading-tight tracking-wide',
className,
)}
{...props}
/>
)
}
function ChineseCardDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="chinese-card-description"
className={cn(
'font-serif text-sm text-muted-foreground',
className,
)}
{...props}
/>
)
}
function ChineseCardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="chinese-card-content"
className={cn('px-5 pb-5', className)}
{...props}
/>
)
}
function ChineseCardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="chinese-card-footer"
className={cn(
'flex items-center px-5 pb-5 pt-0',
className,
)}
{...props}
/>
)
}
/**
* 中国风分隔线
*/
function ChineseDivider({
className,
variant = 'wave',
...props
}: React.ComponentProps<'div'> & {
variant?: 'wave' | 'cloud' | 'meander' | 'simple'
}) {
return (
<div
data-slot="chinese-divider"
className={cn(
'chinese-divider',
variant === 'wave' && 'chinese-divider-wave',
variant === 'cloud' && 'chinese-divider-cloud',
variant === 'meander' && 'chinese-divider-meander',
variant === 'simple' && 'chinese-divider-simple',
className,
)}
{...props}
/>
)
}
/**
* 统计数字组件 - 用于展示大数字
*/
function ChineseStatNumber({
className,
children,
...props
}: React.ComponentProps<'span'>) {
return (
<span
className={cn(
'text-3xl font-serif font-bold tracking-tight text-foreground',
className,
)}
{...props}
>
{children}
</span>
)
}
/**
* 统计标签组件
*/
function ChineseStatLabel({
className,
children,
...props
}: React.ComponentProps<'span'>) {
return (
<span
className={cn(
'text-sm font-medium text-muted-foreground uppercase tracking-wider',
className,
)}
{...props}
>
{children}
</span>
)
}
export {
ChineseCard,
ChineseCardHeader,
ChineseCardTitle,
ChineseCardDescription,
ChineseCardContent,
ChineseCardFooter,
ChineseDivider,
ChineseStatNumber,
ChineseStatLabel,
type ChineseCardVariant,
}
+26 -1
View File
@@ -237,7 +237,32 @@ function FamilyProviderInner({ children }: { children: React.ReactNode }) {
const data = await response.json() 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<string, FamilyMember> = {}
membersData.members.forEach((m: FamilyMember) => {
membersMap[m.id] = m
})
setTreeData(prev => ({
...prev,
members: membersMap,
}))
return
}
}
// 如果没有关系变更,只更新当前成员
setTreeData(prev => ({ setTreeData(prev => ({
...prev, ...prev,
members: { members: {
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.