0.8.4.0
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+365
-204
@@ -71,6 +71,7 @@ export default function DashboardPage() {
|
|||||||
const [recentActivities, setRecentActivities] = useState<ActivityLog[]>([])
|
const [recentActivities, setRecentActivities] = useState<ActivityLog[]>([])
|
||||||
const [selectedPhoto, setSelectedPhoto] = useState<string | null>(null)
|
const [selectedPhoto, setSelectedPhoto] = useState<string | null>(null)
|
||||||
const [statsExpanded, setStatsExpanded] = useState(true)
|
const [statsExpanded, setStatsExpanded] = useState(true)
|
||||||
|
const [recentActivityExpanded, setRecentActivityExpanded] = useState(false)
|
||||||
const [adminToggleLoading, setAdminToggleLoading] = useState<string | null>(null)
|
const [adminToggleLoading, setAdminToggleLoading] = useState<string | null>(null)
|
||||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||||
const [editForm, setEditForm] = useState({ name: '', description: '' })
|
const [editForm, setEditForm] = useState({ name: '', description: '' })
|
||||||
@@ -330,7 +331,9 @@ export default function DashboardPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 按日期排序
|
// 按日期排序
|
||||||
return anniversaries.sort((a, b) => a.day - b.day).slice(0, 5)
|
const sorted = anniversaries.sort((a, b) => a.day - b.day)
|
||||||
|
|
||||||
|
return sorted
|
||||||
}, [treeData])
|
}, [treeData])
|
||||||
|
|
||||||
// 计算家族迁徙记录
|
// 计算家族迁徙记录
|
||||||
@@ -801,8 +804,9 @@ export default function DashboardPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="recent" className="mt-0 flex-1 overflow-y-auto">
|
<TabsContent value="recent" className="mt-0 flex-1 overflow-y-auto">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-6">
|
{/* 第一行:本月祭祖和未来三月纪念日 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<ChineseCard variant="plaque" className="shadow-sm">
|
<ChineseCard variant="plaque" className="shadow-sm">
|
||||||
<ChineseCardHeader>
|
<ChineseCardHeader>
|
||||||
<ChineseCardTitle className="flex items-center gap-2 text-primary">
|
<ChineseCardTitle className="flex items-center gap-2 text-primary">
|
||||||
@@ -811,53 +815,125 @@ export default function DashboardPage() {
|
|||||||
</ChineseCardTitle>
|
</ChineseCardTitle>
|
||||||
<ChineseCardDescription>本月需要纪念的家族成员生日与忌日</ChineseCardDescription>
|
<ChineseCardDescription>本月需要纪念的家族成员生日与忌日</ChineseCardDescription>
|
||||||
</ChineseCardHeader>
|
</ChineseCardHeader>
|
||||||
<ChineseCardContent className="grid gap-4">
|
<ChineseCardContent className="max-h-full overflow-y-auto">
|
||||||
{monthlyAnniversaries.length > 0 ? (
|
{monthlyAnniversaries.length > 0 ? (
|
||||||
monthlyAnniversaries.map((anniversary, index) => {
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear()
|
{/* 诞辰列 */}
|
||||||
return (
|
<div className="space-y-3">
|
||||||
<Link href={`/members/${anniversary.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${anniversary.member.id}-${anniversary.type}`}>
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
<div className="flex items-center gap-4 p-3 rounded-lg bg-background/60 border border-border/50 hover:bg-background transition-colors cursor-pointer">
|
<span className="w-1 h-4 bg-green-500 rounded"></span>
|
||||||
{anniversary.isLunar && anniversary.lunarDisplay ? (
|
诞辰
|
||||||
// 农历日期框 - 红色主题
|
<span className="ml-auto text-xs font-normal text-muted-foreground bg-green-50 px-2 py-1 rounded border border-green-200">
|
||||||
<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">
|
{monthlyAnniversaries.filter(a => a.type === 'birth').length}
|
||||||
<span className="text-[10px] leading-tight text-center px-1">
|
</span>
|
||||||
{anniversary.lunarDisplay.replace('月', '')}
|
</h3>
|
||||||
</span>
|
<div className="space-y-2 max-h-[calc(5*70px)] overflow-y-auto pr-2">
|
||||||
</div>
|
{monthlyAnniversaries.filter(a => a.type === 'birth').length > 0 ? (
|
||||||
) : (
|
monthlyAnniversaries.filter(a => a.type === 'birth').map((anniversary) => {
|
||||||
// 公历日期框 - 蓝色主题
|
const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear()
|
||||||
<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">
|
return (
|
||||||
<span className="text-xs font-medium">
|
<Link href={`/members/${anniversary.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${anniversary.member.id}-${anniversary.type}`}>
|
||||||
{anniversary.month}月
|
<div className="flex items-center gap-3 p-2.5 rounded-lg bg-background/60 border border-border/50 hover:bg-background transition-colors cursor-pointer">
|
||||||
</span>
|
{anniversary.isLunar && anniversary.lunarDisplay ? (
|
||||||
<span className="font-bold text-xl leading-tight">{anniversary.day}</span>
|
<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-2 border-red-200/50 text-red-800 font-serif shadow-sm flex-shrink-0">
|
||||||
</div>
|
<span className="text-[9px] leading-tight text-center px-1">
|
||||||
)}
|
{anniversary.lunarDisplay.replace('月', '')}
|
||||||
<div className="flex-1">
|
</span>
|
||||||
<p className="font-medium flex items-center gap-2">
|
</div>
|
||||||
<MemberNameWithStatus
|
) : (
|
||||||
name={anniversary.member.fullName}
|
<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-2 border-blue-200/50 text-blue-800 font-serif shadow-sm flex-shrink-0">
|
||||||
isDead={!!anniversary.member.deathDate}
|
<span className="text-[10px] font-medium">
|
||||||
/> {anniversary.type === 'birth' ? '诞辰' : '忌日'}
|
{anniversary.month}月
|
||||||
{anniversary.isLunar && (
|
</span>
|
||||||
<span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
<span className="font-bold text-sm leading-tight">{anniversary.day}</span>
|
||||||
农历
|
</div>
|
||||||
</span>
|
)}
|
||||||
)}
|
<div className="flex-1 min-w-0">
|
||||||
</p>
|
<p className="font-medium text-sm flex items-center gap-1 flex-wrap">
|
||||||
<p className="text-sm text-muted-foreground">
|
<MemberNameWithStatus
|
||||||
第 {anniversary.member.generation} 世 · 距今 {yearsAgo} 年
|
name={anniversary.member.fullName}
|
||||||
{anniversary.type === 'death' && ' · 已故'}
|
isDead={!!anniversary.member.deathDate}
|
||||||
{anniversary.isLunar && anniversary.lunarDisplay && (
|
/>
|
||||||
<span className="ml-1">· {anniversary.lunarDisplay}</span>
|
{anniversary.isLunar && (
|
||||||
)}
|
<span className="text-[9px] text-amber-600 bg-amber-50 px-1 py-0.5 rounded border border-amber-200 flex-shrink-0">
|
||||||
</p>
|
农历
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
第 {anniversary.member.generation} 世 · {yearsAgo} 年
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-xs text-muted-foreground">此月无诞辰</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</Link>
|
</div>
|
||||||
)
|
</div>
|
||||||
})
|
|
||||||
|
{/* 忌日列 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<span className="w-1 h-4 bg-gray-500 rounded"></span>
|
||||||
|
忌日
|
||||||
|
<span className="ml-auto text-xs font-normal text-muted-foreground bg-gray-100 px-2 py-1 rounded border border-gray-200">
|
||||||
|
{monthlyAnniversaries.filter(a => a.type === 'death').length}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 max-h-[calc(5*70px)] overflow-y-auto pr-2">
|
||||||
|
{monthlyAnniversaries.filter(a => a.type === 'death').length > 0 ? (
|
||||||
|
monthlyAnniversaries.filter(a => a.type === 'death').map((anniversary) => {
|
||||||
|
const yearsAgo = new Date().getFullYear() - new Date(anniversary.date).getFullYear()
|
||||||
|
return (
|
||||||
|
<Link href={`/members/${anniversary.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${anniversary.member.id}-${anniversary.type}`}>
|
||||||
|
<div className="flex items-center gap-3 p-2.5 rounded-lg bg-background/60 border border-border/50 hover:bg-background transition-colors cursor-pointer">
|
||||||
|
{anniversary.isLunar && anniversary.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-2 border-red-200/50 text-red-800 font-serif shadow-sm flex-shrink-0">
|
||||||
|
<span className="text-[9px] leading-tight text-center px-1">
|
||||||
|
{anniversary.lunarDisplay.replace('月', '')}
|
||||||
|
</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-2 border-blue-200/50 text-blue-800 font-serif shadow-sm flex-shrink-0">
|
||||||
|
<span className="text-[10px] font-medium">
|
||||||
|
{anniversary.month}月
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-sm leading-tight">{anniversary.day}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-sm flex items-center gap-1 flex-wrap">
|
||||||
|
<MemberNameWithStatus
|
||||||
|
name={anniversary.member.fullName}
|
||||||
|
isDead={!!anniversary.member.deathDate}
|
||||||
|
/>
|
||||||
|
{anniversary.isLunar && (
|
||||||
|
<span className="text-[9px] text-amber-600 bg-amber-50 px-1 py-0.5 rounded border border-amber-200 flex-shrink-0">
|
||||||
|
农历
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
第 {anniversary.member.generation} 世 · {yearsAgo} 年
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-xs text-muted-foreground">此月无忌日</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-sm text-foreground font-light tracking-wide">此月无忆</p>
|
<p className="text-sm text-foreground font-light tracking-wide">此月无忆</p>
|
||||||
@@ -875,184 +951,267 @@ export default function DashboardPage() {
|
|||||||
<ChineseCardDescription>即将到来的生日与忌日提醒</ChineseCardDescription>
|
<ChineseCardDescription>即将到来的生日与忌日提醒</ChineseCardDescription>
|
||||||
</ChineseCardHeader>
|
</ChineseCardHeader>
|
||||||
<ChineseCardContent>
|
<ChineseCardContent>
|
||||||
<div className="space-y-3">
|
{(() => {
|
||||||
{(() => {
|
const now = new Date()
|
||||||
const now = new Date()
|
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
|
||||||
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
|
const members = Object.values(treeData.members)
|
||||||
const members = Object.values(treeData.members)
|
const upcomingEvents: Array<{
|
||||||
const upcomingEvents: Array<{
|
member: any
|
||||||
member: any
|
type: 'birth' | 'death'
|
||||||
type: 'birth' | 'death'
|
date: Date
|
||||||
date: Date
|
originalDate: string
|
||||||
originalDate: string
|
isLunar: boolean
|
||||||
isLunar: boolean
|
lunarDisplay?: string
|
||||||
lunarDisplay?: string
|
month: number
|
||||||
month: number
|
day: number
|
||||||
day: number
|
}> = []
|
||||||
}> = []
|
|
||||||
|
|
||||||
members.forEach(member => {
|
members.forEach(member => {
|
||||||
// 生日
|
// 生日
|
||||||
if (member.birthDate) {
|
if (member.birthDate) {
|
||||||
const birthDate = new Date(member.birthDate)
|
const birthDate = new Date(member.birthDate)
|
||||||
let thisYearBirth: Date
|
let thisYearBirth: Date
|
||||||
let lunarDisplay: string | undefined
|
let lunarDisplay: string | undefined
|
||||||
|
|
||||||
// 检查是否按农历计算
|
// 检查是否按农历计算
|
||||||
if (member.isLunarDate) {
|
if (member.isLunarDate) {
|
||||||
// 农历生日:计算今年对应的公历日期
|
// 农历生日:计算今年对应的公历日期
|
||||||
const lunarInfo = solar2lunar(birthDate)
|
const lunarInfo = solar2lunar(birthDate)
|
||||||
if (lunarInfo) {
|
if (lunarInfo) {
|
||||||
const thisYearLunar = lunar2solar(
|
const thisYearLunar = lunar2solar(
|
||||||
now.getFullYear(),
|
now.getFullYear(),
|
||||||
lunarInfo.lunarMonth,
|
lunarInfo.lunarMonth,
|
||||||
lunarInfo.lunarDay,
|
lunarInfo.lunarDay,
|
||||||
lunarInfo.isLeap
|
lunarInfo.isLeap
|
||||||
)
|
)
|
||||||
if (thisYearLunar) {
|
if (thisYearLunar) {
|
||||||
thisYearBirth = thisYearLunar
|
thisYearBirth = thisYearLunar
|
||||||
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
||||||
} else {
|
|
||||||
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 公历生日
|
|
||||||
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if (thisYearBirth >= now && thisYearBirth <= threeMonthsLater) {
|
// 公历生日
|
||||||
upcomingEvents.push({
|
thisYearBirth = new Date(now.getFullYear(), birthDate.getMonth(), birthDate.getDate())
|
||||||
member,
|
|
||||||
type: 'birth',
|
|
||||||
date: thisYearBirth,
|
|
||||||
originalDate: member.birthDate,
|
|
||||||
isLunar: member.isLunarDate || false,
|
|
||||||
lunarDisplay,
|
|
||||||
month: thisYearBirth.getMonth() + 1,
|
|
||||||
day: thisYearBirth.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) {
|
if (member.deathDate) {
|
||||||
const deathDate = new Date(member.deathDate)
|
const deathDate = new Date(member.deathDate)
|
||||||
let thisYearDeath: Date
|
let thisYearDeath: Date
|
||||||
let lunarDisplay: string | undefined
|
let lunarDisplay: string | undefined
|
||||||
|
|
||||||
// 检查是否按农历计算
|
// 检查是否按农历计算
|
||||||
if (member.isLunarDate) {
|
if (member.isLunarDate) {
|
||||||
// 农历忌日:计算今年对应的公历日期
|
// 农历忌日:计算今年对应的公历日期
|
||||||
const lunarInfo = solar2lunar(deathDate)
|
const lunarInfo = solar2lunar(deathDate)
|
||||||
if (lunarInfo) {
|
if (lunarInfo) {
|
||||||
const thisYearLunar = lunar2solar(
|
const thisYearLunar = lunar2solar(
|
||||||
now.getFullYear(),
|
now.getFullYear(),
|
||||||
lunarInfo.lunarMonth,
|
lunarInfo.lunarMonth,
|
||||||
lunarInfo.lunarDay,
|
lunarInfo.lunarDay,
|
||||||
lunarInfo.isLeap
|
lunarInfo.isLeap
|
||||||
)
|
)
|
||||||
if (thisYearLunar) {
|
if (thisYearLunar) {
|
||||||
thisYearDeath = thisYearLunar
|
thisYearDeath = thisYearLunar
|
||||||
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
||||||
} else {
|
|
||||||
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 公历忌日
|
|
||||||
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if (thisYearDeath >= now && thisYearDeath <= threeMonthsLater) {
|
// 公历忌日
|
||||||
upcomingEvents.push({
|
thisYearDeath = new Date(now.getFullYear(), deathDate.getMonth(), deathDate.getDate())
|
||||||
member,
|
|
||||||
type: 'death',
|
|
||||||
date: thisYearDeath,
|
|
||||||
originalDate: member.deathDate,
|
|
||||||
isLunar: member.isLunarDate || false,
|
|
||||||
lunarDisplay,
|
|
||||||
month: thisYearDeath.getMonth() + 1,
|
|
||||||
day: thisYearDeath.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())
|
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||||
|
|
||||||
return upcomingEvents.length > 0 ? (
|
const birthEvents = upcomingEvents.filter(e => e.type === 'birth')
|
||||||
upcomingEvents.slice(0, 5).map((event, index) => {
|
const deathEvents = upcomingEvents.filter(e => e.type === 'death')
|
||||||
const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear()
|
|
||||||
const daysUntil = Math.ceil((event.date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
return upcomingEvents.length > 0 ? (
|
||||||
return (
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<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">
|
<div className="space-y-3">
|
||||||
{event.isLunar && event.lunarDisplay ? (
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
// 农历日期框 - 红色主题
|
<span className="w-1 h-4 bg-green-500 rounded"></span>
|
||||||
<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">
|
<span className="ml-auto text-xs font-normal text-muted-foreground bg-green-50 px-2 py-1 rounded border border-green-200">
|
||||||
{event.lunarDisplay}
|
{birthEvents.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</h3>
|
||||||
) : (
|
<div className="space-y-2 max-h-[calc(5*70px)] overflow-y-auto pr-2">
|
||||||
// 公历日期框 - 蓝色主题
|
{birthEvents.length > 0 ? (
|
||||||
<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">
|
birthEvents.map((event, index) => {
|
||||||
<span className="text-xs font-medium">
|
const yearsAgo = now.getFullYear() - new Date(event.originalDate).getFullYear()
|
||||||
{event.month}月
|
const daysUntil = Math.ceil((event.date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||||
</span>
|
return (
|
||||||
<span className="font-bold text-sm leading-tight">{event.day}</span>
|
<Link href={`/members/${event.member.id}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`} key={`${event.member.id}-${event.type}-${index}`}>
|
||||||
</div>
|
<div className="flex items-center gap-3 p-2.5 rounded-lg bg-background/60 border border-border/50 hover:bg-background transition-colors cursor-pointer">
|
||||||
)}
|
{event.isLunar && event.lunarDisplay ? (
|
||||||
<div className="flex-1">
|
<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 flex-shrink-0">
|
||||||
<p className="font-medium text-sm flex items-center gap-2">
|
<span className="text-[9px] leading-tight text-center px-1">
|
||||||
<MemberNameWithStatus
|
{event.lunarDisplay}
|
||||||
name={event.member.fullName}
|
</span>
|
||||||
isDead={!!event.member.deathDate}
|
</div>
|
||||||
/> {event.type === 'birth' ? '诞辰' : '忌日'}
|
) : (
|
||||||
{event.isLunar && (
|
<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 flex-shrink-0">
|
||||||
<span className="text-[10px] text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded border border-amber-200">
|
<span className="text-[10px] font-medium">
|
||||||
农历
|
{event.month}月
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="font-bold text-sm leading-tight">{event.day}</span>
|
||||||
</p>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
)}
|
||||||
第 {event.member.generation} 世 · {yearsAgo} 年
|
<div className="flex-1 min-w-0">
|
||||||
{event.type === 'death' && ' · 已故'}
|
<p className="font-medium text-sm flex items-center gap-1 flex-wrap">
|
||||||
{event.isLunar && event.lunarDisplay && (
|
<MemberNameWithStatus
|
||||||
<span className="ml-1">· {event.lunarDisplay}</span>
|
name={event.member.fullName}
|
||||||
)}
|
isDead={!!event.member.deathDate}
|
||||||
<span className="ml-1 text-primary">· {daysUntil}天后</span>
|
/>
|
||||||
</p>
|
{event.isLunar && (
|
||||||
</div>
|
<span className="text-[9px] text-amber-600 bg-amber-50 px-1 py-0.5 rounded border border-amber-200 flex-shrink-0">
|
||||||
|
农历
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
第 {event.member.generation} 世 · {yearsAgo} 年 · <span className="text-primary font-medium">{daysUntil}天</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-xs text-muted-foreground">无诞辰</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
)}
|
||||||
)
|
</div>
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<p className="text-sm text-foreground font-light tracking-wide">来日无期</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
})()}
|
{/* 忌日列 */}
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<span className="w-1 h-4 bg-gray-500 rounded"></span>
|
||||||
|
忌日
|
||||||
|
<span className="ml-auto text-xs font-normal text-muted-foreground bg-gray-100 px-2 py-1 rounded border border-gray-200">
|
||||||
|
{deathEvents.length}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 max-h-[calc(5*70px)] overflow-y-auto pr-2">
|
||||||
|
{deathEvents.length > 0 ? (
|
||||||
|
deathEvents.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.5 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 flex-shrink-0">
|
||||||
|
<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 flex-shrink-0">
|
||||||
|
<span className="text-[10px] font-medium">
|
||||||
|
{event.month}月
|
||||||
|
</span>
|
||||||
|
<span className="font-bold text-sm leading-tight">{event.day}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-sm flex items-center gap-1 flex-wrap">
|
||||||
|
<MemberNameWithStatus
|
||||||
|
name={event.member.fullName}
|
||||||
|
isDead={!!event.member.deathDate}
|
||||||
|
/>
|
||||||
|
{event.isLunar && (
|
||||||
|
<span className="text-[9px] text-amber-600 bg-amber-50 px-1 py-0.5 rounded border border-amber-200 flex-shrink-0">
|
||||||
|
农历
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
第 {event.member.generation} 世 · {yearsAgo} 年 · <span className="text-primary font-medium">{daysUntil}天</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-xs text-muted-foreground">无忌日</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-sm text-foreground font-light tracking-wide">来日无期</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</ChineseCardContent>
|
</ChineseCardContent>
|
||||||
</ChineseCard>
|
</ChineseCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChineseCard variant="default" cornerOrnament className="shadow-sm">
|
{/* 第二行:近期修谱动态(折叠状态) */}
|
||||||
<ChineseCardHeader>
|
<Collapsible open={recentActivityExpanded} onOpenChange={setRecentActivityExpanded} className="w-full">
|
||||||
<ChineseCardTitle className="flex items-center gap-2">
|
<ChineseCard variant="default" cornerOrnament className="shadow-sm">
|
||||||
<BarChart3 className="h-5 w-5 text-primary" />
|
<ChineseCardHeader>
|
||||||
近期修谱动态
|
<div className="flex items-center justify-between w-full">
|
||||||
</ChineseCardTitle>
|
<div className="flex items-center gap-2">
|
||||||
<ChineseCardDescription>记录家族成员的最新变动与修订</ChineseCardDescription>
|
<BarChart3 className="h-5 w-5 text-primary" />
|
||||||
</ChineseCardHeader>
|
<ChineseCardTitle>近期修谱动态</ChineseCardTitle>
|
||||||
<ChineseCardContent>
|
</div>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||||
|
{recentActivityExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
<ChineseCardDescription>记录家族成员的最新变动与修订</ChineseCardDescription>
|
||||||
|
</ChineseCardHeader>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<ChineseCardContent>
|
||||||
<ScrollArea className="h-[600px] pr-4">
|
<ScrollArea className="h-[600px] pr-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{recentActivities.length > 0 ? (
|
{recentActivities.length > 0 ? (
|
||||||
@@ -1159,8 +1318,10 @@ export default function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</ChineseCardContent>
|
</ChineseCardContent>
|
||||||
</ChineseCard>
|
</CollapsibleContent>
|
||||||
|
</ChineseCard>
|
||||||
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
+55
-3
@@ -47,7 +47,7 @@ import type { D3OrgChartRef } from "@/components/tree/d3-org-chart-flow"
|
|||||||
type ViewMode = "traditional" | "d3-org-chart"
|
type ViewMode = "traditional" | "d3-org-chart"
|
||||||
|
|
||||||
export default function TreePage() {
|
export default function TreePage() {
|
||||||
const { treeData, currentTree, refreshData } = useFamily()
|
const { treeData, currentTree, refreshData, highlightedMemberId, setHighlightedMemberId } = useFamily()
|
||||||
const { calculateRelationship } = useRelationship()
|
const { calculateRelationship } = useRelationship()
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
const { showAlert } = useDialog()
|
const { showAlert } = useDialog()
|
||||||
@@ -119,9 +119,14 @@ export default function TreePage() {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const d3ChartRef = useRef<D3OrgChartRef>(null)
|
const d3ChartRef = useRef<D3OrgChartRef>(null)
|
||||||
|
|
||||||
// 传统族谱初始定位到始祖并自适应缩放
|
// 传统族谱初始定位到始祖并自适应缩放(如果有高亮成员则跳过)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewMode !== 'traditional' || hasInitialPositioned || !treeData.rootId) return
|
if (viewMode !== 'traditional' || hasInitialPositioned || !treeData.rootId) return
|
||||||
|
// 如果有高亮成员,跳过初始定位,让高亮定位逻辑处理
|
||||||
|
if (highlightedMemberId) {
|
||||||
|
setHasInitialPositioned(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 找到始祖成员:优先使用标记为 isFounder 的成员,否则使用 rootId 对应的成员
|
// 找到始祖成员:优先使用标记为 isFounder 的成员,否则使用 rootId 对应的成员
|
||||||
const founderMember = Object.values(treeData.members).find(m => m.isFounder === true)
|
const founderMember = Object.values(treeData.members).find(m => m.isFounder === true)
|
||||||
@@ -180,7 +185,7 @@ export default function TreePage() {
|
|||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [viewMode, treeData.rootId, treeData.members, hasInitialPositioned])
|
}, [viewMode, treeData.rootId, treeData.members, hasInitialPositioned, highlightedMemberId])
|
||||||
|
|
||||||
// 切换视图模式时重置初始定位状态和缩放位置
|
// 切换视图模式时重置初始定位状态和缩放位置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -189,6 +194,53 @@ export default function TreePage() {
|
|||||||
setPosition({ x: 0, y: 0 })
|
setPosition({ x: 0, y: 0 })
|
||||||
}, [viewMode])
|
}, [viewMode])
|
||||||
|
|
||||||
|
// 当有高亮成员时,定位到该成员并居中显示,设置 scale 为 1.0
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewMode !== 'traditional' || !highlightedMemberId) return
|
||||||
|
|
||||||
|
// 第一步:重置位置和缩放为初始状态
|
||||||
|
setScale(1)
|
||||||
|
setPosition({ x: 0, y: 0 })
|
||||||
|
|
||||||
|
// 延迟执行,等待 DOM 完全渲染(需要足够长的时间让树完全展开)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找高亮成员的 DOM 元素
|
||||||
|
const highlightedNode = containerRef.current.querySelector(`[data-member-id="${highlightedMemberId}"]`) as HTMLElement
|
||||||
|
if (!highlightedNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect()
|
||||||
|
const nodeRect = highlightedNode.getBoundingClientRect()
|
||||||
|
|
||||||
|
// 计算容器中心
|
||||||
|
const containerCenterX = containerRect.width / 2
|
||||||
|
const containerCenterY = containerRect.height / 2
|
||||||
|
|
||||||
|
// 此时 position 已经是 {x:0, y:0},所以节点位置就是相对于原点的位置
|
||||||
|
// 我们需要计算让节点居中的偏移量
|
||||||
|
const nodeCenterX = nodeRect.left - containerRect.left + nodeRect.width / 2
|
||||||
|
const nodeCenterY = nodeRect.top - containerRect.top + nodeRect.height / 2
|
||||||
|
|
||||||
|
// 计算偏移量
|
||||||
|
const offsetX = containerCenterX - nodeCenterX
|
||||||
|
const offsetY = containerCenterY - nodeCenterY
|
||||||
|
|
||||||
|
setPosition({ x: offsetX, y: offsetY })
|
||||||
|
|
||||||
|
// 30秒后清除高亮状态
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlightedMemberId(null)
|
||||||
|
}, 30000)
|
||||||
|
}, 1000) // 增加到1秒,确保DOM完全渲染
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [viewMode, highlightedMemberId, setHighlightedMemberId])
|
||||||
|
|
||||||
// 传统族谱自适应视图函数
|
// 传统族谱自适应视图函数
|
||||||
const handleTraditionalFitView = useCallback(() => {
|
const handleTraditionalFitView = useCallback(() => {
|
||||||
if (!containerRef.current || !treeData.rootId) return
|
if (!containerRef.current || !treeData.rootId) return
|
||||||
|
|||||||
@@ -695,7 +695,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
|||||||
value={formData.birthDate || ""}
|
value={formData.birthDate || ""}
|
||||||
isLunar={formData.isLunarDate || false}
|
isLunar={formData.isLunarDate || false}
|
||||||
onChange={(value) => handleChange("birthDate", value)}
|
onChange={(value) => handleChange("birthDate", value)}
|
||||||
showLunarToggle={false}
|
onLunarChange={(isLunar) => handleChange("isLunarDate", isLunar)}
|
||||||
|
showLunarToggle={true}
|
||||||
/>
|
/>
|
||||||
<DateInputWithLunar
|
<DateInputWithLunar
|
||||||
id="deathDate"
|
id="deathDate"
|
||||||
@@ -703,7 +704,8 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
|||||||
value={formData.deathDate || ""}
|
value={formData.deathDate || ""}
|
||||||
isLunar={formData.isLunarDate || false}
|
isLunar={formData.isLunarDate || false}
|
||||||
onChange={(value) => handleChange("deathDate", value)}
|
onChange={(value) => handleChange("deathDate", value)}
|
||||||
showLunarToggle={false}
|
onLunarChange={(isLunar) => handleChange("isLunarDate", isLunar)}
|
||||||
|
showLunarToggle={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ChineseCardContent>
|
</ChineseCardContent>
|
||||||
|
|||||||
+51
-23
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { BookOpen, Search, Settings, LogOut, User, ChevronDown, Plus, Trash2, Network, LayoutGrid, Calculator, HelpCircle, Calendar, Menu, X, Home, Users as UsersIcon, Clock, TreePine } from "lucide-react"
|
import { BookOpen, Search, Settings, LogOut, User, ChevronDown, Plus, Trash2, Network, LayoutGrid, Calculator, HelpCircle, Calendar, Menu, X, Home, Users as UsersIcon, Clock, TreePine, MapPin } from "lucide-react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
@@ -34,7 +34,7 @@ interface FamilyTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const { searchMembers, searchResults, currentTree: familyCurrentTree } = useFamily()
|
const { searchMembers, searchResults, currentTree: familyCurrentTree, setHighlightedMemberId } = useFamily()
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
const { showAlert, showConfirm, showPrompt } = useDialog()
|
const { showAlert, showConfirm, showPrompt } = useDialog()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
@@ -241,6 +241,21 @@ export function SiteHeader() {
|
|||||||
setShowResults(false)
|
setShowResults(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在族谱图中定位成员
|
||||||
|
const handleLocateMember = (memberId: string) => {
|
||||||
|
setHighlightedMemberId(memberId)
|
||||||
|
// 如果不在族谱页面,跳转到族谱页面
|
||||||
|
if (pathname !== '/tree') {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (currentTree?.id) {
|
||||||
|
params.set('treeId', currentTree.id)
|
||||||
|
}
|
||||||
|
router.push(`/tree?${params.toString()}`)
|
||||||
|
}
|
||||||
|
setSearchQuery("")
|
||||||
|
setShowResults(false)
|
||||||
|
}
|
||||||
|
|
||||||
// 导航菜单项配置
|
// 导航菜单项配置
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: "/", label: "总览", icon: Home },
|
{ href: "/", label: "总览", icon: Home },
|
||||||
@@ -474,31 +489,44 @@ export function SiteHeader() {
|
|||||||
{showResults && searchResults.length > 0 && (
|
{showResults && searchResults.length > 0 && (
|
||||||
<div className="absolute top-full mt-2 w-full bg-background border border-border rounded-lg shadow-lg max-h-96 overflow-y-auto z-50">
|
<div className="absolute top-full mt-2 w-full bg-background border border-border rounded-lg shadow-lg max-h-96 overflow-y-auto z-50">
|
||||||
{searchResults.map((member) => (
|
{searchResults.map((member) => (
|
||||||
<button
|
<div
|
||||||
key={member.id}
|
key={member.id}
|
||||||
onClick={() => handleSelectMember(member.id)}
|
className="w-full px-4 py-3 hover:bg-muted transition-colors border-b border-border last:border-0 flex items-center gap-2"
|
||||||
className="w-full px-4 py-3 text-left hover:bg-muted transition-colors border-b border-border last:border-0"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<button
|
||||||
<div className="flex-1">
|
onClick={() => handleSelectMember(member.id)}
|
||||||
<div className="flex items-center gap-2">
|
className="flex-1 text-left"
|
||||||
<MemberNameWithStatus
|
>
|
||||||
name={member.fullName}
|
<div className="flex items-center gap-3">
|
||||||
isDead={!!member.deathDate}
|
<div className="flex-1">
|
||||||
className="font-medium"
|
<div className="flex items-center gap-2">
|
||||||
/>
|
<MemberNameWithStatus
|
||||||
<span className={`text-xs px-1.5 py-0.5 rounded ${member.gender === "MALE" ? "bg-blue-100 text-blue-700" : "bg-pink-100 text-pink-700"}`}>
|
name={member.fullName}
|
||||||
{member.gender === "MALE" ? "男" : "女"}
|
isDead={!!member.deathDate}
|
||||||
</span>
|
className="font-medium"
|
||||||
<span className="text-xs text-muted-foreground">第{member.generation}世</span>
|
/>
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded ${member.gender === "MALE" ? "bg-blue-100 text-blue-700" : "bg-pink-100 text-pink-700"}`}>
|
||||||
|
{member.gender === "MALE" ? "男" : "女"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">第{member.generation}世</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{member.birthDate && `生于 ${member.birthDate}`}
|
||||||
|
{member.birthPlace && ` · ${member.birthPlace}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
{member.birthDate && `生于 ${member.birthDate}`}
|
|
||||||
{member.birthPlace && ` · ${member.birthPlace}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</button>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 flex-shrink-0 text-muted-foreground hover:text-primary"
|
||||||
|
onClick={() => handleLocateMember(member.id)}
|
||||||
|
title="在族谱图中定位"
|
||||||
|
>
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface FamilyNodeProps {
|
|||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
onSelect?: (member: FamilyMember) => void
|
onSelect?: (member: FamilyMember) => void
|
||||||
isHighlighted?: boolean
|
isHighlighted?: boolean
|
||||||
|
highlightedMemberId?: string | null
|
||||||
relationMode?: boolean
|
relationMode?: boolean
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
selectedMembers?: string[]
|
selectedMembers?: string[]
|
||||||
@@ -502,6 +503,7 @@ export function FamilyNode({
|
|||||||
isRoot,
|
isRoot,
|
||||||
onSelect,
|
onSelect,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
|
highlightedMemberId,
|
||||||
relationMode,
|
relationMode,
|
||||||
isSelected,
|
isSelected,
|
||||||
selectedMembers = [],
|
selectedMembers = [],
|
||||||
@@ -535,6 +537,11 @@ export function FamilyNode({
|
|||||||
// 有配偶时,合并到一个卡片中显示(类似D3布局)
|
// 有配偶时,合并到一个卡片中显示(类似D3布局)
|
||||||
const isMale = member.gender === 'MALE'
|
const isMale = member.gender === 'MALE'
|
||||||
const isDead = !!member.deathDate
|
const isDead = !!member.deathDate
|
||||||
|
|
||||||
|
// 检查是否有任何配偶被高亮,如果是则整个卡片高亮
|
||||||
|
const isAnySpouseHighlighted = spouseList.some(sp => sp.id === highlightedMemberId)
|
||||||
|
const isCardHighlighted = isHighlighted || isAnySpouseHighlighted
|
||||||
|
|
||||||
// 使用 isRoot prop 而不是 isFounder 字段
|
// 使用 isRoot prop 而不是 isFounder 字段
|
||||||
const isFounder = isRoot === true
|
const isFounder = isRoot === true
|
||||||
const avatarUrl = member.avatarUrl
|
const avatarUrl = member.avatarUrl
|
||||||
@@ -552,8 +559,8 @@ export function FamilyNode({
|
|||||||
"relative rounded-xl overflow-hidden transition-all duration-300",
|
"relative rounded-xl overflow-hidden transition-all duration-300",
|
||||||
"shadow-[0_4px_20px_rgba(0,0,0,0.08)] hover:shadow-[0_12px_40px_rgba(0,0,0,0.15)]",
|
"shadow-[0_4px_20px_rgba(0,0,0,0.08)] hover:shadow-[0_12px_40px_rgba(0,0,0,0.15)]",
|
||||||
"hover:scale-[1.02] hover:-translate-y-1",
|
"hover:scale-[1.02] hover:-translate-y-1",
|
||||||
isHighlighted
|
isCardHighlighted
|
||||||
? "bg-gradient-to-br from-yellow-50 via-amber-50 to-yellow-100 ring-2 ring-yellow-400"
|
? "bg-gradient-to-br from-yellow-50 via-amber-50 to-yellow-100 ring-2 ring-yellow-400 shadow-[0_8px_30px_rgba(234,179,8,0.4)]"
|
||||||
: isFounder || isRoot
|
: isFounder || isRoot
|
||||||
? "bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50"
|
? "bg-gradient-to-br from-amber-50 via-orange-50 to-yellow-50"
|
||||||
: "bg-gradient-to-br from-slate-50 via-gray-50 to-slate-100"
|
: "bg-gradient-to-br from-slate-50 via-gray-50 to-slate-100"
|
||||||
@@ -562,7 +569,7 @@ export function FamilyNode({
|
|||||||
{/* 顶部装饰条 */}
|
{/* 顶部装饰条 */}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"h-1.5",
|
"h-1.5",
|
||||||
isHighlighted ? "bg-gradient-to-r from-yellow-400 via-amber-500 to-yellow-400"
|
isCardHighlighted ? "bg-gradient-to-r from-yellow-400 via-amber-500 to-yellow-400"
|
||||||
: isFounder || isRoot ? "bg-gradient-to-r from-amber-400 via-orange-500 to-amber-400"
|
: isFounder || isRoot ? "bg-gradient-to-r from-amber-400 via-orange-500 to-amber-400"
|
||||||
: "bg-gradient-to-r from-slate-400 via-gray-500 to-slate-400"
|
: "bg-gradient-to-r from-slate-400 via-gray-500 to-slate-400"
|
||||||
)} />
|
)} />
|
||||||
@@ -669,8 +676,8 @@ export function FamilyNode({
|
|||||||
{/* 配偶卡片 */}
|
{/* 配偶卡片 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center cursor-pointer min-w-[80px] relative",
|
"flex flex-col items-center cursor-pointer min-w-[80px] relative p-2 rounded-lg transition-all",
|
||||||
relationMode && "hover:ring-2 hover:ring-primary/50 rounded-lg p-1"
|
relationMode && "hover:ring-2 hover:ring-primary/50"
|
||||||
)}
|
)}
|
||||||
data-member-id={sp.id}
|
data-member-id={sp.id}
|
||||||
onClick={() => onSelect?.(sp)}
|
onClick={() => onSelect?.(sp)}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const TreeNode = memo(function TreeNode({
|
|||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
isRoot={isRoot}
|
isRoot={isRoot}
|
||||||
isHighlighted={isHighlighted}
|
isHighlighted={isHighlighted}
|
||||||
|
highlightedMemberId={highlightedMemberId}
|
||||||
relationMode={relationMode}
|
relationMode={relationMode}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
selectedMembers={selectedMembers}
|
selectedMembers={selectedMembers}
|
||||||
|
|||||||
+17
-1
@@ -290,7 +290,23 @@ export function isInCurrentMonth(dateStr: string, isLunar: boolean = false): { d
|
|||||||
originalLunar.isLeap
|
originalLunar.isLeap
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!thisYearSolar) return null
|
if (!thisYearSolar) {
|
||||||
|
// 如果转换失败,尝试用明年的日期
|
||||||
|
const nextYearSolar = lunar2solar(
|
||||||
|
currentYear + 1,
|
||||||
|
originalLunar.lunarMonth,
|
||||||
|
originalLunar.lunarDay,
|
||||||
|
originalLunar.isLeap
|
||||||
|
)
|
||||||
|
if (nextYearSolar) {
|
||||||
|
const nextYearMonth = nextYearSolar.getMonth() + 1
|
||||||
|
const nextYearDay = nextYearSolar.getDate()
|
||||||
|
if (nextYearMonth === currentMonth) {
|
||||||
|
return { day: nextYearDay, month: nextYearMonth }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const thisYearMonth = thisYearSolar.getMonth() + 1
|
const thisYearMonth = thisYearSolar.getMonth() + 1
|
||||||
const thisYearDay = thisYearSolar.getDate()
|
const thisYearDay = thisYearSolar.getDate()
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lastValidatedTimestamp": 1766327831114,
|
"lastValidatedTimestamp": 1766335044864,
|
||||||
"projects": {},
|
"projects": {},
|
||||||
"pnpmfiles": [],
|
"pnpmfiles": [],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
Reference in New Issue
Block a user