0.6.2.0
This commit is contained in:
+165
-103
@@ -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, EyeOff } from "lucide-react"
|
||||
import { UserPlus, Network, Map, Calendar, Users, ArrowRight, Clock, Search, BookOpen, BarChart3, Calculator, Loader2, Images, ChevronDown, ChevronUp, EyeOff, Play, Video } from "lucide-react"
|
||||
import { useFamily } from "@/context/family-context"
|
||||
import { useMemo, useState, useEffect, useRef, useCallback } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
@@ -54,6 +54,12 @@ interface ActivityLog {
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为视频文件
|
||||
const isVideoFile = (url: string) => {
|
||||
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
|
||||
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext))
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { treeData, isLoading, currentTree, updateMember } = useFamily()
|
||||
const { data: session } = useSession()
|
||||
@@ -580,111 +586,158 @@ export default function DashboardPage() {
|
||||
</ChineseCardHeader>
|
||||
<ChineseCardContent>
|
||||
{allPhotos.length > 0 ? (
|
||||
<div className="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 space-y-4">
|
||||
{allPhotos.map((photo, index) => (
|
||||
<div
|
||||
key={`${photo.memberId}-${index}`}
|
||||
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">
|
||||
{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>
|
||||
) : (
|
||||
<p className="text-[11px] text-muted-foreground">管理员已隐藏</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 照片区域 - 点击放大 */}
|
||||
<div
|
||||
className="relative cursor-zoom-in"
|
||||
onClick={() => setSelectedPhoto(photo.url)}
|
||||
<div className="space-y-8">
|
||||
{/* 按月份分组显示 */}
|
||||
{(() => {
|
||||
// 按月份分组
|
||||
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]) => (
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
{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), '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>
|
||||
) : (
|
||||
<p className="text-[11px] text-muted-foreground">管理员已隐藏</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground/70">暂无说明</span>
|
||||
<>
|
||||
{/* 媒体区域 - 点击放大/播放 */}
|
||||
<div
|
||||
className="relative cursor-zoom-in"
|
||||
onClick={() => setSelectedPhoto(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={(checked) => handleAdminPhotoToggle(photo.memberId, photo.url, checked)}
|
||||
disabled={adminToggleLoading === `${photo.memberId}|${photo.url}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<Images className="h-12 w-12 mx-auto text-muted-foreground/30 mb-4" />
|
||||
<p className="text-muted-foreground">暂无照片</p>
|
||||
<p className="text-sm text-muted-foreground/70 mt-1">在成员详情页可以添加照片</p>
|
||||
<p className="text-muted-foreground">暂无照片或视频</p>
|
||||
<p className="text-sm text-muted-foreground/70 mt-1">在成员详情页可以添加照片或视频</p>
|
||||
</div>
|
||||
)}
|
||||
</ChineseCardContent>
|
||||
@@ -1102,20 +1155,29 @@ export default function DashboardPage() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 照片放大预览 Dialog */}
|
||||
{/* 媒体放大预览 Dialog */}
|
||||
<Dialog open={!!selectedPhoto} onOpenChange={() => setSelectedPhoto(null)}>
|
||||
<DialogContent
|
||||
className="max-w-4xl p-0 bg-black/95 border-none"
|
||||
overlayClassName="backdrop-blur-md bg-black/60"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogTitle className="sr-only">照片预览</DialogTitle>
|
||||
<DialogTitle className="sr-only">{selectedPhoto && isVideoFile(selectedPhoto) ? '视频播放' : '照片预览'}</DialogTitle>
|
||||
{selectedPhoto && (
|
||||
<img
|
||||
src={selectedPhoto}
|
||||
alt="照片预览"
|
||||
className="w-full h-auto max-h-[90vh] object-contain"
|
||||
/>
|
||||
isVideoFile(selectedPhoto) ? (
|
||||
<video
|
||||
src={selectedPhoto}
|
||||
controls
|
||||
autoPlay
|
||||
className="w-full h-auto max-h-[90vh] object-contain"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={selectedPhoto}
|
||||
alt="照片预览"
|
||||
className="w-full h-auto max-h-[90vh] object-contain"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user