@ -29,6 +29,26 @@ import {
type User = { nickname : string ; avatar_url : string | null } ;
type User = { nickname : string ; avatar_url : string | null } ;
type Comment = { cid : string ; text : string ; digg_count : number ; created_at : string | Date ; user : User } ;
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 } ) [ ] {
function parseCommentText ( text : string ) : ( string | { type : "emoji" ; name : string } ) [ ] {
const parts : ( 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 router = useRouter ( ) ;
// ====== 布局 & 评论 ======
// ====== 布局 & 评论 ======
const [ open , setOpen ] = useState ( ( ) = > {
const [ open , setOpen ] = useState ( false ) ; // 评论是否展开(竖屏为 bottom sheet; 横屏为并排分栏)
// 从 localStorage 读取评论区状态,默认 false
const [ mounted , setMounted ] = useState ( 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 comments = useMemo ( ( ) = > data . comments ? ? [ ] , [ data ] ) ;
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 mediaContainerRef = useRef < HTMLDivElement | null > ( null ) ;
const videoRef = useRef < HTMLVideoElement | 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 ] ) ;
} , [ 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 ( ( ) = > {
useEffect ( ( ) = > {
if ( ! isVideo ) return ;
if ( ! isVideo ) return ;
const v = videoRef . current ;
const v = videoRef . current ;
@ -488,7 +545,20 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
const toggleFullscreen = ( ) = > {
const toggleFullscreen = ( ) = > {
if ( ! document . fullscreenElement ) {
if ( ! document . fullscreenElement ) {
document . body . requestFullscreen ( ) . catch ( ( ) = > { } ) ;
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 {
} else {
document . exitFullscreen ( ) . catch ( ( ) = > { } ) ;
document . exitFullscreen ( ) . catch ( ( ) = > { } ) ;
}
}
@ -547,21 +617,54 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
return ` ${ mins } : ${ secs . toString ( ) . padStart ( 2 , '0' ) } ` ;
return ` ${ mins } : ${ secs . toString ( ) . padStart ( 2 , '0' ) } ` ;
} ;
} ;
// ====== 侧栏(横屏)/ 抽屉( 竖屏) 样式( Tailwind)
// ====== 评论内容组件 - 使用 useMemo 避免不必要的重新渲染 ======
const asideClasses = [
const commentContent = useMemo ( ( ) = > (
"z-30 flex flex-col bg-[rgba(22,22,22,0.92)] text-white" ,
< >
// 竖屏: bottom sheet, 从下向上弹出
< header className = "flex items-center gap-4 mb-5" >
"portrait:fixed portrait:inset-x-0 portrait:top-110 portrait:w-full portrait:h-[min(80vh,88dvh)]" ,
< div className = "size-10 rounded-full overflow-hidden bg-zinc-700/60" >
"portrait:transition-transform portrait:duration-200 portrait:ease-out" ,
{ data . author . avatar_url ? (
open ? "portrait:translate-y-0" : "portrait:translate-y-full" ,
// eslint-disable-next-line @next/next/no-img-element
"portrait:border-t portrait:border-white/10" ,
< img src = { data . author . avatar_url } alt = "avatar" className = "w-full h-full object-cover" / >
// 横屏:并排分栏,宽度过渡
) : null }
"landscape:relative landscape:h-full landscape:overflow-hidden" ,
< / div >
"landscape:transition-[width] landscape:duration-200 landscape:ease-out" ,
< div >
open
< div className = "font-medium text-white/95 text-sm sm:text-base" > { data . author . nickname } < / div >
? "landscape:w-[min(420px,36vw)] landscape:border-l landscape:border-white/10"
< div className = "text-xs text-white/50" title = { new Date ( data . created_at ) . toLocaleString ( ) } >
: "landscape:w-0" ,
发 布 于 { formatRelativeTime ( data . created_at ) }
] . join ( " " ) ;
< / 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 ( ( ) = > {
useEffect ( ( ) = > {
@ -600,6 +703,72 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
return ( ) = > el . removeEventListener ( "wheel" , onWheel as any ) ;
return ( ) = > el . removeEventListener ( "wheel" , onWheel as any ) ;
} , [ neighbors ? . next ? . aweme_id , neighbors ? . prev ? . aweme_id ] ) ;
} , [ 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 ======
// ====== 动态模糊背景:定时截取当前媒体内容绘制到背景 canvas ======
useEffect ( ( ) = > {
useEffect ( ( ) = > {
const canvas = backgroundCanvasRef . current ;
const canvas = backgroundCanvasRef . current ;
@ -610,11 +779,18 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
// 更新 canvas 尺寸以匹配视口
// 更新 canvas 尺寸以匹配视口
const updateCanvasSize = ( ) = > {
const updateCanvasSize = ( ) = > {
canvas . width = window. innerWidth ;
canvas . width = Math. floor ( window. innerWidth / 10 ) ;
canvas . height = window. innerHeight ;
canvas . height = Math. floor ( window. innerHeight / 10 ) ;
} ;
} ;
updateCanvasSize ( ) ;
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 策略)
// 绘制媒体内容到 canvas( cover 策略)
const drawMediaToCanvas = ( ) = > {
const drawMediaToCanvas = ( ) = > {
@ -672,12 +848,12 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
ctx . drawImage ( sourceElement , offsetX , offsetY , drawWidth , drawHeight ) ;
ctx . drawImage ( sourceElement , offsetX , offsetY , drawWidth , drawHeight ) ;
} ;
} ;
// 使用较高频率的定时器以保持背景连贯( 20ms ~= 50fps)
const intervalId = setInterval ( drawMediaToCanvas , 20 ) ;
const intervalId = setInterval ( drawMediaToCanvas , 20 ) ;
return ( ) = > {
return ( ) = > {
clearInterval ( intervalId ) ;
clearInterval ( intervalId ) ;
window . removeEventListener ( "resize" , updateCanvasSize ) ;
window . removeEventListener ( "resize" , debouncedResize ) ;
clearTimeout ( resizeTimeout ) ;
} ;
} ;
} , [ isVideo , idx ] ) ;
} , [ isVideo , idx ] ) ;
@ -717,7 +893,6 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
objectFit ,
objectFit ,
} }
} }
playsInline
playsInline
autoPlay
loop = { loopMode === "loop" }
loop = { loopMode === "loop" }
onClick = { togglePlay }
onClick = { togglePlay }
/ >
/ >
@ -761,15 +936,30 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / div >
< / 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 在上、进度在下 */ }
{ /* 统一控制条: 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 = "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" >
< 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" / >
< 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-[1 5px] leading-tight text-white/95 drop-shadow">
{ data . author . nickname }
{ data . author . nickname }
< / span >
< / 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 >
< / div >
{ data . desc ? (
{ data . desc ? (
@ -824,41 +1014,20 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / div >
< / div >
) }
) }
{ /* 控制按钮行 */ }
{ /* 控制按钮行 - 响应式布局 */ }
< div className = "flex items-center justify-between gap-2.5" >
< div className = "flex items-center justify-between gap-1.5 sm:gap-2.5" >
< div className = "inline-flex items-center gap-2" >
{ /* 左侧:播放控制 + 时间/进度 */ }
< div className = "inline-flex items-center gap-1.5 sm:gap-2 min-w-0" >
< 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 = "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 }
onClick = { togglePlay }
aria - label = { isPlaying ? "暂停" : "播放" }
aria - label = { isPlaying ? "暂停" : "播放" }
>
>
{ isPlaying ? < Pause size = { 18 } / > : < Play size = { 18 } / > }
{ isPlaying ? < Pause size = { 18 } / > : < Play size = { 18 } / > }
< / button >
< / button >
{ /* 播放进度显示 - 所有设备都显示 */ }
{ /* 倍速仅视频展示 */ }
< div className = "text-[13px] text-white/90 font-mono min-w-[70px] sm:min-w-[80px]" >
{ 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" >
{ isVideo ? (
{ isVideo ? (
( ( ) = > {
( ( ) = > {
const v = videoRef . current ;
const v = videoRef . current ;
@ -870,11 +1039,29 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
` ${ idx + 1 } / ${ totalSegments } `
` ${ idx + 1 } / ${ totalSegments } `
) }
) }
< / div >
< / 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 >
< div className = "inline-flex items-center gap-2" >
{ /* 中间:音量控制 - 中等屏幕以上显示 */ }
< div className = "hidden md:inline-flex items-center gap-2 shrink-0" >
{ /* 旋转按钮 - 小屏幕以上显示 */ }
< 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 = { ( ) = > setRotation ( ( r ) = > ( r + 270 ) % 360 ) }
onClick = { ( ) = > setRotation ( ( r ) = > ( r + 270 ) % 360 ) }
aria - label = "向左旋转 90 度"
aria - label = "向左旋转 90 度"
title = "向左旋转 90 度"
title = "向左旋转 90 度"
@ -895,11 +1082,11 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
step = { 0.05 }
step = { 0.05 }
value = { volume }
value = { volume }
onChange = { ( e ) = > setVolume ( parseFloat ( e . target . value ) ) }
onChange = { ( e ) = > setVolume ( parseFloat ( e . target . value ) ) }
className = "w-2 8 accent-white cursor-pointer"
className = "w-2 0 lg:w-2 8 accent-white cursor-pointer"
aria - label = "音量"
aria - label = "音量"
/ >
/ >
< 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 = { ( ) = > setRotation ( ( r ) = > ( r + 90 ) % 360 ) }
onClick = { ( ) = > setRotation ( ( r ) = > ( r + 90 ) % 360 ) }
aria - label = "向右旋转 90 度"
aria - label = "向右旋转 90 度"
title = "向右旋转 90 度"
title = "向右旋转 90 度"
@ -908,32 +1095,49 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / button >
< / button >
< / div >
< / 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
< 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" ) ) }
onClick = { ( ) = > setLoopMode ( ( m ) = > ( m === "loop" ? "sequential" : "loop" ) ) }
aria - label = { loopMode === "loop" ? "循环播放" : "顺序播放" }
aria - label = { loopMode === "loop" ? "循环播放" : "顺序播放" }
title = { loopMode === "loop" ? "循环播放" : "顺序播放" }
title = { loopMode === "loop" ? "循环播放" : "顺序播放" }
>
>
{ loopMode === "loop" ? < Repeat1 size = { 18 } / > : < ArrowDownUp size = { 18 } / > }
{ loopMode === "loop" ? < Repeat1 size = { 18 } / > : < ArrowDownUp size = { 18 } / > }
< / button >
< / button >
{ /* 适配模式 - 小屏幕以上显示 */ }
< 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" ) ) }
onClick = { ( ) = > setObjectFit ( ( f ) = > ( f === "contain" ? "cover" : "contain" ) ) }
aria - label = { objectFit === "contain" ? "切换到填充模式" : "切换到适应模式" }
aria - label = { objectFit === "contain" ? "切换到填充模式" : "切换到适应模式" }
title = { objectFit === "contain" ? "切换到填充模式" : "切换到适应模式" }
title = { objectFit === "contain" ? "切换到填充模式" : "切换到适应模式" }
>
>
{ objectFit === "contain" ? < Maximize2 size = { 18 } / > : < Minimize size = { 18 } / > }
{ objectFit === "contain" ? < Maximize2 size = { 18 } / > : < Minimize size = { 18 } / > }
< / button >
< / button >
{ /* 下载 - 中等屏幕以上显示 */ }
< 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 }
onClick = { handleDownload }
aria - label = { isVideo ? "下载视频" : "下载当前图片" }
aria - label = { isVideo ? "下载视频" : "下载当前图片" }
title = { isVideo ? "下载视频" : "下载当前图片" }
title = { isVideo ? "下载视频" : "下载当前图片" }
>
>
< Download size = { 18 } / >
< Download size = { 18 } / >
< / button >
< / button >
{ /* 全屏 - 所有设备都显示 */ }
< 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 = "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 }
onClick = { toggleFullscreen }
@ -994,65 +1198,63 @@ export default function AwemeDetailClient(props: { data: VideoData | ImageData;
< / div >
< / div >
< / section >
< / 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 = "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" >
评 论 { comments . length > 0 ? ` ( ${ comments . length } ) ` : "" }
< / div >
< button
< 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 ) }
onClick = { ( ) = > setOpen ( false ) }
aria - label = "关闭评论"
aria - label = "关闭评论"
>
>
< X size = { 18 } / >
< X size = { 18 } / >
< / button >
< / button >
< div className = "text-white font-semibold" >
评 论 { comments . length > 0 ? ` ( ${ comments . length } ) ` : "" }
< / div >
< / div >
< / div >
< div className = "p-3 overflow-auto" >
< div className = "p-3 overflow-auto" >
< header className = "flex items-center gap-4 mb-5" >
{ commentContent }
< 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 >
< / div >
< / div >
< / aside >
< / aside >
< / div >
< / 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 ( 80 vh , 88 dvh ) ]
$ { 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"
onClick = { ( ) = > setOpen ( false ) }
aria - label = "关闭评论"
>
< X size = { 18 } / >
< / button >
< / div >
< div className = "p-3 overflow-auto" >
{ commentContent }
< / div >
< / aside >
< / div >
< / div >
) ;
) ;
}
}