@@ -17,6 +17,13 @@ import {
RotateCw ,
Maximize2 ,
Minimize ,
ChevronUp ,
ChevronDown ,
X ,
Repeat ,
Repeat1 ,
ArrowDownUp ,
Download ,
} from "lucide-react" ;
type User = { nickname : string ; avatar_url : string | null } ;
@@ -149,6 +156,13 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
const [ rotation , setRotation ] = useState ( 0 ) ; // 视频旋转角度:0/90/180/270
const [ progressRestored , setProgressRestored ] = useState ( false ) ; // 标记进度是否已恢复
const [ objectFit , setObjectFit ] = useState < "contain" | "cover" > ( "contain" ) ; // 媒体显示模式
const [ loopMode , setLoopMode ] = useState < "loop" | "sequential" > ( ( ) = > {
// 从 localStorage 读取循环模式,默认 loop
if ( typeof window === "undefined" ) return "loop" ;
const saved = localStorage . getItem ( "aweme_player_loop_mode" ) ;
if ( ! saved ) return "loop" ;
return saved === "sequential" ? "sequential" : "loop" ;
} ) ;
// ====== 图文专用(分段) ======
const images = ( data as any ) . images as ImageData [ "images" ] | undefined ;
@@ -161,6 +175,7 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
const idxRef = useRef < number > ( 0 ) ;
const rafRef = useRef < number | null > ( null ) ;
const [ segProgress , setSegProgress ] = useState ( 0 ) ; // 段内 0..1
const [ , forceUpdate ] = useState ( 0 ) ; // 用于强制更新时间显示
useEffect ( ( ) = > { idxRef . current = idx ; } , [ idx ] ) ;
@@ -182,6 +197,12 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
localStorage . setItem ( "aweme_player_comments_open" , open . toString ( ) ) ;
} , [ open ] ) ;
// ====== 持久化循环模式到 localStorage ======
useEffect ( ( ) = > {
if ( typeof window === "undefined" ) return ;
localStorage . setItem ( "aweme_player_loop_mode" , loopMode ) ;
} , [ loopMode ] ) ;
// ====== 恢复视频播放进度(带有效期) ======
useEffect ( ( ) = > {
if ( ! isVideo || progressRestored ) return ;
@@ -274,18 +295,26 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
} ;
const onPlay = ( ) = > setIsPlaying ( true ) ;
const onPause = ( ) = > setIsPlaying ( false ) ;
const onEnded = ( ) = > {
// 顺序播放模式下,视频结束时自动跳转到下一条
if ( loopMode === "sequential" && neighbors ? . next ) {
router . push ( ` /aweme/ ${ neighbors . next . aweme_id } ` ) ;
}
} ;
v . addEventListener ( "timeupdate" , onTime ) ;
v . addEventListener ( "loadedmetadata" , onTime ) ;
v . addEventListener ( "play" , onPlay ) ;
v . addEventListener ( "pause" , onPause ) ;
v . addEventListener ( "ended" , onEnded ) ;
return ( ) = > {
v . removeEventListener ( "timeupdate" , onTime ) ;
v . removeEventListener ( "loadedmetadata" , onTime ) ;
v . removeEventListener ( "play" , onPlay ) ;
v . removeEventListener ( "pause" , onPause ) ;
v . removeEventListener ( "ended" , onEnded ) ;
} ;
} , [ isVideo ] ) ;
} , [ isVideo , loopMode , neighbors ? . next ? . aweme_id , router ] ) ;
useEffect ( ( ) = > {
if ( ! isVideo ) return ;
@@ -299,6 +328,15 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
if ( v ) v . playbackRate = rate ;
} , [ rate , isVideo ] ) ;
// ====== 视频:定期更新时间显示 ======
useEffect ( ( ) = > {
if ( ! isVideo ) return ;
const interval = setInterval ( ( ) = > {
forceUpdate ( n = > n + 1 ) ;
} , 100 ) ; // 每 100ms 更新一次显示
return ( ) = > clearInterval ( interval ) ;
} , [ isVideo ] ) ;
// ====== 图文:BGM & 初次自动播放尝试 ======
useEffect ( ( ) = > {
if ( isVideo ) return ;
@@ -349,33 +387,38 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
} ;
} , [ router ] ) ;
// ====== 图文:自动切页(消除“闪回”) ======
// ====== 图文:自动切页 ======
useEffect ( ( ) = > {
if ( isVideo || ! images ? . length ) return ;
if ( segStartRef . current == null ) segStartRef . current = performance . now ( ) ;
let lastTs = performance . now ( ) ;
const tick = ( ts : number ) = > {
if ( ! images ? . length ) return ;
if ( ! isPlaying ) segStartRef . current ! += ts - lastTs ;
lastTs = ts
let start = segStartRef . current ! ;
let localIdx = idxRef . current ;
// 暂停时只更新 UI,不推进时间
if ( ! isPlaying ) {
const elapsed = Math . max ( 0 , ts - start ) ;
const localSeg = Math . min ( 1 , elapsed / SEGMENT_MS ) ;
setSegProgress ( localSeg ) ;
setProgress ( ( localIdx + localSeg ) / images . length ) ;
rafRef . current = requestAnimationFrame ( tick ) ;
return ;
}
// 前进时间:处理跨多段情况(极少见,但更稳妥)
let elapsed = ts - start ;
while ( elapsed >= SEGMENT_MS ) {
elapsed -= SEGMENT_MS ;
localIdx = ( localIdx + 1 ) % images . length ;
// 检查是否到达最后一张
if ( localIdx >= images . length - 1 ) {
// 顺序播放模式:跳转到下一条作品
if ( loopMode === "sequential" && neighbors ? . next ) {
router . push ( ` /aweme/ ${ neighbors . next . aweme_id } ` ) ;
return ; // 停止当前动画循环
}
// 循环播放模式:回到第一张
localIdx = 0 ;
} else {
localIdx = localIdx + 1 ;
}
}
segStartRef . current = ts - elapsed ;
@@ -398,25 +441,7 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
if ( rafRef . current ) cancelAnimationFrame ( rafRef . current ) ;
rafRef . current = null ;
} ;
} , [ isVideo , images ? . length , isPlaying ] ) ;
// 横向滚动同步 idx(且重置段起点)
useEffect ( ( ) = > {
const el = scrollerRef . current ;
if ( ! el ) return ;
const onScroll = ( ) = > {
const i = Math . round ( el . scrollLeft / el . clientWidth ) ;
if ( i !== idxRef . current ) {
idxRef . current = i ;
setIdx ( i ) ;
segStartRef . current = performance . now ( ) ;
setSegProgress ( 0 ) ;
setProgress ( images && images . length ? i / images.length : 0 ) ;
}
} ;
el . addEventListener ( "scroll" , onScroll , { passive : true } ) ;
return ( ) = > el . removeEventListener ( "scroll" , onScroll ) ;
} , [ images ? . length ] ) ;
} , [ isVideo , images ? . length , isPlaying , loopMode , neighbors ? . next ? . aweme_id , router ] ) ;
// ====== 统一操作 ======
const seekTo = ( ratio : number ) = > {
@@ -491,11 +516,42 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
if ( el ) el . scrollTo ( { left : next * el . clientWidth , behavior : "smooth" } ) ;
} ;
// ====== 下载功能 ======
const handleDownload = ( ) = > {
if ( isVideo ) {
// 下载视频
const videoUrl = ( data as VideoData ) . video_url ;
const link = document . createElement ( "a" ) ;
link . href = videoUrl ;
link . download = ` video_ ${ data . aweme_id } .mp4 ` ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
} else {
// 下载当前图片
if ( ! images ? . length ) return ;
const currentImage = images [ idx ] ;
const link = document . createElement ( "a" ) ;
link . href = currentImage . url ;
link . download = ` image_ ${ data . aweme_id } _ ${ idx + 1 } .jpg ` ;
document . body . appendChild ( link ) ;
link . click ( ) ;
document . body . removeChild ( link ) ;
}
} ;
// ====== 格式化时间显示 ======
const formatTime = ( seconds : number ) = > {
const mins = Math . floor ( seconds / 60 ) ;
const secs = Math . floor ( seconds % 60 ) ;
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:bottom- 0 portrait:w-full portrait:h-[min(80vh,88dvh)]" ,
"portrait:fixed portrait:inset-x-0 portrait:top-11 0 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" ,
@@ -544,7 +600,7 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
return ( ) = > el . removeEventListener ( "wheel" , onWheel as any ) ;
} , [ neighbors ? . next ? . aweme_id , neighbors ? . prev ? . aweme_id ] ) ;
// ====== 动态模糊背景:每 0.2s 截取当前媒体内容绘制到背景 canvas ======
// ====== 动态模糊背景:定时 截取当前媒体内容绘制到背景 canvas ======
useEffect ( ( ) = > {
const canvas = backgroundCanvasRef . current ;
if ( ! canvas ) return ;
@@ -580,15 +636,16 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
}
}
if ( ! sourceElement || ( sourceElement instanceof HTMLVideoElement && sourceElement . readyState < 2 ) ) {
return ;
}
// 不要在帧不可用时清空画布,避免出现黑屏闪烁
// 仅当有可绘制帧时才进行绘制,画面会被完整覆盖,无需提前 clear
if ( ! sourceElement || ( sourceElement instanceof HTMLVideoElement && sourceElement . readyState < 2 ) ) return ;
const canvasWidth = canvas . width ;
const canvasHeight = canvas . height ;
const sourceWidth = sourceElement instanceof HTMLVideoElement ? sourceElement.videoWidth : sourceElement.naturalWidth ;
const sourceHeight = sourceElement instanceof HTMLVideoElement ? sourceElement.videoHeight : sourceElement.naturalHeight ;
// 图片在切换时可能尚未完成解码(naturalWidth/Height 为 0),此时保持上一帧
if ( ! sourceWidth || ! sourceHeight ) return ;
// 计算 cover 模式的尺寸和位置
@@ -611,11 +668,11 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
offsetY = 0 ;
}
// 清空画布并绘制
ctx . clearRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
// 直接绘制一帧即可(cover 会完全覆盖整个画布,无需 clearRect)
ctx . drawImage ( sourceElement , offsetX , offsetY , drawWidth , drawHeight ) ;
} ;
// 使用较高频率的定时器以保持背景连贯(20ms ~= 50fps)
const intervalId = setInterval ( drawMediaToCanvas , 20 ) ;
return ( ) = > {
@@ -648,8 +705,8 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
// 旋转 0/180:充满容器盒子;
// 旋转 90/270:用中心定位 + 100vh/100vw,保证铺满全屏
rotation % 180 === 0
? ` absolute inset-0 h-full w-full object- ${ objectFit } bg-black/70 cursor-pointer `
: ` absolute top-1/2 left-1/2 h-[100vw] w-[100vh] object- ${ objectFit } bg-black/70 cursor-pointer ` ,
? ` absolute inset-0 h-full w-full bg-black/70 cursor-pointer `
: ` absolute top-1/2 left-1/2 h-[100vw] w-[100vh] bg-black/70 cursor-pointer ` ,
] . join ( " " ) }
style = { {
transform :
@@ -657,16 +714,18 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
? ` rotate( ${ rotation } deg) `
: ` translate(-50%, -50%) rotate( ${ rotation } deg) ` ,
transformOrigin : "center center" ,
objectFit ,
} }
playsInline
autoPlay
loop onClick = { togglePlay }
loop = { loopMode === "loop" }
onClick = { togglePlay }
/ >
) : (
< div className = "absolute inset-0" >
< div
ref = { scrollerRef }
className = "absolute inset-0 overflow-x-auto overflow-y-hidden snap-x snap-mandatory flex no-scrollbar"
className = "absolute inset-0 overflow-x-auto overflow-y-hidden snap-x snap-mandatory flex no-scrollbar"
>
{ ( data as ImageData ) . images . map ( ( img ) = > (
< div
@@ -675,7 +734,7 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
style = { { aspectRatio : img.width && img . height ? ` ${ img . width } / ${ img . height } ` : undefined } }
>
{ /* eslint-disable-next-line @next/next/no-img-element */ }
< img src = { img . url } alt = "image" className = { ` absolute inset-0 w-full h-full object- ${ objectFit } cursor-pointer` } onClick = { togglePlay } draggable = { false } / >
< img src = { img . url } alt = "image" className = { ` absolute inset-0 w-full h-full cursor-pointer ` } style = { { objectFit } } onClick = { togglePlay } draggable = { false } / >
< / div >
) ) }
< / div >
@@ -684,14 +743,14 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
{ images && images . length > 1 ? (
< >
< button
className = "absolute top-1/2 -translate-y-1/2 left-3 w-[42px] h-[42px] grid place-items-center rounded-full bg-black/15 text-white border border-white/20 drop-shadow-lg cursor-pointer "
className = "absolute top-5/11 left-3 -translate-y-1/2 w-12 h-12 flex items-center justify-center text-white hover:bg-white/10 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed border-b border-white/20 bg-black/40 backdrop-blur-sm border overflow-hidden rounded-full "
onClick = { prevImg }
aria-label = "上一张"
>
< ChevronLeft / >
< / button >
< button
className = "absolute top-1/2 -translate-y-1/2 right-3 w-[42px] h-[42px] grid place-items-center rounded-full bg-black/15 text-white border border-white/20 drop-shadow-lg cursor-pointer "
className = "absolute top-5/11 right-3 -translate-y-1/2 w-12 h-12 flex items-center justify-center text-white hover:bg-white/10 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed border-b border-white/20 bg-black/40 backdrop-blur-sm border overflow-hidden rounded-full "
onClick = { nextImg }
aria-label = "下一张"
>
@@ -752,7 +811,7 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / div >
) : (
< div
className = "relative h-1 rounded-full bg-white/25 overflow-hidden cursor-pointer"
className = "relative h-1.5 rounded-full bg-white/25 overflow-hidden cursor-pointer"
onClick = { ( e ) = > {
const rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
seekTo ( ( e . clientX - rect . left ) / rect . width ) ;
@@ -769,18 +828,19 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< div className = "flex items-center justify-between gap-2.5" >
< div className = "inline-flex items-center gap-2" >
< 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"
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 = { 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"
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 ) ;
@@ -792,14 +852,29 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
{ rate } x
< / button >
{ /* 旋转:向左/向右各 90° */ }
< / >
) : null }
{ /* 播放进度显示 */ }
< div className = "text-[13px] text-white/90 font-mono min-w-[80px] ml-2" >
{ isVideo ? (
( ( ) = > {
const v = videoRef . current ;
const current = v ? . currentTime ? ? 0 ;
const total = v ? . duration ? ? 0 ;
return total > 0 ? ` ${ formatTime ( current ) } / ${ formatTime ( total ) } ` : '--:-- / --:--' ;
} ) ( )
) : (
` ${ idx + 1 } / ${ totalSegments } `
) }
< / div >
< / div >
< div className = "inline-flex items-center gap-2" >
< 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"
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 = { ( ) = > setRotation ( ( r ) = > ( r + 270 ) % 360 ) }
aria-label = "向左旋转 90 度"
title = "向左旋转 90 度"
@@ -807,7 +882,7 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< RotateCcw 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"
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 = { ( ) = > setVolume ( ( v ) = > ( v > 0 ? 0 : 1 ) ) }
aria-label = { volume > 0 ? "静音" : "取消静音" }
>
@@ -820,11 +895,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"
className = "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"
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 = { ( ) = > setRotation ( ( r ) = > ( r + 90 ) % 360 ) }
aria-label = "向右旋转 90 度"
title = "向右旋转 90 度"
@@ -834,8 +909,17 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / div >
< div className = "inline-flex items-center gap-2" >
{ /* 循环模式切换 */ }
< 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"
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 = { ( ) = > 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"
onClick = { ( ) = > setObjectFit ( ( f ) = > ( f === "contain" ? "cover" : "contain" ) ) }
aria-label = { objectFit === "contain" ? "切换到填充模式" : "切换到适应模式" }
title = { objectFit === "contain" ? "切换到填充模式" : "切换到适应模式" }
@@ -843,7 +927,15 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
{ 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"
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 = { 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 }
aria-label = "切换全屏"
>
@@ -876,20 +968,46 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / span >
< / div >
< / button >
{ /* 上下切换视频按钮(右侧胶囊形状) */ }
< div className = "absolute right-4 top-6/11 -translate-y-1/2 z-10" >
< div className = "flex flex-col rounded-full bg-black/40 backdrop-blur-sm border border-white/20 overflow-hidden" >
< button
className = "w-10 h-12 flex items-center justify-center text-white hover:bg-white/10 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed border-b border-white/20"
onClick = { ( ) = > neighbors ? . prev && router . push ( ` /aweme/ ${ neighbors . prev . aweme_id } ` ) }
disabled = { ! neighbors ? . prev }
aria-label = "上一个视频"
title = "上一个视频"
>
< ChevronUp size = { 20 } / >
< / button >
< button
className = "w-10 h-12 flex items-center justify-center text-white hover:bg-white/10 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
onClick = { ( ) = > neighbors ? . next && router . push ( ` /aweme/ ${ neighbors . next . aweme_id } ` ) }
disabled = { ! neighbors ? . next }
aria-label = "下一个视频"
title = "下一个视频"
>
< ChevronDown size = { 20 } / >
< / button >
< / div >
< / div >
< / section >
{ /* 评论面板:竖屏 bottom sheet;横屏并排分栏 */ }
< aside className = { asideClasses } >
< div className = "flex items-center justify-between px-3 py-3 border-b border-white/10" >
< button
className = "text-white/90 text-xs px-2 py-1 rounded-lg bg-white/15 border border-white/20"
onClick = { ( ) = > setOpen ( false ) }
>
关 闭
< / button >
< div className = "text-white font-semibold" >
{ /* 竖屏:评论在左,关闭按钮在右 */ }
< div className = "text-white font-semibold portrait:order-1 landscape:order-2" >
评 论 { 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"
onClick = { ( ) = > setOpen ( false ) }
aria-label = "关闭评论"
>
< X size = { 18 } / >
< / button >
< / div >
< div className = "p-3 overflow-auto" >