0.9.0.0
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
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 | null
|
||||
currentTree?: { id?: string } | null
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user