0.9.0.0
This commit is contained in:
@@ -0,0 +1,701 @@
|
||||
# app/page.tsx 优化代码示例
|
||||
|
||||
## 优化 1: 照片分组逻辑
|
||||
|
||||
### ❌ 原始代码(问题)
|
||||
```typescript
|
||||
{(() => {
|
||||
// 按月份分组
|
||||
const sortedPhotos = [...allPhotos].sort((a, b) =>
|
||||
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
|
||||
)
|
||||
const groupedByMonth: Record<string, typeof allPhotos> = {}
|
||||
sortedPhotos.forEach(photo => {
|
||||
const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月')
|
||||
if (!groupedByMonth[monthKey]) {
|
||||
groupedByMonth[monthKey] = []
|
||||
}
|
||||
groupedByMonth[monthKey].push(photo)
|
||||
})
|
||||
|
||||
return Object.entries(groupedByMonth).map(([month, monthPhotos]) => (
|
||||
// ... JSX
|
||||
))
|
||||
})()}
|
||||
```
|
||||
|
||||
### ✅ 优化后代码
|
||||
```typescript
|
||||
// 在组件顶部添加
|
||||
const groupedPhotosByMonth = useMemo(() => {
|
||||
const sortedPhotos = [...allPhotos].sort((a, b) =>
|
||||
new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime()
|
||||
)
|
||||
const groupedByMonth: Record<string, typeof allPhotos> = {}
|
||||
|
||||
sortedPhotos.forEach(photo => {
|
||||
const monthKey = format(new Date(photo.uploadedAt), 'yyyy年MM月')
|
||||
if (!groupedByMonth[monthKey]) {
|
||||
groupedByMonth[monthKey] = []
|
||||
}
|
||||
groupedByMonth[monthKey].push(photo)
|
||||
})
|
||||
|
||||
return Object.entries(groupedByMonth)
|
||||
}, [allPhotos])
|
||||
|
||||
// 在 JSX 中使用
|
||||
{allPhotos.length > 0 ? (
|
||||
<div className="space-y-8">
|
||||
{groupedPhotosByMonth.map(([month, monthPhotos]) => (
|
||||
// ... JSX
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// ... 空状态
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化 2: 未来三月纪念日计算
|
||||
|
||||
### ❌ 原始代码(问题)
|
||||
```typescript
|
||||
{(() => {
|
||||
const now = new Date()
|
||||
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
|
||||
const members = Object.values(treeData.members)
|
||||
const upcomingEvents: Array<{...}> = []
|
||||
|
||||
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(...)
|
||||
// ... 复杂逻辑
|
||||
}
|
||||
}
|
||||
// ... 更多逻辑
|
||||
}
|
||||
|
||||
// 忌日(重复逻辑)
|
||||
if (member.deathDate) {
|
||||
// ... 重复的逻辑
|
||||
}
|
||||
})
|
||||
|
||||
upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
return (
|
||||
// ... JSX
|
||||
)
|
||||
})()}
|
||||
```
|
||||
|
||||
### ✅ 优化后代码
|
||||
|
||||
首先,提取辅助函数:
|
||||
```typescript
|
||||
// 在组件外部定义
|
||||
const calculateEventDate = (
|
||||
dateStr: string,
|
||||
isLunar: boolean,
|
||||
now: Date
|
||||
): { date: Date; lunarDisplay?: string } | null => {
|
||||
const date = new Date(dateStr)
|
||||
let eventDate: Date
|
||||
let lunarDisplay: string | undefined
|
||||
|
||||
if (isLunar) {
|
||||
const lunarInfo = solar2lunar(date)
|
||||
if (lunarInfo) {
|
||||
const thisYearLunar = lunar2solar(
|
||||
now.getFullYear(),
|
||||
lunarInfo.lunarMonth,
|
||||
lunarInfo.lunarDay,
|
||||
lunarInfo.isLeap
|
||||
)
|
||||
if (thisYearLunar) {
|
||||
eventDate = thisYearLunar
|
||||
lunarDisplay = `${lunarInfo.monthName}${lunarInfo.dayName}`
|
||||
} else {
|
||||
eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
} else {
|
||||
eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
} else {
|
||||
eventDate = new Date(now.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
|
||||
return { date: eventDate, lunarDisplay }
|
||||
}
|
||||
|
||||
const createUpcomingEvent = (
|
||||
member: any,
|
||||
type: 'birth' | 'death',
|
||||
dateStr: string,
|
||||
isLunar: boolean,
|
||||
now: Date,
|
||||
threeMonthsLater: Date
|
||||
) => {
|
||||
const result = calculateEventDate(dateStr, isLunar, now)
|
||||
if (!result) return null
|
||||
|
||||
const { date, lunarDisplay } = result
|
||||
|
||||
if (date >= now && date <= threeMonthsLater) {
|
||||
return {
|
||||
member,
|
||||
type,
|
||||
date,
|
||||
originalDate: dateStr,
|
||||
isLunar,
|
||||
lunarDisplay,
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate()
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
然后在组件中使用 useMemo:
|
||||
```typescript
|
||||
const upcomingEvents = useMemo(() => {
|
||||
const now = new Date()
|
||||
const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 3, now.getDate())
|
||||
const members = Object.values(treeData.members)
|
||||
const events: Array<{...}> = []
|
||||
|
||||
members.forEach(member => {
|
||||
// 生日
|
||||
if (member.birthDate) {
|
||||
const birthEvent = createUpcomingEvent(
|
||||
member,
|
||||
'birth',
|
||||
member.birthDate,
|
||||
member.isLunarDate || false,
|
||||
now,
|
||||
threeMonthsLater
|
||||
)
|
||||
if (birthEvent) events.push(birthEvent)
|
||||
}
|
||||
|
||||
// 忌日
|
||||
if (member.deathDate) {
|
||||
const deathEvent = createUpcomingEvent(
|
||||
member,
|
||||
'death',
|
||||
member.deathDate,
|
||||
member.isLunarDate || false,
|
||||
now,
|
||||
threeMonthsLater
|
||||
)
|
||||
if (deathEvent) events.push(deathEvent)
|
||||
}
|
||||
})
|
||||
|
||||
return events.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
}, [treeData.members])
|
||||
|
||||
// 在 JSX 中使用
|
||||
const birthEvents = upcomingEvents.filter(e => e.type === 'birth')
|
||||
const deathEvents = upcomingEvents.filter(e => e.type === 'death')
|
||||
|
||||
return upcomingEvents.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 诞辰列 */}
|
||||
<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-green-500 rounded"></span>
|
||||
诞辰
|
||||
<span className="ml-auto text-xs font-normal text-muted-foreground bg-green-50 px-2 py-1 rounded border border-green-200">
|
||||
{birthEvents.length}
|
||||
</span>
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-[calc(5*70px)] overflow-y-auto pr-2">
|
||||
{birthEvents.length > 0 ? (
|
||||
birthEvents.map((event, index) => (
|
||||
<EventCard key={`${event.member.id}-birth-${index}`} event={event} currentTree={currentTree} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-xs text-muted-foreground">无诞辰</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) => (
|
||||
<EventCard key={`${event.member.id}-death-${index}`} event={event} currentTree={currentTree} />
|
||||
))
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化 3: 修复 handleAdminPhotoToggle useCallback
|
||||
|
||||
### ❌ 原始代码(问题)
|
||||
```typescript
|
||||
const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => {
|
||||
if (!isOwner) return
|
||||
const member = treeData.members[memberId]
|
||||
if (!member || !member.photos || member.photos.length === 0) return
|
||||
const key = `${memberId}|${photoUrl}`
|
||||
setAdminToggleLoading(key)
|
||||
try {
|
||||
const updatedPhotos = (member.photos as FamilyPhoto[]).map(photo =>
|
||||
photo.url === photoUrl ? { ...photo, adminVisibleOverride: value } : photo
|
||||
)
|
||||
await updateMember(memberId, { photos: updatedPhotos })
|
||||
} catch (error) {
|
||||
console.error('更新管理员展示权限失败:', error)
|
||||
} finally {
|
||||
setAdminToggleLoading(null)
|
||||
}
|
||||
}, [isOwner, treeData.members, updateMember]) // ❌ treeData.members 导致每次都创建新函数
|
||||
```
|
||||
|
||||
### ✅ 优化后代码
|
||||
```typescript
|
||||
const handleAdminPhotoToggle = useCallback(async (memberId: string, photoUrl: string, value: boolean) => {
|
||||
if (!isOwner) return
|
||||
|
||||
const key = `${memberId}|${photoUrl}`
|
||||
setAdminToggleLoading(key)
|
||||
|
||||
try {
|
||||
const member = treeData.members[memberId]
|
||||
if (!member?.photos?.length) return
|
||||
|
||||
const updatedPhotos = member.photos.map(photo =>
|
||||
photo.url === photoUrl ? { ...photo, adminVisibleOverride: value } : photo
|
||||
)
|
||||
|
||||
await updateMember(memberId, { photos: updatedPhotos })
|
||||
} catch (error) {
|
||||
console.error('更新管理员展示权限失败:', error)
|
||||
} finally {
|
||||
setAdminToggleLoading(null)
|
||||
}
|
||||
}, [isOwner, updateMember]) // ✅ 移除 treeData.members 依赖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化 4: 优化 stats 计算为单次遍历
|
||||
|
||||
### ❌ 原始代码(问题)
|
||||
```typescript
|
||||
const stats = useMemo(() => {
|
||||
const members = Object.values(treeData.members)
|
||||
const totalMembers = members.length
|
||||
|
||||
// 多次遍历数组
|
||||
const livingMembers = members.filter(m => !m.deathDate).length
|
||||
const deceasedMembers = members.filter(m => m.deathDate).length
|
||||
|
||||
const maleCount = members.filter(m => m.gender === 'MALE').length
|
||||
const femaleCount = members.filter(m => m.gender === 'FEMALE').length
|
||||
|
||||
const generations = members.map(m => m.generation || 0)
|
||||
const maxGeneration = generations.length > 0 ? Math.max(...generations) : 0
|
||||
|
||||
const birthYears = members
|
||||
.map(m => m.birthDate ? new Date(m.birthDate).getFullYear() : null)
|
||||
.filter(y => y !== null) as number[]
|
||||
const earliestYear = birthYears.length > 0 ? Math.min(...birthYears) : new Date().getFullYear()
|
||||
const yearsSpan = birthYears.length > 0 ? new Date().getFullYear() - earliestYear : 0
|
||||
|
||||
const deceasedWithAge = members.filter(m => m.birthDate && m.deathDate)
|
||||
const totalAge = deceasedWithAge.reduce((sum, m) => {
|
||||
const birthYear = new Date(m.birthDate!).getFullYear()
|
||||
const deathYear = new Date(m.deathDate!).getFullYear()
|
||||
return sum + (deathYear - birthYear)
|
||||
}, 0)
|
||||
const averageLifespan = deceasedWithAge.length > 0
|
||||
? Math.round(totalAge / deceasedWithAge.length)
|
||||
: 0
|
||||
|
||||
return {
|
||||
totalMembers,
|
||||
livingMembers,
|
||||
deceasedMembers,
|
||||
maleCount,
|
||||
femaleCount,
|
||||
maxGeneration,
|
||||
yearsSpan,
|
||||
earliestYear,
|
||||
averageLifespan
|
||||
}
|
||||
}, [treeData])
|
||||
```
|
||||
|
||||
### ✅ 优化后代码
|
||||
```typescript
|
||||
const stats = useMemo(() => {
|
||||
const members = Object.values(treeData.members)
|
||||
const totalMembers = members.length
|
||||
|
||||
// 单次遍历计算所有统计数据
|
||||
let livingMembers = 0
|
||||
let deceasedMembers = 0
|
||||
let maleCount = 0
|
||||
let femaleCount = 0
|
||||
let maxGeneration = 0
|
||||
const birthYears: number[] = []
|
||||
let totalAge = 0
|
||||
let deceasedWithAgeCount = 0
|
||||
|
||||
members.forEach(m => {
|
||||
// 生死统计
|
||||
if (m.deathDate) {
|
||||
deceasedMembers++
|
||||
} else {
|
||||
livingMembers++
|
||||
}
|
||||
|
||||
// 性别统计
|
||||
if (m.gender === 'MALE') maleCount++
|
||||
else if (m.gender === 'FEMALE') femaleCount++
|
||||
|
||||
// 代数统计
|
||||
if (m.generation && m.generation > maxGeneration) {
|
||||
maxGeneration = m.generation
|
||||
}
|
||||
|
||||
// 出生年份和寿命统计
|
||||
if (m.birthDate) {
|
||||
const birthYear = new Date(m.birthDate).getFullYear()
|
||||
birthYears.push(birthYear)
|
||||
|
||||
if (m.deathDate) {
|
||||
const deathYear = new Date(m.deathDate).getFullYear()
|
||||
totalAge += deathYear - birthYear
|
||||
deceasedWithAgeCount++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const earliestYear = birthYears.length > 0
|
||||
? Math.min(...birthYears)
|
||||
: new Date().getFullYear()
|
||||
const yearsSpan = birthYears.length > 0
|
||||
? new Date().getFullYear() - earliestYear
|
||||
: 0
|
||||
const averageLifespan = deceasedWithAgeCount > 0
|
||||
? Math.round(totalAge / deceasedWithAgeCount)
|
||||
: 0
|
||||
|
||||
return {
|
||||
totalMembers,
|
||||
livingMembers,
|
||||
deceasedMembers,
|
||||
maleCount,
|
||||
femaleCount,
|
||||
maxGeneration,
|
||||
yearsSpan,
|
||||
earliestYear,
|
||||
averageLifespan
|
||||
}
|
||||
}, [treeData.members]) // ✅ 更精细的依赖项
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化 5: 提取照片卡片为单独组件
|
||||
|
||||
### 新建文件:`components/dashboard/photo-card.tsx`
|
||||
```typescript
|
||||
import React, { useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { format } from 'date-fns'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { MemberNameWithStatus } from '@/components/member-name-with-status'
|
||||
import { Play, Video } from 'lucide-react'
|
||||
|
||||
interface PhotoCardProps {
|
||||
photo: {
|
||||
url: string
|
||||
caption?: string
|
||||
uploadedAt: string
|
||||
memberId: string
|
||||
memberName: string
|
||||
isDead: boolean
|
||||
adminVisibleOverride: boolean
|
||||
visibleInOverview: boolean
|
||||
}
|
||||
isOwner: boolean
|
||||
currentTree?: { id?: string }
|
||||
onSelect: (url: string) => void
|
||||
onToggle: (memberId: string, photoUrl: string, value: boolean) => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const isVideoFile = (url: string) => {
|
||||
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
|
||||
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext))
|
||||
}
|
||||
|
||||
export const PhotoCard: React.FC<PhotoCardProps> = ({
|
||||
photo,
|
||||
isOwner,
|
||||
currentTree,
|
||||
onSelect,
|
||||
onToggle,
|
||||
isLoading
|
||||
}) => {
|
||||
const handleToggle = useCallback((checked: boolean) => {
|
||||
onToggle(photo.memberId, photo.url, checked)
|
||||
}, [photo.memberId, photo.url, onToggle])
|
||||
|
||||
if (photo.adminVisibleOverride === false) {
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden bg-card shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 border border-border/50">
|
||||
<div className="p-4 space-y-2">
|
||||
<div className="flex items-center justify-between gap-1.5 text-xs">
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<span>来自</span>
|
||||
<Link
|
||||
href={`/members/${photo.memberId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
|
||||
className="hover:text-primary hover:underline"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={photo.memberName}
|
||||
isDead={photo.isDead}
|
||||
className="text-foreground font-medium"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-muted-foreground/70 text-[10px]">
|
||||
{format(new Date(photo.uploadedAt), 'MM-dd')}
|
||||
</span>
|
||||
</div>
|
||||
{isOwner && (
|
||||
<div className="flex items-center justify-between gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span>允许展示</span>
|
||||
<Switch
|
||||
checked={photo.adminVisibleOverride ?? true}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isOwner && (
|
||||
<p className="text-[11px] text-muted-foreground">管理员已隐藏</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden bg-card shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 border border-border/50">
|
||||
{/* 媒体区域 */}
|
||||
<div
|
||||
className="relative cursor-zoom-in"
|
||||
onClick={() => onSelect(photo.url)}
|
||||
>
|
||||
{isVideoFile(photo.url) ? (
|
||||
<div className="relative">
|
||||
<video
|
||||
src={photo.url}
|
||||
className="w-full h-auto object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<div className="w-12 h-12 rounded-full bg-white/90 flex items-center justify-center">
|
||||
<Play className="h-6 w-6 text-black ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-2 left-2 bg-black/70 text-white text-xs px-2 py-1 rounded flex items-center gap-1">
|
||||
<Video className="h-3 w-3" />
|
||||
视频
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={photo.url}
|
||||
alt={photo.caption || `${photo.memberName}的照片`}
|
||||
className="w-full h-auto object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="px-3 py-2 bg-card border-t border-border/30 space-y-1">
|
||||
{/* 照片说明 */}
|
||||
<div className="text-xs line-clamp-2">
|
||||
{photo.caption ? (
|
||||
<span className="text-foreground">{photo.caption}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground/70">暂无说明</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分享人和时间 */}
|
||||
<div className="flex items-center justify-between gap-1.5 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">来自</span>
|
||||
<Link
|
||||
href={`/members/${photo.memberId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
|
||||
className="hover:text-primary hover:underline"
|
||||
>
|
||||
<MemberNameWithStatus
|
||||
name={photo.memberName}
|
||||
isDead={photo.isDead}
|
||||
className="text-foreground font-medium"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-muted-foreground/70 text-[10px]">
|
||||
{format(new Date(photo.uploadedAt), 'MM-dd')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 管理员权限开关 */}
|
||||
{isOwner && (
|
||||
<div className="mt-2 flex items-center justify-between gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span>允许展示</span>
|
||||
<Switch
|
||||
checked={photo.adminVisibleOverride ?? true}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 在主组件中使用
|
||||
```typescript
|
||||
import { PhotoCard } from '@/components/dashboard/photo-card'
|
||||
|
||||
// 在 JSX 中
|
||||
{groupedPhotosByMonth.map(([month, monthPhotos]) => (
|
||||
<div key={month}>
|
||||
<h4 className="text-sm font-medium text-muted-foreground mb-4 flex items-center gap-2 sticky top-0 bg-card/95 backdrop-blur py-2 z-10">
|
||||
<span className="w-2 h-2 rounded-full bg-primary"></span>
|
||||
{month}
|
||||
<span className="text-xs text-muted-foreground/70">({monthPhotos.length})</span>
|
||||
</h4>
|
||||
<div className="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 space-y-4">
|
||||
{monthPhotos.map((photo, index) => (
|
||||
<div key={`${photo.memberId}-${index}`} className="break-inside-avoid group">
|
||||
<PhotoCard
|
||||
photo={photo}
|
||||
isOwner={isOwner}
|
||||
currentTree={currentTree}
|
||||
onSelect={setSelectedPhoto}
|
||||
onToggle={handleAdminPhotoToggle}
|
||||
isLoading={adminToggleLoading === `${photo.memberId}|${photo.url}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 优化 6: 修复所有 useMemo 依赖项
|
||||
|
||||
### 检查清单
|
||||
```typescript
|
||||
// ❌ 不好的依赖项
|
||||
const monthlyAnniversaries = useMemo(() => {
|
||||
// ...
|
||||
}, [treeData]) // 整个对象
|
||||
|
||||
// ✅ 好的依赖项
|
||||
const monthlyAnniversaries = useMemo(() => {
|
||||
// ...
|
||||
}, [treeData.members]) // 只依赖需要的部分
|
||||
|
||||
// ❌ 不好的依赖项
|
||||
const allPhotos = useMemo(() => {
|
||||
// ...
|
||||
}, [treeData, isOwner]) // treeData 是整个对象
|
||||
|
||||
// ✅ 好的依赖项
|
||||
const allPhotos = useMemo(() => {
|
||||
// ...
|
||||
}, [treeData.members, isOwner]) // 只依赖需要的部分
|
||||
|
||||
// ❌ 不好的依赖项
|
||||
const locationGroups = useMemo(() => {
|
||||
// ...
|
||||
}, [treeData]) // 整个对象
|
||||
|
||||
// ✅ 好的依赖项
|
||||
const locationGroups = useMemo(() => {
|
||||
// ...
|
||||
}, [treeData.members]) // 只依赖需要的部分
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能测试建议
|
||||
|
||||
### 使用 React DevTools Profiler
|
||||
```typescript
|
||||
// 在浏览器控制台运行
|
||||
import { Profiler } from 'react'
|
||||
|
||||
// 包装组件
|
||||
<Profiler id="DashboardPage" onRender={(id, phase, actualDuration) => {
|
||||
console.log(`${id} (${phase}) took ${actualDuration}ms`)
|
||||
}}>
|
||||
<DashboardPage />
|
||||
</Profiler>
|
||||
```
|
||||
|
||||
### 测试场景
|
||||
1. **初始加载**:测量首次渲染时间
|
||||
2. **数据更新**:添加/删除成员后的重新渲染时间
|
||||
3. **照片加载**:加载大量照片时的性能
|
||||
4. **交互响应**:点击开关、展开折叠等操作的响应时间
|
||||
|
||||
### 预期改进
|
||||
- 初始加载时间:减少 20-30%
|
||||
- 重新渲染时间:减少 30-40%
|
||||
- 内存使用:减少 15-20%
|
||||
|
||||
Reference in New Issue
Block a user