+
{/* 当前条目的版本切换:多版本时置顶,随时可换 */}
{versionsToSwitch.length > 0 && (
)}
- {/* 集数宫格 */}
+ {/* 集数宫格:剧场模式下底部面板只做单层滚动,宫格区不再自己滚动 */}
{hasEpisodeList ? (
-
+
{filteredEpisodes.length === 0 ? (
{filterText ? '未找到匹配的剧集' : '暂无剧集列表'}
@@ -326,11 +338,12 @@ export function PlayerPlaylistPanel({
) : (
versionsToSwitch.length === 0 && (
-
)
}
diff --git a/web/src/components/playerTheme.ts b/web/src/components/playerTheme.ts
index 2c9392b..ad894e2 100644
--- a/web/src/components/playerTheme.ts
+++ b/web/src/components/playerTheme.ts
@@ -28,6 +28,22 @@ export const PLAYER_POPOVER =
export const PLAYER_DRAWER =
`flex h-full w-[88%] flex-col border-white/10 ${PANEL_BG} text-white ${PANEL_SHADOW} backdrop-blur-xl sm:w-[380px] sm:border-l`
+/**
+ * 底部动作面板(竖屏剧场模式):贴着视频区底部向上弹出,内容最多占
+ * 视频区高度的 72%,留出一些画面可见;底部垫上安全区高度,避免
+ * iPhone 小横条遮挡最后一行选项。
+ */
+export const PLAYER_SHEET =
+ `z-40 flex max-h-[72%] min-h-[8rem] w-full flex-col overflow-hidden rounded-t-2xl border border-white/10 border-b-0 ${PANEL_BG} text-white ${PANEL_SHADOW} backdrop-blur-xl`
+
+/** 底部动作面板的标题栏(带关闭按钮)。 */
+export const PLAYER_SHEET_HEADER =
+ 'flex shrink-0 items-center justify-between gap-2 border-b border-white/10 px-4 py-2.5'
+
+/** 底部动作面板的可滚动内容区,底部垫上安全区高度。 */
+export const PLAYER_SHEET_BODY =
+ 'min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))] pt-1'
+
/** 操作栏里的图标按钮。所有图标按钮共用同一尺寸与悬停反馈,排在一起才整齐。 */
export const PLAYER_ICON_BUTTON =
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-white/85 transition hover:bg-white/15 hover:text-white disabled:cursor-not-allowed disabled:text-white/30 disabled:hover:bg-transparent sm:h-8 sm:w-8'
diff --git a/web/src/hooks/useIsMobileTheater.ts b/web/src/hooks/useIsMobileTheater.ts
new file mode 100644
index 0000000..39a7ea1
--- /dev/null
+++ b/web/src/hooks/useIsMobileTheater.ts
@@ -0,0 +1,32 @@
+import { useEffect, useState } from 'react'
+
+const THEATER_MAX_WIDTH = 768
+
+function queryTheater(): boolean {
+ if (typeof window === 'undefined') return false
+ // 窄屏 + 竖屏才进剧场模式:横屏手机自动回到全屏沉浸式布局。
+ return window.innerWidth < THEATER_MAX_WIDTH && window.innerHeight >= window.innerWidth
+}
+
+/**
+ * 是否使用移动端竖屏剧场布局。
+ *
+ * 桌面端与横屏手机保持原来的全屏居中播放器;只有竖屏手机进
+ * 「视频贴顶 + 下方内容区」的剧场模式,避免 16:9 视频在竖屏里
+ * 上下大黑边、控制栏远离画面的问题。
+ */
+export function useIsMobileTheater(): boolean {
+ const [theater, setTheater] = useState(queryTheater)
+
+ useEffect(() => {
+ const onChange = () => setTheater(queryTheater())
+ window.addEventListener('resize', onChange)
+ window.addEventListener('orientationchange', onChange)
+ return () => {
+ window.removeEventListener('resize', onChange)
+ window.removeEventListener('orientationchange', onChange)
+ }
+ }, [])
+
+ return theater
+}
diff --git a/web/src/pages/PlayerPage.tsx b/web/src/pages/PlayerPage.tsx
index 87553c7..ff5b763 100644
--- a/web/src/pages/PlayerPage.tsx
+++ b/web/src/pages/PlayerPage.tsx
@@ -39,6 +39,8 @@ import { classifyDirectPlayError } from './directPlayError'
import { apiErrorMessage } from './StrmManagePage'
import { PlayerTopBar } from './PlayerTopBar'
import { PlayerVideoStage } from './PlayerVideoStage'
+import { PlayerMobileTheaterInfo } from '../components/PlayerMobileTheaterInfo'
+import { useIsMobileTheater } from '../hooks/useIsMobileTheater'
import { PlayerDanmakuPanel } from '../components/PlayerDanmakuPanel'
import { PlayerPlaylistPanel } from '../components/PlayerPlaylistPanel'
import {
@@ -1655,6 +1657,10 @@ export function PlayerPage() {
media?.title?.trim() ||
''
+ // 竖屏手机走剧场布局:视频贴顶按 16:9 自适应,下方是标题/选集/简介。
+ // 桌面端与横屏手机保持原来的全屏居中沉浸式布局。
+ const isMobileTheater = useIsMobileTheater()
+
return (
setPlaylistOpen(false)}
currentMediaId={media?.id ?? ''}
@@ -1741,6 +1749,7 @@ export function PlayerPage() {
}
danmakuPanel={
setDanmakuOpen(false)}
enabled={danmakuEnabled}
@@ -1775,6 +1784,22 @@ export function PlayerPage() {
/>
}
/>
+ {isMobileTheater ? (
+
+ ) : null}
)
}
diff --git a/web/src/pages/PlayerVideoStage.tsx b/web/src/pages/PlayerVideoStage.tsx
index 30fe43c..5b01591 100644
--- a/web/src/pages/PlayerVideoStage.tsx
+++ b/web/src/pages/PlayerVideoStage.tsx
@@ -156,6 +156,11 @@ function buildSubtitleRenderGroups(
type PlayerVideoStageProps = {
media: Media | null
+ /**
+ * 竖屏剧场模式:舞台不再全屏居中,而是贴顶按 16:9 自适应高度;
+ * 弹层改为底部动作面板,避免右侧抽屉与悬浮小弹层在窄屏下被裁剪。
+ */
+ theater?: boolean
/** 媒体元数据加载失败提示(非空时替代「加载中」展示)。 */
loadError?: string
playerError: string
@@ -234,6 +239,7 @@ type PlayerVideoStageProps = {
export function PlayerVideoStage({
media,
+ theater = false,
loadError,
playerError,
subs,
@@ -725,12 +731,22 @@ export function PlayerVideoStage({
const assTrack = subtitleIndex >= 0 ? subs[subtitleIndex] : undefined
// VR 模式下画面由 WebGL 画布输出,舞台窗口本身就是「镜头」,
// 不再按视频宽高比留黑边。
+ //
+ // 竖屏剧场模式(theater):舞台不再全屏居中,视频贴顶按容器宽度
+ // 16:9 自适应高度,下方留给标题/选集/简介内容区。stage 本体的
+ // 高度只包住视频区,控制栏与弹层都相对这块区域定位。
const wrapperStyle = vr360
? {
width: '100%',
height: '100%',
}
- : videoRatio
+ : theater
+ ? {
+ aspectRatio: '16 / 9',
+ width: '100%',
+ maxHeight: '100%',
+ }
+ : videoRatio
? {
aspectRatio: `${videoRatio}`,
width: isWiderThanStage ? '100%' : 'auto',
@@ -747,7 +763,11 @@ export function PlayerVideoStage({
setLockHovered(false)}
@@ -851,6 +871,7 @@ export function PlayerVideoStage({
) : null}