添加相对时间格式化功能,优化评论区状态恢复逻辑,增强视频播放体验
This commit is contained in:
parent
5c98257366
commit
657c8320b5
@ -29,6 +29,26 @@ import {
|
||||
type User = { nickname: string; avatar_url: string | null };
|
||||
type Comment = { cid: string; text: string; digg_count: number; created_at: string | Date; user: User };
|
||||
|
||||
// 格式化相对时间
|
||||
function formatRelativeTime(date: string | Date): string {
|
||||
const now = new Date();
|
||||
const target = new Date(date);
|
||||
const diffMs = now.getTime() - target.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
const diffYears = Math.floor(diffDays / 365);
|
||||
|
||||
if (diffYears > 0) return `${diffYears}年前`;
|
||||
if (diffMonths > 0) return `${diffMonths}月前`;
|
||||
if (diffDays > 0) return `${diffDays}天前`;
|
||||
if (diffHours > 0) return `${diffHours}小时前`;
|
||||
if (diffMinutes > 0) return `${diffMinutes}分钟前`;
|
||||
return '刚刚';
|
||||
}
|
||||
|
||||
// 处理评论文本中的表情占位符
|
||||
function parseCommentText(text: string): (string | { type: "emoji"; name: string })[] {
|
||||
const parts: (string | { type: "emoji"; name: string })[] = [];
|
||||
@ -117,15 +137,25 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
const router = useRouter();
|
||||
|
||||
// ====== 布局 & 评论 ======
|
||||
const [open, setOpen] = useState(() => {
|
||||
// 从 localStorage 读取评论区状态,默认 false
|
||||
if (typeof window === "undefined") return false;
|
||||
const saved = localStorage.getItem("aweme_player_comments_open");
|
||||
if (!saved) return false;
|
||||
return saved === "true";
|
||||
}); // 评论是否展开(竖屏为 bottom sheet;横屏为并排分栏)
|
||||
const [open, setOpen] = useState(false); // 评论是否展开(竖屏为 bottom sheet;横屏为并排分栏)
|
||||
const [mounted, setMounted] = useState(false); // 用于跳过首次加载的动画
|
||||
const comments = useMemo(() => data.comments ?? [], [data]);
|
||||
|
||||
// ====== 从 localStorage 恢复评论区状态(仅客户端) ======
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = localStorage.getItem("aweme_player_comments_open");
|
||||
if (saved === "true") {
|
||||
setOpen(true);
|
||||
}
|
||||
// 短暂延迟后标记为已挂载,启用动画
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setMounted(true);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ====== 媒体引用 ======
|
||||
const mediaContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
@ -316,6 +346,33 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
};
|
||||
}, [isVideo, loopMode, neighbors?.next?.aweme_id, router]);
|
||||
|
||||
// ====== 视频:监听自动播放失败 ======
|
||||
useEffect(() => {
|
||||
if (!isVideo) return;
|
||||
const v = videoRef.current;
|
||||
if (!v) return;
|
||||
|
||||
// 检测自动播放是否成功
|
||||
const checkAutoplay = async () => {
|
||||
try {
|
||||
await v.play();
|
||||
setIsPlaying(true);
|
||||
} catch (error) {
|
||||
// 自动播放失败(通常是浏览器策略限制)
|
||||
console.log("自动播放被阻止,需要用户交互");
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 等待元数据加载后尝试播放
|
||||
if (v.readyState >= 1) {
|
||||
checkAutoplay();
|
||||
} else {
|
||||
v.addEventListener("loadedmetadata", checkAutoplay, { once: true });
|
||||
return () => v.removeEventListener("loadedmetadata", checkAutoplay);
|
||||
}
|
||||
}, [isVideo, data.aweme_id]); // 依赖 aweme_id 确保切换视频时重新检查
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVideo) return;
|
||||
const v = videoRef.current;
|
||||
@ -488,7 +545,20 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
if (document.body.requestFullscreen) {
|
||||
document.body.requestFullscreen().catch(() => { });
|
||||
return
|
||||
}
|
||||
const vRef = videoRef.current;
|
||||
if (vRef && vRef.requestFullscreen) {
|
||||
vRef.requestFullscreen().catch(() => { });
|
||||
return
|
||||
}
|
||||
// @ts-ignore
|
||||
if (vRef && vRef.webkitEnterFullscreen) {
|
||||
// @ts-ignore
|
||||
vRef.webkitEnterFullscreen();
|
||||
}
|
||||
} else {
|
||||
document.exitFullscreen().catch(() => { });
|
||||
}
|
||||
@ -547,21 +617,54 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// ====== 侧栏(横屏)/ 抽屉(竖屏)样式(Tailwind)
|
||||
const asideClasses = [
|
||||
"z-30 flex flex-col bg-[rgba(22,22,22,0.92)] text-white",
|
||||
// 竖屏:bottom sheet,从下向上弹出
|
||||
"portrait:fixed portrait:inset-x-0 portrait:top-110 portrait:w-full portrait:h-[min(80vh,88dvh)]",
|
||||
"portrait:transition-transform portrait:duration-200 portrait:ease-out",
|
||||
open ? "portrait:translate-y-0" : "portrait:translate-y-full",
|
||||
"portrait:border-t portrait:border-white/10",
|
||||
// 横屏:并排分栏,宽度过渡
|
||||
"landscape:relative landscape:h-full landscape:overflow-hidden",
|
||||
"landscape:transition-[width] landscape:duration-200 landscape:ease-out",
|
||||
open
|
||||
? "landscape:w-[min(420px,36vw)] landscape:border-l landscape:border-white/10"
|
||||
: "landscape:w-0",
|
||||
].join(" ");
|
||||
// ====== 评论内容组件 - 使用 useMemo 避免不必要的重新渲染 ======
|
||||
const commentContent = useMemo(() => (
|
||||
<>
|
||||
<header className="flex items-center gap-4 mb-5">
|
||||
<div className="size-10 rounded-full overflow-hidden bg-zinc-700/60">
|
||||
{data.author.avatar_url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={data.author.avatar_url} alt="avatar" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white/95 text-sm sm:text-base">{data.author.nickname}</div>
|
||||
<div className="text-xs text-white/50" title={new Date(data.created_at).toLocaleString()}>
|
||||
发布于 {formatRelativeTime(data.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ul className="space-y-4 sm:space-y-5">
|
||||
{comments.map((c) => (
|
||||
<li key={c.cid} className="flex items-start gap-3 sm:gap-4">
|
||||
<div className="size-8 rounded-full overflow-hidden bg-zinc-700/60 shrink-0">
|
||||
{c.user.avatar_url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={c.user.avatar_url} alt="avatar" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white/95 text-sm">{c.user.nickname}</span>
|
||||
<span className="text-xs text-white/50" title={new Date(c.created_at).toLocaleString()}>
|
||||
{formatRelativeTime(c.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-relaxed text-white/90 break-words">
|
||||
<CommentText text={c.text} />
|
||||
</p>
|
||||
<div className="mt-2 inline-flex items-center gap-1 text-xs text-white/70">
|
||||
<ThumbsUp size={14} />
|
||||
<span>{c.digg_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{comments.length === 0 ? <li className="text-sm text-white/60">暂无评论</li> : null}
|
||||
</ul>
|
||||
</>
|
||||
), [comments, data.author, data.created_at]);
|
||||
|
||||
// ====== 预取上/下一条路由,提高切换流畅度 ======
|
||||
useEffect(() => {
|
||||
@ -600,6 +703,72 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
return () => el.removeEventListener("wheel", onWheel as any);
|
||||
}, [neighbors?.next?.aweme_id, neighbors?.prev?.aweme_id]);
|
||||
|
||||
// ====== 键盘快捷键 ======
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
// 如果焦点在输入框等元素上,不处理快捷键
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
// 上下方向键 / w s:切换上一条/下一条视频
|
||||
if (key === 'arrowup' || key === 'w') {
|
||||
e.preventDefault();
|
||||
const now = performance.now();
|
||||
if (now - wheelCooldownRef.current < 700) return; // 冷却 700ms
|
||||
if (neighbors?.prev) {
|
||||
wheelCooldownRef.current = now;
|
||||
router.push(`/aweme/${neighbors.prev.aweme_id}`);
|
||||
}
|
||||
} else if (key === 'arrowdown' || key === 's') {
|
||||
e.preventDefault();
|
||||
const now = performance.now();
|
||||
if (now - wheelCooldownRef.current < 700) return; // 冷却 700ms
|
||||
if (neighbors?.next) {
|
||||
wheelCooldownRef.current = now;
|
||||
router.push(`/aweme/${neighbors.next.aweme_id}`);
|
||||
}
|
||||
}
|
||||
// 左右方向键 / a d:快进快退(视频) 或 切换图片(图文)
|
||||
else if (key === 'arrowleft' || key === 'a') {
|
||||
e.preventDefault();
|
||||
if (isVideo) {
|
||||
// 视频:后退 5 秒
|
||||
const v = videoRef.current;
|
||||
if (v && v.duration) {
|
||||
v.currentTime = Math.max(0, v.currentTime - 5);
|
||||
}
|
||||
} else {
|
||||
// 图文:上一张
|
||||
prevImg();
|
||||
}
|
||||
} else if (key === 'arrowright' || key === 'd') {
|
||||
e.preventDefault();
|
||||
if (isVideo) {
|
||||
// 视频:前进 5 秒
|
||||
const v = videoRef.current;
|
||||
if (v && v.duration) {
|
||||
v.currentTime = Math.min(v.duration, v.currentTime + 5);
|
||||
}
|
||||
} else {
|
||||
// 图文:下一张
|
||||
nextImg();
|
||||
}
|
||||
}
|
||||
// 空格:播放/暂停
|
||||
else if (key === ' ') {
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [isVideo, neighbors?.prev?.aweme_id, neighbors?.next?.aweme_id, router]);
|
||||
|
||||
// ====== 动态模糊背景:定时截取当前媒体内容绘制到背景 canvas ======
|
||||
useEffect(() => {
|
||||
const canvas = backgroundCanvasRef.current;
|
||||
@ -610,11 +779,18 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
|
||||
// 更新 canvas 尺寸以匹配视口
|
||||
const updateCanvasSize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.width = Math.floor(window.innerWidth / 10);
|
||||
canvas.height = Math.floor(window.innerHeight / 10);
|
||||
};
|
||||
updateCanvasSize();
|
||||
window.addEventListener("resize", updateCanvasSize);
|
||||
|
||||
// 防抖处理 resize 事件(300ms)
|
||||
let resizeTimeout: NodeJS.Timeout;
|
||||
const debouncedResize = () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(updateCanvasSize, 300);
|
||||
};
|
||||
window.addEventListener("resize", debouncedResize);
|
||||
|
||||
// 绘制媒体内容到 canvas(cover 策略)
|
||||
const drawMediaToCanvas = () => {
|
||||
@ -672,12 +848,12 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
ctx.drawImage(sourceElement, offsetX, offsetY, drawWidth, drawHeight);
|
||||
};
|
||||
|
||||
// 使用较高频率的定时器以保持背景连贯(20ms ~= 50fps)
|
||||
const intervalId = setInterval(drawMediaToCanvas, 20);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
window.removeEventListener("resize", updateCanvasSize);
|
||||
window.removeEventListener("resize", debouncedResize);
|
||||
clearTimeout(resizeTimeout);
|
||||
};
|
||||
}, [isVideo, idx]);
|
||||
|
||||
@ -717,7 +893,6 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
objectFit,
|
||||
}}
|
||||
playsInline
|
||||
autoPlay
|
||||
loop={loopMode === "loop"}
|
||||
onClick={togglePlay}
|
||||
/>
|
||||
@ -761,15 +936,30 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 暂停状态时显示的播放图标 */}
|
||||
{!isPlaying && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-20">
|
||||
<div className="w-20 h-20 rounded-full bg-black/40 backdrop-blur-sm border border-white/30 flex items-center justify-center">
|
||||
<Play size={40} className="text-white/90 ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 统一控制条:desc 在上、进度在下 */}
|
||||
<div className="absolute left-0 right-0 bottom-0 px-3 pb-4 pt-2 bg-gradient-to-b from-black/0 via-black/45 to-black/65 flex flex-col gap-2.5">
|
||||
{/* 描述行 */}
|
||||
|
||||
<div className="pointer-events-none flex items-center gap-2.5 mb-1">
|
||||
<img src={data.author.avatar_url!} alt="" className="w-8 h-8 rounded-full" />
|
||||
<span className="text-[13px] leading-tight text-white/95 max-h-[3.9em] overflow-hidden [display:-webkit-box] [-webkit-line-clamp:3] [-webkit-box-orient:vertical] drop-shadow">
|
||||
<span className="text-[15px] leading-tight text-white/95 drop-shadow">
|
||||
{data.author.nickname}
|
||||
</span>
|
||||
<span className="text-[13px] leading-tight text-white/95 drop-shadow">
|
||||
·
|
||||
</span>
|
||||
<span className="text-[11px] leading-tight text-white/95 drop-shadow" title={new Date(data.created_at).toLocaleString()}>
|
||||
{formatRelativeTime(data.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{data.desc ? (
|
||||
@ -824,41 +1014,20 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 控制按钮行 */}
|
||||
<div className="flex items-center justify-between gap-2.5">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{/* 控制按钮行 - 响应式布局 */}
|
||||
<div className="flex items-center justify-between gap-1.5 sm:gap-2.5">
|
||||
{/* 左侧:播放控制 + 时间/进度 */}
|
||||
<div className="inline-flex items-center gap-1.5 sm:gap-2 min-w-0">
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
className="w-[34px] h-[34px] shrink-0 inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={togglePlay}
|
||||
aria-label={isPlaying ? "暂停" : "播放"}
|
||||
>
|
||||
{isPlaying ? <Pause size={18} /> : <Play size={18} />}
|
||||
</button>
|
||||
|
||||
|
||||
{/* 倍速仅视频展示 */}
|
||||
{isVideo ? (
|
||||
<>
|
||||
<button
|
||||
className="h-[30px] px-[10px] rounded-full text-[12px] bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={() => {
|
||||
const steps = [1, 1.25, 1.5, 2, 0.75, 0.5];
|
||||
const i = steps.indexOf(rate);
|
||||
const next = steps[(i + 1) % steps.length];
|
||||
setRate(next);
|
||||
}}
|
||||
aria-label="切换倍速"
|
||||
>
|
||||
{rate}x
|
||||
</button>
|
||||
|
||||
|
||||
{/* 旋转:向左/向右各 90° */}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* 播放进度显示 */}
|
||||
<div className="text-[13px] text-white/90 font-mono min-w-[80px] ml-2">
|
||||
{/* 播放进度显示 - 所有设备都显示 */}
|
||||
<div className="text-[13px] text-white/90 font-mono min-w-[70px] sm:min-w-[80px]">
|
||||
{isVideo ? (
|
||||
(() => {
|
||||
const v = videoRef.current;
|
||||
@ -870,11 +1039,29 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
`${idx + 1} / ${totalSegments}`
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 倍速 - 中等屏幕以上显示,仅视频 */}
|
||||
{isVideo && (
|
||||
<button
|
||||
className="hidden md:block h-[30px] px-[10px] rounded-full text-[12px] bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer shrink-0"
|
||||
onClick={() => {
|
||||
const steps = [1, 1.25, 1.5, 2, 0.75, 0.5];
|
||||
const i = steps.indexOf(rate);
|
||||
const next = steps[(i + 1) % steps.length];
|
||||
setRate(next);
|
||||
}}
|
||||
aria-label="切换倍速"
|
||||
>
|
||||
{rate}x
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{/* 中间:音量控制 - 中等屏幕以上显示 */}
|
||||
<div className="hidden md:inline-flex items-center gap-2 shrink-0">
|
||||
{/* 旋转按钮 - 小屏幕以上显示 */}
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
className="hidden sm:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={() => setRotation((r) => (r + 270) % 360)}
|
||||
aria-label="向左旋转 90 度"
|
||||
title="向左旋转 90 度"
|
||||
@ -895,11 +1082,11 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
step={0.05}
|
||||
value={volume}
|
||||
onChange={(e) => setVolume(parseFloat(e.target.value))}
|
||||
className="w-28 accent-white cursor-pointer"
|
||||
className="w-20 lg:w-28 accent-white cursor-pointer"
|
||||
aria-label="音量"
|
||||
/>
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
className="hidden sm:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={() => setRotation((r) => (r + 90) % 360)}
|
||||
aria-label="向右旋转 90 度"
|
||||
title="向右旋转 90 度"
|
||||
@ -908,32 +1095,49 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{/* 循环模式切换 */}
|
||||
{/* 右侧:功能按钮组 */}
|
||||
<div className="inline-flex items-center gap-1 sm:gap-1.5 lg:gap-2 shrink-0">
|
||||
|
||||
{/* 音量按钮 - 仅在小屏幕显示(中等屏幕以上有滑块) */}
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
className="md:hidden w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={() => setVolume((v) => (v > 0 ? 0 : 1))}
|
||||
aria-label={volume > 0 ? "静音" : "取消静音"}
|
||||
>
|
||||
{volume > 0 ? <Volume2 size={18} /> : <VolumeX size={18} />}
|
||||
</button>
|
||||
|
||||
{/* 循环模式 - 中等屏幕以上显示 */}
|
||||
<button
|
||||
className="hidden md:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={() => setLoopMode((m) => (m === "loop" ? "sequential" : "loop"))}
|
||||
aria-label={loopMode === "loop" ? "循环播放" : "顺序播放"}
|
||||
title={loopMode === "loop" ? "循环播放" : "顺序播放"}
|
||||
>
|
||||
{loopMode === "loop" ? <Repeat1 size={18} /> : <ArrowDownUp size={18} />}
|
||||
</button>
|
||||
|
||||
{/* 适配模式 - 小屏幕以上显示 */}
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
className="hidden sm:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={() => setObjectFit((f) => (f === "contain" ? "cover" : "contain"))}
|
||||
aria-label={objectFit === "contain" ? "切换到填充模式" : "切换到适应模式"}
|
||||
title={objectFit === "contain" ? "切换到填充模式" : "切换到适应模式"}
|
||||
>
|
||||
{objectFit === "contain" ? <Maximize2 size={18} /> : <Minimize size={18} />}
|
||||
</button>
|
||||
|
||||
{/* 下载 - 中等屏幕以上显示 */}
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
className="hidden md:inline-flex w-[34px] h-[34px] items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={handleDownload}
|
||||
aria-label={isVideo ? "下载视频" : "下载当前图片"}
|
||||
title={isVideo ? "下载视频" : "下载当前图片"}
|
||||
>
|
||||
<Download size={18} />
|
||||
</button>
|
||||
|
||||
{/* 全屏 - 所有设备都显示 */}
|
||||
<button
|
||||
className="w-[34px] h-[34px] inline-flex items-center justify-center rounded-full bg-white/15 text-white border border-white/20 backdrop-blur-sm cursor-pointer"
|
||||
onClick={toggleFullscreen}
|
||||
@ -994,15 +1198,52 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 评论面板:竖屏 bottom sheet;横屏并排分栏 */}
|
||||
<aside className={asideClasses}>
|
||||
{/* 横屏评论面板:并排分栏 */}
|
||||
<aside
|
||||
className={`
|
||||
hidden landscape:flex
|
||||
z-30 flex-col bg-[rgba(22,22,22,0.92)] text-white
|
||||
relative h-full overflow-hidden
|
||||
${mounted ? "transition-[width] duration-200 ease-out" : ""}
|
||||
${open ? "w-[min(420px,36vw)] border-l border-white/10" : "w-0"}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-3 py-3 border-b border-white/10">
|
||||
{/* 竖屏:评论在左,关闭按钮在右 */}
|
||||
<div className="text-white font-semibold portrait:order-1 landscape:order-2">
|
||||
<button
|
||||
className="w-8 h-8 inline-flex items-center justify-center rounded-full bg-white/15 text-white/90 border border-white/20 hover:bg-white/25 transition-colors"
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="关闭评论"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
<div className="text-white font-semibold">
|
||||
评论 {comments.length > 0 ? `(${comments.length})` : ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 overflow-auto">
|
||||
{commentContent}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* 竖屏评论面板:bottom sheet */}
|
||||
<aside
|
||||
className={`
|
||||
landscape:hidden
|
||||
z-30 flex flex-col bg-[rgba(22,22,22,0.92)] text-white
|
||||
fixed inset-x-0 bottom-0 w-full h-[min(80vh,88dvh)]
|
||||
${mounted ? "transition-transform duration-200 ease-out" : ""}
|
||||
border-t border-white/10
|
||||
${open ? "translate-y-0" : "translate-y-full"}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between px-3 py-3 border-b border-white/10">
|
||||
<div className="text-white font-semibold">
|
||||
评论 {comments.length > 0 ? `(${comments.length})` : ""}
|
||||
</div>
|
||||
<button
|
||||
className="w-8 h-8 inline-flex items-center justify-center rounded-full bg-white/15 text-white/90 border border-white/20 hover:bg-white/25 transition-colors portrait:order-2 landscape:order-1"
|
||||
className="w-8 h-8 inline-flex items-center justify-center rounded-full bg-white/15 text-white/90 border border-white/20 hover:bg-white/25 transition-colors"
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="关闭评论"
|
||||
>
|
||||
@ -1011,48 +1252,9 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
|
||||
</div>
|
||||
|
||||
<div className="p-3 overflow-auto">
|
||||
<header className="flex items-center gap-4 mb-5">
|
||||
<div className="size-10 rounded-full overflow-hidden bg-zinc-700/60">
|
||||
{data.author.avatar_url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={data.author.avatar_url} alt="avatar" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white/95 text-sm sm:text-base">{data.author.nickname}</div>
|
||||
<div className="text-xs text-white/50">发布于 {new Date(data.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ul className="space-y-4 sm:space-y-5">
|
||||
{comments.map((c) => (
|
||||
<li key={c.cid} className="flex items-start gap-3 sm:gap-4">
|
||||
<div className="size-8 rounded-full overflow-hidden bg-zinc-700/60 shrink-0">
|
||||
{c.user.avatar_url ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={c.user.avatar_url} alt="avatar" className="w-full h-full object-cover" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white/95 text-sm">{c.user.nickname}</span>
|
||||
<span className="text-xs text-white/50">{new Date(c.created_at).toLocaleString()}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-relaxed text-white/90 break-words">
|
||||
<CommentText text={c.text} />
|
||||
</p>
|
||||
<div className="mt-2 inline-flex items-center gap-1 text-xs text-white/70">
|
||||
<ThumbsUp size={14} />
|
||||
<span>{c.digg_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{comments.length === 0 ? <li className="text-sm text-white/60">暂无评论</li> : null}
|
||||
</ul>
|
||||
{commentContent}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user