|
|
|
|
@ -1,52 +1,72 @@
|
|
|
|
|
import React, {useCallback, useEffect, useState} from 'react';
|
|
|
|
|
import {Button, Dialog, DialogClose, DialogContent, LucideIcon} from '@tryghost/shade';
|
|
|
|
|
import {ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
|
|
|
|
import {getAttachment} from '@components/feed/FeedItem';
|
|
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
|
|
|
// 导入UI组件:按钮、对话框及内容容器、Lucide图标库
|
|
|
|
|
import { Button, Dialog, DialogClose, DialogContent, LucideIcon } from '@tryghost/shade';
|
|
|
|
|
// 导入活动发布对象的类型定义
|
|
|
|
|
import { ObjectProperties } from '@tryghost/admin-x-framework/api/activitypub';
|
|
|
|
|
// 导入获取附件的工具函数
|
|
|
|
|
import { getAttachment } from '@components/feed/FeedItem';
|
|
|
|
|
|
|
|
|
|
// 定义灯箱中图片的类型接口
|
|
|
|
|
export interface LightboxImage {
|
|
|
|
|
url: string;
|
|
|
|
|
alt: string;
|
|
|
|
|
url: string; // 图片URL
|
|
|
|
|
alt: string; // 图片替代文本
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 定义灯箱状态的类型接口
|
|
|
|
|
export interface LightboxState {
|
|
|
|
|
images: LightboxImage[];
|
|
|
|
|
currentIndex: number;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
images: LightboxImage[]; // 所有图片列表
|
|
|
|
|
currentIndex: number; // 当前显示图片的索引
|
|
|
|
|
isOpen: boolean; // 灯箱是否打开
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 自定义Hook:管理图片灯箱的状态和操作
|
|
|
|
|
* @param object - 包含图片的活动发布对象
|
|
|
|
|
* @returns 灯箱状态及相关操作方法
|
|
|
|
|
*/
|
|
|
|
|
export function useLightboxImages(object: ObjectProperties | null) {
|
|
|
|
|
// 初始化灯箱状态
|
|
|
|
|
const [lightboxState, setLightboxState] = useState<LightboxState>({
|
|
|
|
|
images: [],
|
|
|
|
|
currentIndex: 0,
|
|
|
|
|
isOpen: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从对象中提取所有图片
|
|
|
|
|
* @param obj - 活动发布对象
|
|
|
|
|
* @returns 提取出的图片数组
|
|
|
|
|
*/
|
|
|
|
|
const getAllImagesFromAttachment = (obj: ObjectProperties): LightboxImage[] => {
|
|
|
|
|
// 获取对象的附件
|
|
|
|
|
const attachment = getAttachment(obj);
|
|
|
|
|
if (!attachment) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理附件为数组的情况
|
|
|
|
|
if (Array.isArray(attachment)) {
|
|
|
|
|
return attachment.map((item, index) => ({
|
|
|
|
|
url: item.url,
|
|
|
|
|
alt: item.name || `Image-${index}`
|
|
|
|
|
alt: item.name || `Image-${index}` // 用索引作为默认alt文本
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理单个图片附件
|
|
|
|
|
if (attachment.mediaType?.startsWith('image/') || attachment.type === 'Image') {
|
|
|
|
|
return [{
|
|
|
|
|
url: attachment.url,
|
|
|
|
|
alt: attachment.name || 'Image'
|
|
|
|
|
alt: attachment.name || 'Image' // 用默认文本作为fallback
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理对象中直接包含image字段的情况
|
|
|
|
|
if (obj.image) {
|
|
|
|
|
let imageUrl;
|
|
|
|
|
if (typeof obj.image === 'string') {
|
|
|
|
|
imageUrl = obj.image;
|
|
|
|
|
imageUrl = obj.image; // 图片URL直接是字符串
|
|
|
|
|
} else {
|
|
|
|
|
imageUrl = obj.image?.url;
|
|
|
|
|
imageUrl = obj.image?.url; // 图片是对象,取其url属性
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (imageUrl) {
|
|
|
|
|
@ -57,17 +77,23 @@ export function useLightboxImages(object: ObjectProperties | null) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
return []; // 没有找到图片时返回空数组
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 打开灯箱并显示指定图片
|
|
|
|
|
* @param clickedUrl - 被点击图片的URL
|
|
|
|
|
*/
|
|
|
|
|
const openLightbox = (clickedUrl: string) => {
|
|
|
|
|
if (!object) {
|
|
|
|
|
return;
|
|
|
|
|
return; // 对象为空时不执行操作
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取所有图片并找到被点击图片的索引
|
|
|
|
|
const images = getAllImagesFromAttachment(object);
|
|
|
|
|
const clickedIndex = images.findIndex(img => img.url === clickedUrl);
|
|
|
|
|
|
|
|
|
|
// 找到对应图片时更新灯箱状态
|
|
|
|
|
if (clickedIndex !== -1) {
|
|
|
|
|
setLightboxState({
|
|
|
|
|
images,
|
|
|
|
|
@ -77,6 +103,9 @@ export function useLightboxImages(object: ObjectProperties | null) {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 关闭灯箱
|
|
|
|
|
*/
|
|
|
|
|
const closeLightbox = () => {
|
|
|
|
|
setLightboxState(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
@ -84,6 +113,10 @@ export function useLightboxImages(object: ObjectProperties | null) {
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 导航到指定索引的图片
|
|
|
|
|
* @param newIndex - 目标图片索引
|
|
|
|
|
*/
|
|
|
|
|
const navigateToIndex = (newIndex: number) => {
|
|
|
|
|
setLightboxState(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
@ -99,14 +132,18 @@ export function useLightboxImages(object: ObjectProperties | null) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 图片灯箱组件的属性接口
|
|
|
|
|
interface ImageLightboxProps {
|
|
|
|
|
images: LightboxImage[];
|
|
|
|
|
currentIndex: number;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onNavigate: (newIndex: number) => void;
|
|
|
|
|
images: LightboxImage[]; // 图片列表
|
|
|
|
|
currentIndex: number; // 当前显示图片索引
|
|
|
|
|
isOpen: boolean; // 是否打开
|
|
|
|
|
onClose: () => void; // 关闭回调
|
|
|
|
|
onNavigate: (newIndex: number) => void; // 导航回调
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 图片灯箱组件:用于放大查看图片,支持左右导航和键盘控制
|
|
|
|
|
*/
|
|
|
|
|
const ImageLightbox: React.FC<ImageLightboxProps> = ({
|
|
|
|
|
images,
|
|
|
|
|
currentIndex,
|
|
|
|
|
@ -114,44 +151,65 @@ const ImageLightbox: React.FC<ImageLightboxProps> = ({
|
|
|
|
|
onClose,
|
|
|
|
|
onNavigate
|
|
|
|
|
}) => {
|
|
|
|
|
// 判断是否是第一张/最后一张图片
|
|
|
|
|
const isFirstImage = currentIndex === 0;
|
|
|
|
|
const isLastImage = currentIndex === images.length - 1;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 导航到下一张图片
|
|
|
|
|
* 使用useCallback缓存函数,避免不必要的重渲染
|
|
|
|
|
*/
|
|
|
|
|
const goToNext = useCallback(() => {
|
|
|
|
|
// 只有一张图片或已经是最后一张时不执行
|
|
|
|
|
if (images.length <= 1 || isLastImage) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 计算下一张索引(循环导航)
|
|
|
|
|
const nextIndex = (currentIndex + 1) % images.length;
|
|
|
|
|
onNavigate(nextIndex);
|
|
|
|
|
}, [images.length, isLastImage, currentIndex, onNavigate]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 导航到上一张图片
|
|
|
|
|
* 使用useCallback缓存函数
|
|
|
|
|
*/
|
|
|
|
|
const goToPrev = useCallback(() => {
|
|
|
|
|
// 只有一张图片或已经是第一张时不执行
|
|
|
|
|
if (images.length <= 1 || isFirstImage) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 计算上一张索引(循环导航)
|
|
|
|
|
const prevIndex = (currentIndex - 1 + images.length) % images.length;
|
|
|
|
|
onNavigate(prevIndex);
|
|
|
|
|
}, [images.length, isFirstImage, currentIndex, onNavigate]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 监听键盘事件,支持左右箭头导航
|
|
|
|
|
*/
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
return;
|
|
|
|
|
return; // 灯箱关闭时不处理
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 右箭头导航到下一张(非最后一张时)
|
|
|
|
|
if (e.key === 'ArrowRight' && !isLastImage) {
|
|
|
|
|
goToNext();
|
|
|
|
|
} else if (e.key === 'ArrowLeft' && !isFirstImage) {
|
|
|
|
|
}
|
|
|
|
|
// 左箭头导航到上一张(非第一张时)
|
|
|
|
|
else if (e.key === 'ArrowLeft' && !isFirstImage) {
|
|
|
|
|
goToPrev();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
// 组件卸载时移除事件监听
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
};
|
|
|
|
|
}, [isOpen, currentIndex, images.length, goToNext, goToPrev, isLastImage, isFirstImage]);
|
|
|
|
|
|
|
|
|
|
// 灯箱未打开或没有图片时不渲染
|
|
|
|
|
if (!isOpen || images.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
@ -159,37 +217,46 @@ const ImageLightbox: React.FC<ImageLightboxProps> = ({
|
|
|
|
|
return (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={isOpen}
|
|
|
|
|
// 监听对话框状态变化,关闭时触发回调
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogContent className="top-[50%] h-[100vh] max-h-[100vh] w-[100vw] max-w-[100vw] translate-y-[-50%] items-center border-none bg-transparent p-0 shadow-none data-[state=closed]:zoom-out-100 data-[state=open]:zoom-in-100 data-[state=closed]:slide-out-to-top-[50%] data-[state=open]:slide-in-from-top-[50%]" onClick={() => onClose()}>
|
|
|
|
|
{/* 灯箱内容容器,全屏显示且居中 */}
|
|
|
|
|
<DialogContent
|
|
|
|
|
className="top-[50%] h-[100vh] max-h-[100vh] w-[100vw] max-w-[100vw] translate-y-[-50%] items-center border-none bg-transparent p-0 shadow-none data-[state=closed]:zoom-out-100 data-[state=open]:zoom-in-100 data-[state=closed]:slide-out-to-top-[50%] data-[state=open]:slide-in-from-top-[50%]"
|
|
|
|
|
onClick={() => onClose()} // 点击空白区域关闭
|
|
|
|
|
>
|
|
|
|
|
{/* 当前显示的图片 */}
|
|
|
|
|
<img
|
|
|
|
|
alt={images[currentIndex].alt}
|
|
|
|
|
className="mx-auto max-h-[90vh] max-w-[90vw] object-contain"
|
|
|
|
|
className="mx-auto max-h-[90vh] max-w-[90vw] object-contain" // 保持比例,限制最大尺寸
|
|
|
|
|
src={images[currentIndex].url}
|
|
|
|
|
onClick={e => e.stopPropagation()}
|
|
|
|
|
onClick={e => e.stopPropagation()} // 点击图片本身不关闭灯箱
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 多张图片时显示导航按钮 */}
|
|
|
|
|
{images.length > 1 && (
|
|
|
|
|
<>
|
|
|
|
|
{/* 上一张按钮 */}
|
|
|
|
|
<Button
|
|
|
|
|
className="absolute left-5 top-1/2 size-11 -translate-y-1/2 rounded-full bg-black/50 p-0 pr-0.5 hover:bg-black/70"
|
|
|
|
|
disabled={isFirstImage}
|
|
|
|
|
disabled={isFirstImage} // 第一张时禁用
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.stopPropagation(); // 阻止事件冒泡(避免关闭灯箱)
|
|
|
|
|
goToPrev();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<LucideIcon.ChevronLeft className="!size-6" />
|
|
|
|
|
<span className="sr-only">Previous image</span>
|
|
|
|
|
<span className="sr-only">Previous image</span> // 屏幕阅读器文本
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* 下一张按钮 */}
|
|
|
|
|
<Button
|
|
|
|
|
className="absolute right-5 top-1/2 size-11 -translate-y-1/2 rounded-full bg-black/50 p-0 pl-0.5 hover:bg-black/70"
|
|
|
|
|
disabled={isLastImage}
|
|
|
|
|
disabled={isLastImage} // 最后一张时禁用
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
goToNext();
|
|
|
|
|
@ -200,6 +267,8 @@ const ImageLightbox: React.FC<ImageLightboxProps> = ({
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 关闭按钮 */}
|
|
|
|
|
<DialogClose asChild>
|
|
|
|
|
<Button className="absolute right-5 top-5 size-11 rounded-full bg-black/50 p-0 hover:bg-black/70">
|
|
|
|
|
<LucideIcon.X className="!size-5" />
|
|
|
|
|
@ -211,4 +280,4 @@ const ImageLightbox: React.FC<ImageLightboxProps> = ({
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ImageLightbox;
|
|
|
|
|
export default ImageLightbox;
|