0.3.1.2
This commit is contained in:
+1
-1
@@ -222,7 +222,7 @@ export default function AdminPage() {
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push("/")}>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">用户管理</h1>
|
||||
<h1 className="text-3xl font-serif font-bold">用户管理</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+416
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+341
-289
@@ -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() {
|
||||
{/* 固定头部区域 */}
|
||||
<div className="flex-shrink-0 pb-4 border-b border-border">
|
||||
{/* 返回按钮 */}
|
||||
<div className="py-4">
|
||||
<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>
|
||||
<BackButton onClick={() => setIsEditing(false)} label="返回" />
|
||||
|
||||
{/* Header Section */}
|
||||
<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 导航栏 */}
|
||||
<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="photos">生平影像</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="py-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
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"
|
||||
>
|
||||
<ArrowLeft className="h-6 w-6" />
|
||||
返回成员列表
|
||||
</Button>
|
||||
</div>
|
||||
<BackButton
|
||||
onClick={() => router.push(`/members${typeof window !== 'undefined' ? window.location.search : ''}`)}
|
||||
label="返回"
|
||||
/>
|
||||
|
||||
{/* Header Section */}
|
||||
<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 导航栏 */}
|
||||
<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="photos">生平影像</TabsTrigger>
|
||||
<TabsTrigger value="stories">家族故事</TabsTrigger>
|
||||
@@ -511,304 +506,361 @@ export default function MemberProfilePage() {
|
||||
|
||||
{/* Tab 内容区域 - 可滚动 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* 详细信息Tab - 包含基本信息和家庭关系 */}
|
||||
{/* 基本信息Tab - 包含基本信息和家庭关系 */}
|
||||
<TabsContent value="info" className="mt-6 space-y-6">
|
||||
{/* 基本信息 */}
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">基本信息</h3>
|
||||
<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>
|
||||
<dt className="text-muted-foreground mb-1">姓名</dt>
|
||||
<dd className="font-medium text-lg">{member.fullName}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">性别</dt>
|
||||
<dd className="font-medium">{member.gender === 'MALE' ? '男' : '女'}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">世系</dt>
|
||||
<dd className="font-medium">第 {member.generation} 世</dd>
|
||||
</div>
|
||||
{member.phone && (
|
||||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||||
<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">
|
||||
<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>
|
||||
<dt className="text-muted-foreground mb-1">姓名</dt>
|
||||
<dd className="font-medium text-lg">{member.fullName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.occupation && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">职业</dt>
|
||||
<dd className="font-medium">{member.occupation}</dd>
|
||||
<dt className="text-muted-foreground mb-1">性别</dt>
|
||||
<dd className="font-medium">{member.gender === 'MALE' ? '男' : '女'}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">世系</dt>
|
||||
<dd className="font-medium">第 {member.generation} 世</dd>
|
||||
</div>
|
||||
{member.occupation && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">职业</dt>
|
||||
<dd className="font-medium">{member.occupation}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
{/* 家庭关系 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider">
|
||||
父母
|
||||
</h3>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">父亲</p>
|
||||
{member.fatherId ? (
|
||||
<Link
|
||||
href={`/members/${member.fatherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(member.fatherId)?.fullName || member.fatherId}
|
||||
isDead={!!getMember(member.fatherId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
) : member.spouseFatherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseFatherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">母亲</p>
|
||||
{member.motherId ? (
|
||||
<Link
|
||||
href={`/members/${member.motherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(member.motherId)?.fullName || member.motherId}
|
||||
isDead={!!getMember(member.motherId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
) : member.spouseMotherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseMotherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`bg-card p-4 rounded-lg ${founderBorderClass}`}>
|
||||
<h3 className="font-medium text-muted-foreground mb-3 text-sm uppercase tracking-wider">
|
||||
配偶与子女
|
||||
</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">配偶</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.spouseIds?.length > 0 ? (
|
||||
member.spouseIds.map((spouseId) => (
|
||||
<Link
|
||||
href={`/members/${spouseId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
key={spouseId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(spouseId)?.fullName || spouseId}
|
||||
isDead={!!getMember(spouseId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
<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>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">父亲</p>
|
||||
{member.fatherId ? (
|
||||
<Link
|
||||
href={`/members/${member.fatherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(member.fatherId)?.fullName || member.fatherId}
|
||||
isDead={!!getMember(member.fatherId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
) : member.spouseFatherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseFatherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-1">母亲</p>
|
||||
{member.motherId ? (
|
||||
<Link
|
||||
href={`/members/${member.motherId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
className="text-lg font-serif font-bold hover:underline text-primary"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(member.motherId)?.fullName || member.motherId}
|
||||
isDead={!!getMember(member.motherId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
) : member.spouseMotherName ? (
|
||||
<span className="text-lg font-serif font-bold text-foreground">
|
||||
{member.spouseMotherName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">子女</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.childrenIds?.length > 0 ? (
|
||||
member.childrenIds.map((childId) => (
|
||||
<Link
|
||||
href={`/members/${childId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
key={childId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(childId)?.fullName || childId}
|
||||
isDead={!!getMember(childId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</ChineseCardContent>
|
||||
</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>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">配偶</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.spouseIds?.length > 0 ? (
|
||||
member.spouseIds.map((spouseId) => (
|
||||
<Link
|
||||
href={`/members/${spouseId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
key={spouseId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(spouseId)?.fullName || spouseId}
|
||||
isDead={!!getMember(spouseId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-widest mb-2">子女</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.childrenIds?.length > 0 ? (
|
||||
member.childrenIds.map((childId) => (
|
||||
<Link
|
||||
href={`/members/${childId}${typeof window !== 'undefined' ? window.location.search : ''}`}
|
||||
key={childId}
|
||||
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={getMember(childId)?.fullName || childId}
|
||||
isDead={!!getMember(childId)?.deathDate}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">无记录</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 详细资料Tab */}
|
||||
<TabsContent value="details" className="mt-6 space-y-6">
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">传统称谓与生平</h3>
|
||||
<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>
|
||||
<dt className="text-muted-foreground mb-1">生卒</dt>
|
||||
<dd className="font-medium space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>出生:{member.birthDate || "-"}</span>
|
||||
{member.birthDate && member.isLunarDate && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||||
<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">
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">生卒</dt>
|
||||
<dd className="font-medium space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>出生:{member.birthDate || "-"}</span>
|
||||
{member.birthDate && member.isLunarDate && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>逝世:{member.deathDate || "-"}</span>
|
||||
{member.deathDate && member.isLunarDate && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
{ageInfo && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">{ageInfo.type === 'deceased' ? '享年' : '年龄'}</dt>
|
||||
<dd className="font-medium text-primary">{ageInfo.age} 岁</dd>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>逝世:{member.deathDate || "-"}</span>
|
||||
{member.deathDate && member.isLunarDate && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
{member.generationName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">字辈</dt>
|
||||
<dd className="font-medium">{member.generationName}</dd>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
{ageInfo && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">{ageInfo.type === 'deceased' ? '享年' : '年龄'}</dt>
|
||||
<dd className="font-medium text-primary">{ageInfo.age} 岁</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.generationName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">字辈</dt>
|
||||
<dd className="font-medium">{member.generationName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.courtesyName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">字</dt>
|
||||
<dd className="font-medium">{member.courtesyName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.artName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">号</dt>
|
||||
<dd className="font-medium">{member.artName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.posthumousName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">谥号</dt>
|
||||
<dd className="font-medium">{member.posthumousName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.birthPlace && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">出生地</dt>
|
||||
<dd className="font-medium">{member.birthPlace}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.deathPlace && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">逝世地</dt>
|
||||
<dd className="font-medium">{member.deathPlace}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.address && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">居住地</dt>
|
||||
<dd className="font-medium">{member.address}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
{member.courtesyName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">字</dt>
|
||||
<dd className="font-medium">{member.courtesyName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.artName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">号</dt>
|
||||
<dd className="font-medium">{member.artName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.posthumousName && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">谥号</dt>
|
||||
<dd className="font-medium">{member.posthumousName}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.ancestralHome && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">籍贯</dt>
|
||||
<dd className="font-medium">{member.ancestralHome}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.birthPlace && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">出生地</dt>
|
||||
<dd className="font-medium">{member.birthPlace}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.deathPlace && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">逝世地</dt>
|
||||
<dd className="font-medium">{member.deathPlace}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.burialPlace && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">安葬地</dt>
|
||||
<dd className="font-medium">{member.burialPlace}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.address && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">居住地</dt>
|
||||
<dd className="font-medium">{member.address}</dd>
|
||||
</div>
|
||||
)}
|
||||
{member.occupation && (
|
||||
<div>
|
||||
<dt className="text-muted-foreground mb-1">职业</dt>
|
||||
<dd className="font-medium">{member.occupation}</dd>
|
||||
</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 && (
|
||||
<div className={`prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">个人简介</h3>
|
||||
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
|
||||
</div>
|
||||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||||
<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>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
)}
|
||||
|
||||
{/* 标签 */}
|
||||
{member.tags && member.tags.length > 0 && (
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-md border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ChineseCard variant="default" cornerOrnament className={`shadow-sm ${cardStyle}`}>
|
||||
<ChineseCardContent className="p-6">
|
||||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-4">标签</ChineseCardTitle>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{member.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-md border border-primary/20"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 生平影像Tab */}
|
||||
<TabsContent value="photos" className="mt-6 space-y-6">
|
||||
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||||
<Images className="h-5 w-5 text-primary" />
|
||||
影像记忆
|
||||
</h3>
|
||||
<PhotoGallery
|
||||
photos={member.photos}
|
||||
photoIds={member.photoIds}
|
||||
onChange={async (photos) => {
|
||||
await updateMember(member.id, { photos, photoIds: undefined })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
readonly={false}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 家族故事Tab */}
|
||||
<TabsContent value="stories" className="mt-6 space-y-6">
|
||||
<div className={`bg-card p-6 rounded-lg ${founderBorderClass} shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4 flex items-center gap-2">
|
||||
<BookOpen className="h-5 w-5 text-primary" />
|
||||
家族故事
|
||||
</h3>
|
||||
{member.stories && member.stories.length > 0 ? (
|
||||
<StoryManager
|
||||
stories={member.stories}
|
||||
onChange={async (stories) => {
|
||||
await updateMember(member.id, { stories })
|
||||
<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">
|
||||
<Images className="h-5 w-5 text-primary" />
|
||||
影像记忆
|
||||
</ChineseCardTitle>
|
||||
<PhotoGallery
|
||||
photos={member.photos}
|
||||
photoIds={member.photoIds}
|
||||
onChange={async (photos) => {
|
||||
await updateMember(member.id, { photos, photoIds: undefined })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
readonly={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center py-10">
|
||||
暂无故事记录,期待您书写这段家族记忆。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
|
||||
{/* 家族故事Tab */}
|
||||
<TabsContent value="stories" className="mt-6 space-y-6">
|
||||
<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">
|
||||
<BookOpen className="h-5 w-5 text-primary" />
|
||||
家族故事
|
||||
</ChineseCardTitle>
|
||||
{member.stories && member.stories.length > 0 ? (
|
||||
<StoryManager
|
||||
stories={member.stories}
|
||||
onChange={async (stories) => {
|
||||
await updateMember(member.id, { stories })
|
||||
setLastSaveTime(Date.now())
|
||||
}}
|
||||
readonly={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center py-10">
|
||||
暂无故事记录,期待您书写这段家族记忆。
|
||||
</div>
|
||||
)}
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
|
||||
{/* 时间线Tab - 仅显示视图 */}
|
||||
<TabsContent value="history" className="mt-6 space-y-6">
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-2">最近更新</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
最后保存于 {new Date(lastSaveTime).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`bg-card rounded-lg ${founderBorderClass} p-6 shadow-sm w-full`}>
|
||||
<h3 className="text-lg font-serif font-semibold mb-4">版本历史</h3>
|
||||
<MemberVersionHistory
|
||||
memberId={member.id}
|
||||
treeId={currentTree?.id || ''}
|
||||
lastUpdated={lastSaveTime}
|
||||
/>
|
||||
</div>
|
||||
<ChineseCard variant="default" cornerOrnament className={`w-full shadow-sm ${cardStyle}`}>
|
||||
<ChineseCardContent className="p-6">
|
||||
<ChineseCardTitle className="text-lg font-serif font-semibold mb-2">最近更新</ChineseCardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
最后保存于 {new Date(lastSaveTime).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
<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
|
||||
memberId={member.id}
|
||||
treeId={currentTree?.id || ''}
|
||||
lastUpdated={lastSaveTime}
|
||||
/>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
@@ -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() {
|
||||
{/* 固定头部区域 */}
|
||||
<div className="flex-shrink-0 pb-4 border-b border-border">
|
||||
{/* 返回按钮 */}
|
||||
<div className="py-4">
|
||||
<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>
|
||||
<BackButton onClick={() => router.back()} />
|
||||
|
||||
{/* Header Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start gap-6 pb-4">
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="h-24 w-24 rounded-lg bg-muted flex items-center justify-center border-2 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 ? (
|
||||
<span className="text-4xl">👑</span>
|
||||
) : (
|
||||
@@ -208,7 +201,7 @@ export default function NewMemberPage() {
|
||||
|
||||
{/* Tab 导航栏 */}
|
||||
<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="photos">生平影像</TabsTrigger>
|
||||
<TabsTrigger value="stories">家族故事</TabsTrigger>
|
||||
|
||||
+16
-9
@@ -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 (
|
||||
<div className="relative group">
|
||||
<Link href={`/members/${member.id}${typeof window !== 'undefined' ? window.location.search : ''}`}>
|
||||
<Card className={`hover:border-primary transition-colors cursor-pointer h-full ${
|
||||
isSpouse
|
||||
? '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">
|
||||
<ChineseCard variant="default" className={`hover:shadow-lg transition-all cursor-pointer h-full ${cardStyle}`}>
|
||||
<ChineseCardContent className="px-4 py-3 flex items-center gap-5">
|
||||
<div className="relative flex-shrink-0">
|
||||
<AvatarDisplay
|
||||
imageUrl={member.avatarUrl}
|
||||
@@ -225,8 +232,8 @@ const MemberCard = memo(function MemberCard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</Link>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
+240
-237
@@ -6,6 +6,7 @@ import Image from "next/image"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Button } from "@/components/ui/button"
|
||||
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 { SiteHeader } from "@/components/site-header"
|
||||
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">
|
||||
{/* 第一个卡片:家族成员 + 在世/已故 */}
|
||||
<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">
|
||||
<CardContent className="p-5">
|
||||
<ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
|
||||
<ChineseCardContent className="p-5 pt-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">家族成员</p>
|
||||
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors">
|
||||
<Users className="h-5 w-5" />
|
||||
<div className="chinese-seal chinese-seal-sm">
|
||||
<Users className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{stats.totalMembers} 人</span>
|
||||
<ChineseDivider variant="simple" className="my-2" />
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground/80 font-medium">
|
||||
<span className="text-green-600">在世 {stats.livingMembers}</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="text-gray-600">已故 {stats.deceasedMembers}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<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" />
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</Link>
|
||||
|
||||
{/* 第二个卡片:繁衍代数 + 记录年代 + 平均寿命 */}
|
||||
<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">
|
||||
<CardContent className="p-5">
|
||||
<ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
|
||||
<ChineseCardContent className="p-5 pt-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">家族历史</p>
|
||||
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors">
|
||||
<Clock className="h-5 w-5" />
|
||||
<div className="chinese-seal chinese-seal-sm">
|
||||
<Clock className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<span className="text-3xl font-serif font-bold text-foreground tracking-tight">{stats.maxGeneration} 代</span>
|
||||
<ChineseDivider variant="simple" className="my-2" />
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground/80 font-medium">
|
||||
<span>跨越 {stats.yearsSpan} 年</span>
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span>均寿 {stats.averageLifespan > 0 ? `${stats.averageLifespan}岁` : '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<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" />
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</Link>
|
||||
|
||||
{/* 第三个卡片:性别比例 */}
|
||||
<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">
|
||||
<CardContent className="p-5">
|
||||
<ChineseCard variant="default" cornerOrnament className="chinese-stat-card transition-all hover:shadow-lg group cursor-pointer h-full">
|
||||
<ChineseCardContent className="p-5 pt-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">性别比例</p>
|
||||
<div className="text-primary/70 bg-primary/10 p-2.5 rounded-lg group-hover:bg-primary/20 transition-colors">
|
||||
<Users className="h-5 w-5" />
|
||||
<div className="chinese-seal chinese-seal-sm">
|
||||
<Users className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<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'}
|
||||
</span>
|
||||
</div>
|
||||
<ChineseDivider variant="simple" className="my-2" />
|
||||
<div className="text-center text-sm text-muted-foreground/80 font-medium">
|
||||
男性 {stats.maleCount} · 女性 {stats.femaleCount}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<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" />
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</Link>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
@@ -538,10 +539,9 @@ export default function DashboardPage() {
|
||||
<div className="flex items-center justify-between mb-4 flex-shrink-0">
|
||||
<TabsList className="bg-muted/50">
|
||||
<TabsTrigger value="photos">照片展示</TabsTrigger>
|
||||
<TabsTrigger value="recent">最近更新</TabsTrigger>
|
||||
<TabsTrigger value="anniversaries">近期纪念日</TabsTrigger>
|
||||
<TabsTrigger value="recent">最近动态</TabsTrigger>
|
||||
<TabsTrigger value="statistics">统计图表</TabsTrigger>
|
||||
<TabsTrigger value="migration">迁徙记录</TabsTrigger>
|
||||
<TabsTrigger value="migration">籍贯记录</TabsTrigger>
|
||||
</TabsList>
|
||||
<Link href={currentTree?.id ? `/members?treeId=${currentTree.id}` : '/members'}>
|
||||
<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">
|
||||
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif flex items-center gap-2">
|
||||
<ChineseCard variant="scroll" className="shadow-sm">
|
||||
<ChineseCardHeader className="pt-6">
|
||||
<ChineseCardTitle className="flex items-center gap-2">
|
||||
<Images className="h-5 w-5 text-primary" />
|
||||
家族影像
|
||||
</CardTitle>
|
||||
<CardDescription>珍藏的家族照片,记录美好时光</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</ChineseCardTitle>
|
||||
<ChineseCardDescription>珍藏的家族照片,记录美好时光</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
{allPhotos.length > 0 ? (
|
||||
<div className="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 space-y-4">
|
||||
{allPhotos.map((photo, index) => (
|
||||
@@ -622,21 +622,22 @@ export default function DashboardPage() {
|
||||
<p className="text-sm text-muted-foreground/70 mt-1">在成员详情页可以添加照片</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
|
||||
<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="space-y-6">
|
||||
<Card className="border-none shadow-sm bg-primary/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-primary font-serif">
|
||||
<ChineseCard variant="plaque" className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-primary">
|
||||
<Calendar className="h-5 w-5" />
|
||||
本月祭祖/纪念
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
</ChineseCardTitle>
|
||||
<ChineseCardDescription>本月需要纪念的家族成员生日与忌日</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent className="grid gap-4">
|
||||
{monthlyAnniversaries.length > 0 ? (
|
||||
monthlyAnniversaries.map((anniversary, index) => {
|
||||
const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear()
|
||||
@@ -688,17 +689,197 @@ export default function DashboardPage() {
|
||||
<p className="text-sm text-foreground font-light tracking-wide">此月无忆</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<ChineseCard variant="scroll" className="shadow-sm">
|
||||
<ChineseCardHeader className="pt-6">
|
||||
<ChineseCardTitle className="flex items-center gap-2">
|
||||
<Clock className="h-5 w-5 text-primary" />
|
||||
未来三月纪念日
|
||||
</ChineseCardTitle>
|
||||
<ChineseCardDescription>即将到来的生日与忌日提醒</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div className="space-y-3">
|
||||
{(() => {
|
||||
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.slice(0, 5).map((event, index) => {
|
||||
const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear()
|
||||
const daysUntil = Math.ceil((event.date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
return (
|
||||
<Link href={`/members/${event.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${event.member.id}-${event.type}-${index}`}>
|
||||
<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">
|
||||
{event.isLunar && event.lunarDisplay ? (
|
||||
// 农历日期框 - 红色主题
|
||||
<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-[9px] leading-tight text-center px-1">
|
||||
{event.lunarDisplay}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
// 公历日期框 - 蓝色主题
|
||||
<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">
|
||||
{event.month}月
|
||||
</span>
|
||||
<span className="font-bold text-sm leading-tight">{event.day}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm flex items-center gap-2">
|
||||
<MemberNameWithStatus
|
||||
name={event.member.fullName}
|
||||
isDead={!!event.member.deathDate}
|
||||
/> {event.type === 'birth' ? '诞辰' : '忌日'}
|
||||
{event.isLunar && (
|
||||
<span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
第 {event.member.generation} 世 · {yearsAgo} 年
|
||||
{event.type === 'death' && ' · 已故'}
|
||||
{event.isLunar && event.lunarDisplay && (
|
||||
<span className="ml-1">· {event.lunarDisplay}</span>
|
||||
)}
|
||||
<span className="ml-1 text-primary">· {daysUntil}天后</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-sm text-foreground font-light tracking-wide">来日无期</p>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</div>
|
||||
|
||||
<Card className="border-none shadow-sm bg-card/50 backdrop-blur">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">近期修谱动态</CardTitle>
|
||||
<CardDescription>记录家族成员的最新变动与修订</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<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) => (
|
||||
@@ -804,189 +985,11 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</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 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 (
|
||||
<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">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{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">
|
||||
<span className="text-[10px] leading-tight text-center px-1">
|
||||
{event.lunarDisplay}
|
||||
</span>
|
||||
</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">
|
||||
<span className="text-xs font-medium">
|
||||
{event.month}月
|
||||
</span>
|
||||
<span className="font-bold text-xl leading-tight">{event.day}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm flex items-center gap-2">
|
||||
<MemberNameWithStatus
|
||||
name={event.member.fullName}
|
||||
isDead={!!event.member.deathDate}
|
||||
/> {event.type === 'birth' ? '诞辰' : '忌日'}
|
||||
{event.isLunar && (
|
||||
<span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
||||
农历
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
第 {event.member.generation} 世 · {yearsAgo} 年
|
||||
{event.type === 'death' && ' · 已故'}
|
||||
{event.isLunar && event.lunarDisplay && (
|
||||
<span className="ml-1">· {event.lunarDisplay}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="col-span-full text-center py-16">
|
||||
<p className="text-sm text-foreground font-light tracking-wide">来日无期</p>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 统计图表标签页 */}
|
||||
<TabsContent value="statistics" className="mt-0 flex-1 overflow-y-auto">
|
||||
@@ -995,12 +998,12 @@ export default function DashboardPage() {
|
||||
|
||||
{/* 迁徙记录标签页 */}
|
||||
<TabsContent value="migration" 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>
|
||||
<ChineseCard variant="bamboo" className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle>家族籍贯记录</ChineseCardTitle>
|
||||
<ChineseCardDescription>记录家族成员的籍贯分布</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div className="space-y-6">
|
||||
{locationGroups.length > 0 ? (
|
||||
locationGroups.map(([location, locationMembers]) => (
|
||||
@@ -1025,12 +1028,12 @@ export default function DashboardPage() {
|
||||
))
|
||||
) : (
|
||||
<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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ export default function RelationshipTestPage() {
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* 标题 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-serif font-bold mb-1">家族关系计算器</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h1 className="text-3xl font-serif font-bold">家族关系计算器</h1>
|
||||
<p className="text-muted-foreground">
|
||||
选择两个家族成员,自动计算他们之间的亲属关系
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+19
-19
@@ -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() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="timeline" className="flex-1 overflow-y-auto">
|
||||
<Card className="border-none shadow-none bg-transparent">
|
||||
<CardContent className="p-0">
|
||||
<ChineseCard variant="scroll" className="border-none shadow-none bg-transparent">
|
||||
<ChineseCardContent className="p-0">
|
||||
<div className="relative border-l-2 border-primary/20 ml-6 md:ml-12 space-y-8 py-4">
|
||||
{events.map((event, idx) => (
|
||||
<div key={idx} className="relative pl-8 md:pl-12 group">
|
||||
@@ -713,17 +713,17 @@ export default function TimelinePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="migration" className="flex-1 overflow-y-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">迁徙记录</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="bamboo" className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="font-serif">迁徙记录</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<ScrollArea className="h-[600px] pr-4">
|
||||
<div className="space-y-6">
|
||||
{migrations.map((mig, idx) => (
|
||||
@@ -770,14 +770,14 @@ export default function TimelinePage() {
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<Card className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">迁徙路径图</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="default" cornerOrnament className="bg-gradient-to-br from-primary/5 via-background to-secondary/5 border-primary/20 shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="font-serif">迁徙路径图</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div className="relative h-[600px] overflow-hidden rounded-lg bg-background/50 backdrop-blur border border-border/50">
|
||||
{!mapLoaded ? (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
@@ -793,8 +793,8 @@ export default function TimelinePage() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>我的家族树</CardTitle>
|
||||
<CardDescription>请先登录查看您的家族树</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="plaque" className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle>我的家族树</ChineseCardTitle>
|
||||
<ChineseCardDescription>请先登录查看您的家族树</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<Link href="/auth/signin">
|
||||
<Button>登录</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>我的家族树</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="plaque" className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle>我的家族树</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<p className="text-muted-foreground">加载中...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<ChineseCard variant="plaque" className="shadow-sm">
|
||||
<ChineseCardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>我的家族树</CardTitle>
|
||||
<CardDescription>
|
||||
<ChineseCardTitle>我的家族树</ChineseCardTitle>
|
||||
<ChineseCardDescription>
|
||||
{trees.length > 0 ? `共 ${trees.length} 个家族树` : '还没有家族树'}
|
||||
</CardDescription>
|
||||
</ChineseCardDescription>
|
||||
</div>
|
||||
<Link href="/trees/new">
|
||||
<Button size="sm">
|
||||
@@ -89,8 +89,8 @@ export function FamilyTreesList() {
|
||||
创建家族树
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
{trees.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground mb-4">您还没有创建任何家族树</p>
|
||||
@@ -106,7 +106,7 @@ export function FamilyTreesList() {
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
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">
|
||||
<h3 className="font-semibold text-lg">{tree.name}</h3>
|
||||
@@ -134,7 +134,7 @@ export function FamilyTreesList() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>性别分布</CardTitle>
|
||||
<CardDescription>家族成员性别比例</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle>性别分布</ChineseCardTitle>
|
||||
<ChineseCardDescription>家族成员性别比例</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div ref={genderChartRef} className="h-[300px]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>世代分布</CardTitle>
|
||||
<CardDescription>各世代人数统计</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle>世代分布</ChineseCardTitle>
|
||||
<ChineseCardDescription>各世代人数统计</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div ref={generationChartRef} className="h-[300px]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>年龄分布</CardTitle>
|
||||
<CardDescription>按年龄段划分的人数</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="scroll" className="shadow-sm">
|
||||
<ChineseCardHeader className="pt-6">
|
||||
<ChineseCardTitle>年龄分布</ChineseCardTitle>
|
||||
<ChineseCardDescription>按年龄段划分的人数</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div ref={ageDistributionRef} className="h-[300px]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>出生年代分布</CardTitle>
|
||||
<CardDescription>各年代出生人数趋势</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChineseCard variant="scroll" className="shadow-sm">
|
||||
<ChineseCardHeader className="pt-6">
|
||||
<ChineseCardTitle>出生年代分布</ChineseCardTitle>
|
||||
<ChineseCardDescription>各年代出生人数趋势</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div ref={birthYearChartRef} className="h-[300px]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+195
-131
@@ -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 && (
|
||||
<div className="flex-shrink-0">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="info">详细信息</TabsTrigger>
|
||||
<TabsTrigger value="info">基本信息</TabsTrigger>
|
||||
<TabsTrigger value="details">详细资料</TabsTrigger>
|
||||
<TabsTrigger value="photos">生平影像</TabsTrigger>
|
||||
<TabsTrigger value="stories">家族故事</TabsTrigger>
|
||||
@@ -346,17 +350,17 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* 详细信息Tab - 包含基本信息和家庭关系 */}
|
||||
{/* 基本信息Tab - 包含基本信息和家庭关系 */}
|
||||
<TabsContent value="info" className="space-y-6 mt-6 w-full">
|
||||
{/* 基本信息卡片 */}
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<User className="h-5 w-5" />
|
||||
基本信息
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent className="space-y-6">
|
||||
{/* 头像和基本信息两栏布局 */}
|
||||
<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 className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{/* 右侧:表单 */}
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<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
|
||||
id="surname"
|
||||
value={formData.surname}
|
||||
value={formData.surname || ""}
|
||||
onChange={(e) => handleChange("surname", e.target.value)}
|
||||
placeholder="例如:李"
|
||||
className={errors.includes("surname") ? "border-red-500" : ""}
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
id="givenName"
|
||||
value={formData.givenName}
|
||||
value={formData.givenName || ""}
|
||||
onChange={(e) => handleChange("givenName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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))}
|
||||
placeholder="例如:明"
|
||||
className={errors.includes("givenName") ? "border-red-500" : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">联系电话</Label>
|
||||
<div className="space-y-2">
|
||||
<Label>性别</Label>
|
||||
<RadioGroup
|
||||
value={formData.gender}
|
||||
onValueChange={(val) => handleChange("gender", val)}
|
||||
className="flex space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="MALE" id="male" />
|
||||
<Label htmlFor="male">男</Label>
|
||||
</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">
|
||||
<Label htmlFor="generation">世代 (第几世)</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone || ""}
|
||||
onChange={(e) => handleChange("phone", e.target.value)}
|
||||
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="请输入职业"
|
||||
id="generation"
|
||||
type="number"
|
||||
value={formData.generation || ""}
|
||||
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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
{/* 家庭关系 - 左右两个独立卡片 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 左栏:父母 */}
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Users className="h-5 w-5" />
|
||||
父母
|
||||
</CardTitle>
|
||||
</ChineseCardTitle>
|
||||
{!isFounder && (
|
||||
<CardDescription className="text-xs">
|
||||
<ChineseCardDescription className="text-xs">
|
||||
需关联父母或配偶以连接至家族树
|
||||
</CardDescription>
|
||||
</ChineseCardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fatherId">父亲</Label>
|
||||
@@ -558,23 +561,23 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
{/* 右栏:配偶和子女 */}
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Users className="h-5 w-5" />
|
||||
配偶与子女
|
||||
</CardTitle>
|
||||
</ChineseCardTitle>
|
||||
{!isFounder && (
|
||||
<CardDescription className="text-xs">
|
||||
<ChineseCardDescription className="text-xs">
|
||||
需关联父母或配偶以连接至家族树
|
||||
</CardDescription>
|
||||
</ChineseCardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="spouseId">配偶</Label>
|
||||
@@ -697,8 +700,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -706,14 +709,14 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
<TabsContent value="details" className="space-y-6 mt-6 w-full">
|
||||
{/* 使用两列布局放置两个卡片 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
传统称谓
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="courtesyName">字</Label>
|
||||
@@ -752,17 +755,17 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
生平日期
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<DateInputWithLunar
|
||||
id="birthDate"
|
||||
@@ -781,20 +784,29 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
showLunarToggle={false}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</div>
|
||||
|
||||
{/* 地点和简介卡片 */}
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
地点与简介
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</ChineseCardTitle>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent className="space-y-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">
|
||||
<Label htmlFor="birthPlace">出生地</Label>
|
||||
<Input
|
||||
@@ -804,6 +816,18 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
placeholder="请输入出生地"
|
||||
/>
|
||||
</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">
|
||||
<Label htmlFor="deathPlace">逝世地</Label>
|
||||
<Input
|
||||
@@ -814,12 +838,52 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">居住地</Label>
|
||||
<Label htmlFor="burialPlace">安葬地</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={formData.address || ""}
|
||||
onChange={(e) => handleChange("address", e.target.value)}
|
||||
placeholder="请输入居住地"
|
||||
id="burialPlace"
|
||||
value={formData.burialPlace || ""}
|
||||
onChange={(e) => handleChange("burialPlace", e.target.value)}
|
||||
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>
|
||||
@@ -834,57 +898,57 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
|
||||
{/* 生平影像Tab */}
|
||||
<TabsContent value="photos" className="space-y-6 mt-6 w-full">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Images className="h-5 w-5" />
|
||||
照片墙
|
||||
</CardTitle>
|
||||
<CardDescription>上传多张照片记录珍贵时刻</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</ChineseCardTitle>
|
||||
<ChineseCardDescription>上传多张照片记录珍贵时刻</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<PhotoGallery
|
||||
photos={formData.photos}
|
||||
photoIds={formData.photoIds}
|
||||
onChange={handlePhotosChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
|
||||
{/* 家族故事Tab */}
|
||||
<TabsContent value="stories" className="space-y-6 mt-6 w-full">
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
家族故事
|
||||
</CardTitle>
|
||||
<CardDescription>记录与此成员相关的家族故事</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</ChineseCardTitle>
|
||||
<ChineseCardDescription>记录与此成员相关的家族故事</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
<StoryManager
|
||||
stories={formData.stories || []}
|
||||
onChange={(stories) => handleChange("stories", stories)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
|
||||
<Card className={`w-full ${founderCardClass}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm w-full">
|
||||
<ChineseCardHeader>
|
||||
<ChineseCardTitle className="flex items-center gap-2 text-lg font-serif">
|
||||
<Scroll className="h-5 w-5" />
|
||||
标签
|
||||
</CardTitle>
|
||||
<CardDescription>添加标签以便分类和搜索(按回车添加)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
</ChineseCardTitle>
|
||||
<ChineseCardDescription>添加标签以便分类和搜索(按回车添加)</ChineseCardDescription>
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -925,8 +989,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ChineseCardContent>
|
||||
</ChineseCard>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
@@ -146,7 +146,7 @@ export function MemberVersionHistory({ memberId, treeId, onVersionSelect, lastUp
|
||||
{/* 其他信息变更 */}
|
||||
{changedOthers.length > 0 && (
|
||||
<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">
|
||||
{changedOthers.map(([field, change]) => (
|
||||
<div key={field} className="flex items-start gap-2">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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<string, FamilyMember> = {}
|
||||
membersData.members.forEach((m: FamilyMember) => {
|
||||
membersMap[m.id] = m
|
||||
})
|
||||
setTreeData(prev => ({
|
||||
...prev,
|
||||
members: membersMap,
|
||||
}))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有关系变更,只更新当前成员
|
||||
setTreeData(prev => ({
|
||||
...prev,
|
||||
members: {
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Reference in New Issue
Block a user