🐛 修复视频播放器无限循环渲染问题

## 主要修复
- 修复 FullscreenMediaModal 和 FullscreenVideoModal 中的 useEffect 循环依赖
- 重构 VideoPlayer 组件的视频属性管理逻辑
- 优化 useVideoPlayer 的初始化回调机制

## 新增功能
- 新增标签 API 支持 (lib/api/tags.ts)
- 新增内容骨架屏组件 (components/profile/content-skeleton.tsx)
- 新增返回按钮组件 (components/ui/back-button.tsx)

## 改进优化
- 优化视频播放器的性能,避免重复初始化
- 修复 useEffect 依赖项导致的无限循环更新
- 完善类型定义和 API 接口

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2025-11-10 11:58:56 +08:00
parent af64049c69
commit eee280d9b3
26 changed files with 1425 additions and 885 deletions

View File

@@ -1,6 +1,6 @@
import { ThemedText } from '@/components/themed-text';
import { type VideoReadyForDisplayEvent, ResizeMode, Video } from 'expo-av';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { VideoView, useVideoPlayer } from 'expo-video';
import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
Dimensions,
@@ -43,7 +43,19 @@ export function FullscreenVideoModal({
hasNext = false,
hasPrevious = false,
}: FullscreenVideoModalProps) {
const videoRef = useRef<Video>(null);
const player = useVideoPlayer(
{
uri: videoUrl,
},
(player) => {
player.loop = isLooping;
player.muted = isMuted;
if (autoPlay) {
player.play();
}
}
);
const [isLoading, setIsLoading] = useState(true);
// 重置状态当模态框打开/关闭时
@@ -51,18 +63,17 @@ export function FullscreenVideoModal({
if (visible) {
setIsLoading(true);
} else {
// 停止播放当关闭时
if (videoRef.current) {
videoRef.current.stopAsync();
if (player) {
player.pause();
}
}
}, [visible, autoPlay]);
// 处理视频加载完成
const handleVideoReady = (_event: VideoReadyForDisplayEvent) => {
const handleVideoReady = () => {
setIsLoading(false);
if (autoPlay) {
videoRef.current?.playAsync();
player?.play();
}
};
@@ -82,20 +93,16 @@ export function FullscreenVideoModal({
};
// 创建手势处理器
const panResponder = useRef(
const panResponder = React.useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (_, gestureState) => {
// 只有水平移动时才响应
return Math.abs(gestureState.dx) > Math.abs(gestureState.dy) && Math.abs(gestureState.dx) > 10;
},
onPanResponderRelease: (_, gestureState) => {
const threshold = screenWidth / 4; // 滑动屏幕宽度的1/4触发切换
const threshold = screenWidth / 4;
if (gestureState.dx > threshold && hasPrevious) {
// 向右滑动,显示上一个
onPrevious?.();
} else if (gestureState.dx < -threshold && hasNext) {
// 向左滑动,显示下一个
onNext?.();
}
},
@@ -106,7 +113,7 @@ export function FullscreenVideoModal({
useEffect(() => {
const handleBackPress = () => {
if (visible) {
handleClose();
onClose();
return true;
}
return false;
@@ -114,7 +121,7 @@ export function FullscreenVideoModal({
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => subscription.remove();
}, [visible, handleClose]);
}, [visible, onClose]);
if (!visible) return null;
@@ -127,10 +134,8 @@ export function FullscreenVideoModal({
statusBarTranslucent={true}
>
<View style={styles.container}>
{/* 状态栏处理 */}
{Platform.OS === 'android' && <StatusBar hidden />}
{/* 视频容器 */}
<TouchableOpacity
style={styles.videoContainer}
onPress={handleVideoPress}
@@ -140,67 +145,58 @@ export function FullscreenVideoModal({
style={styles.videoWrapper}
{...panResponder.panHandlers}
>
{/* 视频播放器 */}
{Platform.OS === 'web' ? (
<video
ref={videoRef as any}
src={videoUrl}
style={styles.video}
autoPlay={autoPlay}
loop={isLooping}
muted={isMuted}
playsInline
onLoadedData={() => setIsLoading(false)}
onError={handleVideoError}
/>
) : (
<Video
ref={videoRef}
source={{ uri: videoUrl }}
style={styles.video}
resizeMode={ResizeMode.CONTAIN}
shouldPlay={autoPlay}
isLooping={isLooping}
isMuted={isMuted}
useNativeControls={false}
onReadyForDisplay={handleVideoReady}
onError={handleVideoError}
/>
)}
{Platform.OS === 'web' ? (
<video
src={videoUrl}
style={styles.video}
autoPlay={autoPlay}
loop={isLooping}
muted={isMuted}
playsInline
onLoadedData={() => setIsLoading(false)}
onError={handleVideoError}
/>
) : (
<VideoView
player={player}
style={styles.video}
contentFit="contain" as any
nativeControls={false}
onError={handleVideoError}
/>
)}
{/* 加载指示器 */}
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#4ECDC4" />
</View>
)}
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#4ECDC4" />
</View>
)}
{/* 左右导航指示器 */}
{hasPrevious && (
<TouchableOpacity
style={styles.leftIndicator}
onPress={(e) => {
e.stopPropagation();
onPrevious?.();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
{hasNext && (
<TouchableOpacity
style={styles.rightIndicator}
onPress={(e) => {
e.stopPropagation();
onNext?.();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
</View>
{hasPrevious && (
<TouchableOpacity
style={styles.leftIndicator}
onPress={(e) => {
e.stopPropagation();
onPrevious?.();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
{hasNext && (
<TouchableOpacity
style={styles.rightIndicator}
onPress={(e) => {
e.stopPropagation();
onNext?.();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
</View>
</TouchableOpacity>
</View>
</Modal>
@@ -228,7 +224,6 @@ const styles = StyleSheet.create({
width: screenWidth,
height: screenHeight,
backgroundColor: '#000',
objectFit: 'cover'
},
loadingOverlay: {
position: 'absolute',