0.6.0.0
This commit is contained in:
+121
-56
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user