0.3.0.0
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import * as echarts from "echarts"
|
||||
// 显式导入 ECharts 核心和渲染器
|
||||
import * as echarts from "echarts/core"
|
||||
import { PieChart, BarChart, LineChart } from "echarts/charts"
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
} from "echarts/components"
|
||||
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"
|
||||
|
||||
// 注册必要的组件
|
||||
echarts.use([
|
||||
PieChart,
|
||||
BarChart,
|
||||
LineChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
CanvasRenderer,
|
||||
])
|
||||
|
||||
interface StatisticsChartsProps {
|
||||
members: FamilyMember[]
|
||||
}
|
||||
@@ -133,7 +155,7 @@ export function StatisticsCharts({ members }: StatisticsChartsProps) {
|
||||
type: 'bar',
|
||||
data: sortedGenerations.map(([, count]) => count),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 0.5, color: '#188df0' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
@@ -141,7 +163,7 @@ export function StatisticsCharts({ members }: StatisticsChartsProps) {
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#2378f7' },
|
||||
{ offset: 0.7, color: '#2378f7' },
|
||||
{ offset: 1, color: '#83bff6' }
|
||||
@@ -285,7 +307,7 @@ export function StatisticsCharts({ members }: StatisticsChartsProps) {
|
||||
data: sortedYears.map(([, count]) => count),
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(99, 102, 241, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(99, 102, 241, 0.1)' }
|
||||
])
|
||||
|
||||
@@ -287,7 +287,7 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
<div className="flex justify-center mb-4">
|
||||
<ImageUpload
|
||||
value={formData.avatarUrl}
|
||||
onChange={(imageUrl) => handleChange("avatarUrl", imageUrl)}
|
||||
onChange={(imageUrl) => handleChange("avatarUrl", imageUrl || null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,15 +26,16 @@ interface MemberVersionHistoryProps {
|
||||
memberId: string
|
||||
treeId: string
|
||||
onVersionSelect?: (version: MemberHistory) => void
|
||||
lastUpdated?: number // 用于触发刷新的时间戳
|
||||
}
|
||||
|
||||
export function MemberVersionHistory({ memberId, treeId, onVersionSelect }: MemberVersionHistoryProps) {
|
||||
export function MemberVersionHistory({ memberId, treeId, onVersionSelect, lastUpdated }: MemberVersionHistoryProps) {
|
||||
const [history, setHistory] = useState<MemberHistory[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
loadHistory()
|
||||
}, [memberId, treeId])
|
||||
}, [memberId, treeId, lastUpdated])
|
||||
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
@@ -80,12 +81,18 @@ export function MemberVersionHistory({ memberId, treeId, onVersionSelect }: Memb
|
||||
motherId: '母亲', fatherId: '父亲', isFounder: '始祖',
|
||||
isLunarDate: '农历日期', burialPlace: '安葬地', tags: '标签',
|
||||
spouseFatherName: '岳父/公公', spouseMotherName: '岳母/婆婆',
|
||||
avatarUrl: '头像', stories: '家族故事',
|
||||
}
|
||||
|
||||
const formatValue = (value: any) => {
|
||||
const formatValue = (value: any, field?: string) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
if (Array.isArray(value)) return value.length > 0 ? value.join(', ') : '-'
|
||||
if (Array.isArray(value)) {
|
||||
if (field === 'photoIds') return `${value.length} 张照片`
|
||||
if (field === 'stories') return `${value.length} 个故事`
|
||||
return value.length > 0 ? value.join(', ') : '-'
|
||||
}
|
||||
if (field === 'avatarUrl') return value ? '已上传' : '无'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
@@ -125,9 +132,9 @@ export function MemberVersionHistory({ memberId, treeId, onVersionSelect }: Memb
|
||||
<span className="text-muted-foreground min-w-[60px]">{fieldLabels[field]}:</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-600 line-through">{formatValue(change.old)}</span>
|
||||
<span className="text-red-600 line-through">{formatValue(change.old, field)}</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="text-green-600 font-medium">{formatValue(change.new)}</span>
|
||||
<span className="text-green-600 font-medium">{formatValue(change.new, field)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,9 +153,9 @@ export function MemberVersionHistory({ memberId, treeId, onVersionSelect }: Memb
|
||||
<span className="text-muted-foreground min-w-[60px]">{fieldLabels[field]}:</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-red-600 line-through">{formatValue(change.old)}</span>
|
||||
<span className="text-red-600 line-through">{formatValue(change.old, field)}</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="text-green-600 font-medium">{formatValue(change.new)}</span>
|
||||
<span className="text-green-600 font-medium">{formatValue(change.new, field)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useState, useRef, useEffect, useCallback } from "react"
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from "react"
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounce"
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation"
|
||||
import { useSession, signOut } from "next-auth/react"
|
||||
import { useDialog } from "@/components/ui/alert-dialog-custom"
|
||||
@@ -101,7 +102,10 @@ export function SiteHeader() {
|
||||
}
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut({ callbackUrl: "/auth/signin" })
|
||||
const confirmed = await showConfirm("确定要退出登录吗?", "退出登录", "destructive")
|
||||
if (confirmed) {
|
||||
await signOut({ callbackUrl: "/auth/signin" })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户名首字母作为头像
|
||||
@@ -219,10 +223,15 @@ export function SiteHeader() {
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query)
|
||||
// 使用防抖优化搜索性能
|
||||
const debouncedSearch = useDebouncedCallback((query: string) => {
|
||||
searchMembers(query)
|
||||
setShowResults(query.length > 0)
|
||||
}, 300)
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query)
|
||||
debouncedSearch(query)
|
||||
}
|
||||
|
||||
const handleSelectMember = (memberId: string) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { FamilyMember } from "@/types/family"
|
||||
import { FamilyNode } from "./family-node"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useEffect, useRef, useState, useCallback } from "react"
|
||||
import { useEffect, useRef, useState, useCallback, useMemo, memo } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface TreeLayoutProps {
|
||||
@@ -131,10 +131,33 @@ export function TreeLayout({
|
||||
}
|
||||
}, [calculateConnections, treeData])
|
||||
|
||||
// 预计算排序后的子节点映射,避免每次渲染时重复计算
|
||||
const sortedChildrenMap = useMemo(() => {
|
||||
const map = new Map<string, string[]>()
|
||||
Object.values(treeData.members).forEach(member => {
|
||||
if (member.childrenIds && member.childrenIds.length > 0) {
|
||||
const sorted = [...member.childrenIds].sort((aId, bId) => {
|
||||
const a = treeData.members[aId]
|
||||
const b = treeData.members[bId]
|
||||
if (!a || !b) return 0
|
||||
|
||||
const aIsFounder = a.tags?.includes('始祖') || a.generation === 1
|
||||
const bIsFounder = b.tags?.includes('始祖') || b.generation === 1
|
||||
if (aIsFounder && !bIsFounder) return -1
|
||||
if (!aIsFounder && bIsFounder) return 1
|
||||
|
||||
return (a.birthDate || "").localeCompare(b.birthDate || "")
|
||||
})
|
||||
map.set(member.id, sorted)
|
||||
}
|
||||
})
|
||||
return map
|
||||
}, [treeData.members])
|
||||
|
||||
if (!root) return null
|
||||
|
||||
// 递归渲染树节点
|
||||
const TreeNode = ({ memberId, isRoot = false }: { memberId: string; isRoot?: boolean }) => {
|
||||
// 递归渲染树节点 - 使用 memo 优化
|
||||
const TreeNode = memo(function TreeNode({ memberId, isRoot = false }: { memberId: string; isRoot?: boolean }) {
|
||||
const member = getMember(memberId)
|
||||
if (!member) return null
|
||||
|
||||
@@ -145,6 +168,9 @@ export function TreeLayout({
|
||||
? member.spouseIds.map(id => getMember(id)).filter(Boolean) as FamilyMember[]
|
||||
: []
|
||||
|
||||
// 使用预计算的排序结果
|
||||
const sortedChildren = sortedChildrenMap.get(memberId) || []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
{/* 节点本身 */}
|
||||
@@ -178,32 +204,14 @@ export function TreeLayout({
|
||||
gap: `${HORIZONTAL_GAP}px`
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// 对子节点进行排序:始祖排最左边,其他按出生日期排序
|
||||
const sortedChildren = [...member.childrenIds].sort((aId, bId) => {
|
||||
const a = getMember(aId)
|
||||
const b = getMember(bId)
|
||||
if (!a || !b) return 0
|
||||
|
||||
// 始祖永远排最左边
|
||||
const aIsFounder = a.tags?.includes('始祖') || a.generation === 1
|
||||
const bIsFounder = b.tags?.includes('始祖') || b.generation === 1
|
||||
if (aIsFounder && !bIsFounder) return -1
|
||||
if (!aIsFounder && bIsFounder) return 1
|
||||
|
||||
// 其他按出生日期排序
|
||||
return (a.birthDate || "").localeCompare(b.birthDate || "")
|
||||
})
|
||||
|
||||
return sortedChildren.map((childId) => (
|
||||
<TreeNode key={childId} memberId={childId} />
|
||||
))
|
||||
})()}
|
||||
{sortedChildren.map((childId) => (
|
||||
<TreeNode key={childId} memberId={childId} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
|
||||
Reference in New Issue
Block a user