This commit is contained in:
freedakgmail
2025-11-30 23:09:54 +08:00
parent 96aa886adc
commit debcce71bf
150 changed files with 431 additions and 321 deletions
+121 -56
View File
@@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import { SiteHeader } from "@/components/site-header"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator, Loader2, Images, ChevronDown, ChevronUp } from "lucide-react"
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator, Loader2, Images, ChevronDown, ChevronUp, EyeOff } from "lucide-react"
import { useFamily } from "@/context/family-context"
import { useMemo, useState, useEffect, useRef, useCallback } from "react"
import { useRouter } from "next/navigation"
@@ -23,6 +23,8 @@ import { permissions } from "@/lib/permissions"
import { isInCurrentMonth, solar2lunar, lunar2solar } from "@/lib/lunar-calendar"
import { MemberNameWithStatus } from "@/components/member-name-with-status"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Switch } from "@/components/ui/switch"
import type { FamilyPhoto } from "@/types/family"
// 动态导入统计图表组件(减少初始加载体积)
const StatisticsCharts = dynamic(
@@ -53,11 +55,12 @@ interface ActivityLog {
}
export default function DashboardPage() {
const { treeData, isLoading, currentTree } = useFamily()
const { treeData, isLoading, currentTree, updateMember } = useFamily()
const { data: session } = useSession()
const [recentActivities, setRecentActivities] = useState<ActivityLog[]>([])
const [selectedPhoto, setSelectedPhoto] = useState<string | null>(null)
const [statsExpanded, setStatsExpanded] = useState(true)
const [adminToggleLoading, setAdminToggleLoading] = useState<string | null>(null)
const router = useRouter()
// 首次登录跳转到使用帮助
@@ -158,6 +161,8 @@ export default function DashboardPage() {
}, [treeData])
// 收集所有成员的照片
const isOwner = session && currentTree?.ownerId === session.user?.id
const allPhotos = useMemo(() => {
const photos: Array<{
url: string
@@ -166,38 +171,51 @@ export default function DashboardPage() {
memberId: string
memberName: string
isDead: boolean
adminVisibleOverride: boolean
visibleInOverview: boolean
}> = []
Object.values(treeData.members).forEach(member => {
// 优先使用新格式 photos
if (member.photos && member.photos.length > 0) {
member.photos.forEach(photo => {
(member.photos as FamilyPhoto[]).forEach(photo => {
const visible = photo.visibleInOverview ?? false
if (!visible) return
const adminAllowed = photo.adminVisibleOverride ?? true
if (!adminAllowed && !isOwner) return
photos.push({
url: photo.url,
caption: photo.caption,
uploadedAt: photo.uploadedAt,
memberId: member.id,
memberName: member.fullName,
isDead: !!member.deathDate
})
})
} else if (member.photoIds && member.photoIds.length > 0) {
// 兼容旧格式 photoIds
member.photoIds.forEach(url => {
photos.push({
url,
uploadedAt: member.updatedAt || new Date().toISOString(),
memberId: member.id,
memberName: member.fullName,
isDead: !!member.deathDate
isDead: !!member.deathDate,
adminVisibleOverride: adminAllowed,
visibleInOverview: visible,
})
})
}
})
// 按上传时间倒序排列(最新的在前)
return photos.sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime())
}, [treeData])
}, [treeData, isOwner])
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])
// 获取本月纪念日(生日和忌日)- 支持农历
const monthlyAnniversaries = useMemo(() => {
@@ -569,48 +587,95 @@ export default function DashboardPage() {
className="break-inside-avoid group"
>
<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={() => setSelectedPhoto(photo.url)}
>
<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>
{photo.adminVisibleOverride === false ? (
<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), 'yyyy-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={(checked) => handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
/>
</div>
) : (
<span className="text-muted-foreground/70"></span>
<p className="text-[11px] text-muted-foreground"></p>
)}
</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
className="relative cursor-zoom-in"
onClick={() => setSelectedPhoto(photo.url)}
>
<img
src={photo.url}
alt={photo.caption || `${photo.memberName}的照片`}
className="w-full h-auto object-cover"
loading="lazy"
/>
</div>
<span className="text-muted-foreground/70 text-[10px]">
{format(new Date(photo.uploadedAt), 'yyyy-MM-dd')}
</span>
</div>
</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), 'yyyy-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={(checked) => handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
/>
</div>
)}
</div>
</>
)}
</div>
</div>
))}