feat: 移动端沉浸式 Shorts 与播放器/下载徽章体验优化

- 移动端移除底部导航,Shorts 视频改为圆角卡片并适配 iOS 裁剪
- 播放进度条全局贴到视频最底,移动端精简控件(仅播放/全屏)
- 右侧互动栏下移,首页进入播放页增加模糊封面过渡
- 关于页下载入口改为标准 App Store / Google Play 徽章
- 新增自动连播下一集与自动播放被拦截时的开启声音提示
This commit is contained in:
Zhou Weidong committed 2026-11-12 15:14:09 +08:00
1 parent 6fd936ac97
commit 2d1f754a46
11 files changed
+267 -54

No files matched your search

+23 -2
View File
@@ -45,7 +45,7 @@ function formatTime(seconds) {
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
}
export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProgress }) {
export function VideoPlayer({ src, poster, autoPlay = true, muted = true, soundHint = 'Tap for sound', onProgress, onEnded }) {
const wrapRef = useRef(null);
const videoRef = useRef(null);
const hideTimerRef = useRef(null);
@@ -58,6 +58,7 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
const [reloadKey, setReloadKey] = useState(0);
const [playing, setPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(() => (soundEnabled ? false : muted));
const [soundBlocked, setSoundBlocked] = useState(false);
const [volume, setVolume] = useState(1);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
@@ -104,6 +105,7 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
setReady(false);
setError('');
setPlaying(false);
setSoundBlocked(false);
setCurrentTime(0);
setBuffered(0);
const hls = new Hls({ enableWorker: true });
@@ -157,10 +159,11 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
setIsMuted(!wantSound);
video.play().catch((playError) => {
if (playError.name === 'AbortError') return;
// 浏览器拦截带声音的自动播放时,降级为静音播放
// 浏览器拦截带声音的自动播放时,降级为静音播放并提示用户手动开启
if (playError.name === 'NotAllowedError' && wantSound) {
video.muted = true;
setIsMuted(true);
setSoundBlocked(true);
video.play().catch(() => {});
return;
}
@@ -218,6 +221,16 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
seekToRatio((event.clientX - rect.left) / rect.width);
};
const enableSound = () => {
const video = videoRef.current;
if (!video) return;
video.muted = false;
setIsMuted(false);
setSoundBlocked(false);
persistSoundPreference(true);
video.play().catch(() => {});
};
const toggleMute = () => {
const video = videoRef.current;
if (!video) return;
@@ -294,6 +307,7 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
onPause={() => { setPlaying(false); setControlsVisible(true); }}
onWaiting={() => setReady(false)}
onPlaying={() => setReady(true)}
onEnded={onEnded}
onLoadedMetadata={(event) => setDuration(event.currentTarget.duration || 0)}
onTimeUpdate={(event) => {
const video = event.currentTarget;
@@ -307,6 +321,13 @@ export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProg
{!ready && !error && <span className="vp-spinner" aria-hidden="true" />}
{soundBlocked && !error && (
<button className="vp-sound-hint" type="button" onClick={enableSound}>
<VolumeX size={15} />
<span>{soundHint}</span>
</button>
)}
{error && (
<button
className="vp-error"