feat: 视频推荐关联当前视频、UP主信息展示、直播全屏修复及UI优化

- 推荐列表改用 getVideoRelated 基于当前 bvid 推荐,与首页 feed 解耦
- 视频详情页 UP主名称下方展示粉丝数和视频数
- 推荐视频点击改为 router.replace 避免页面堆叠
- 直播全屏改用 position:absolute,解决退出全屏视频重建暂停问题
- 退出全屏时直播自动暂停
- 直播画质选中状态修复,过滤杜比/4K 画质选项
- 直播画质面板改为居中 Modal 弹出框
- 视频详情 Tab 按钮左对齐,评论排序及设置页按钮统一实心背景风格
This commit is contained in:
Developer
2026-03-25 12:03:28 +08:00
parent 7870c83b12
commit e1fa8cc347
8 changed files with 340 additions and 227 deletions

View File

@@ -140,16 +140,14 @@ const styles = StyleSheet.create({
sectionLabel: { fontSize: 13, marginBottom: 10 },
optionRow: { flexDirection: 'row', gap: 10 },
option: {
paddingHorizontal: 20,
paddingVertical: 6,
borderRadius: 20,
borderWidth: 1,
borderColor: '#e0e0e0',
backgroundColor: 'transparent',
paddingHorizontal: 10,
paddingVertical: 2,
borderRadius: 16,
backgroundColor: '#f0f0f0',
},
optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' },
optionText: { fontSize: 14, color: '#666' },
optionTextActive: { color: '#00AEEC', fontWeight: '600' },
optionActive: { backgroundColor: '#00AEEC' },
optionText: { fontSize: 13, color: '#333', fontWeight: '500' },
optionTextActive: { color: '#fff', fontWeight: '600' },
hint: { fontSize: 12, marginTop: 8 },
versionRow: {
flexDirection: 'row',

View File

@@ -13,7 +13,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { VideoPlayer } from "../../components/VideoPlayer";
import { CommentItem } from "../../components/CommentItem";
import { getDanmaku } from "../../services/bilibili";
import { getDanmaku, getUploaderStat } from "../../services/bilibili";
import { DanmakuItem } from "../../services/types";
import DanmakuList from "../../components/DanmakuList";
import { useVideoDetail } from "../../hooks/useVideoDetail";
@@ -49,11 +49,12 @@ export default function VideoDetailScreen() {
const [danmakus, setDanmakus] = useState<DanmakuItem[]>([]);
const [currentTime, setCurrentTime] = useState(0);
const [showDownload, setShowDownload] = useState(false);
const [uploaderStat, setUploaderStat] = useState<{ follower: number; archiveCount: number } | null>(null);
const {
videos: relatedVideos,
loading: relatedLoading,
load: loadRelated,
} = useRelatedVideos();
} = useRelatedVideos(bvid as string);
useEffect(() => {
loadRelated();
@@ -68,6 +69,11 @@ export default function VideoDetailScreen() {
getDanmaku(video.cid).then(setDanmakus);
}, [video?.cid]);
useEffect(() => {
if (!video?.owner?.mid) return;
getUploaderStat(video.owner.mid).then(setUploaderStat).catch(() => {});
}, [video?.owner?.mid]);
return (
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
{/* TopBar */}
@@ -178,10 +184,6 @@ export default function VideoDetailScreen() {
data={relatedVideos}
keyExtractor={(item) => item.bvid}
showsVerticalScrollIndicator={false}
onEndReached={() => {
if (!relatedLoading) loadRelated();
}}
onEndReachedThreshold={0.5}
ListHeaderComponent={
<>
<View style={styles.upRow}>
@@ -189,9 +191,16 @@ export default function VideoDetailScreen() {
source={{ uri: proxyImageUrl(video.owner.face) }}
style={styles.avatar}
/>
<Text style={[styles.upName, { color: theme.text }]}>
{video.owner.name}
</Text>
<View style={styles.upInfo}>
<Text style={[styles.upName, { color: theme.text }]}>
{video.owner.name}
</Text>
{uploaderStat && (
<Text style={styles.upStat}>
{formatCount(uploaderStat.follower)} · {formatCount(uploaderStat.archiveCount)}
</Text>
)}
</View>
<TouchableOpacity style={styles.followBtn}>
<Text style={styles.followTxt}>+ </Text>
</TouchableOpacity>
@@ -249,7 +258,7 @@ export default function VideoDetailScreen() {
borderBottomColor: theme.border,
},
]}
onPress={() => router.push(`/video/${item.bvid}` as any)}
onPress={() => router.replace(`/video/${item.bvid}` as any)}
activeOpacity={0.85}
>
<View
@@ -321,7 +330,6 @@ export default function VideoDetailScreen() {
<View
style={[styles.sortRow, { borderBottomColor: theme.border }]}
>
<Text style={styles.sortLabel}></Text>
<TouchableOpacity
style={[
styles.sortBtn,
@@ -447,7 +455,7 @@ function SeasonSection({
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
getItemLayout={(_data, index) => ({
length: 130,
offset: 12 + index * 140,
offset: 12 + index * 130,
index,
})}
onScrollToIndexFailed={() => {}}
@@ -528,8 +536,10 @@ const styles = StyleSheet.create({
paddingBottom: 0,
paddingTop: 12,
},
avatar: { width: 48, height: 48, borderRadius: 30, marginRight: 10 },
upName: { flex: 1, fontSize: 14, fontWeight: "500" },
avatar: { width: 40, height: 40, borderRadius: 30, marginRight: 10 },
upInfo: { flex: 1, justifyContent: "center" },
upName: { fontSize: 14, fontWeight: "500" },
upStat: { fontSize: 11, color: "#999", marginTop: 2 },
followBtn: {
backgroundColor: "#00AEEC",
paddingHorizontal: 10,
@@ -570,11 +580,12 @@ const styles = StyleSheet.create({
tabBar: {
flexDirection: "row",
borderBottomWidth: StyleSheet.hairlineWidth,
paddingLeft: 3,
},
tabItem: {
flex: 1,
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 12,
position: "relative",
},
tabLabel: { fontSize: 13 },
@@ -593,7 +604,7 @@ const styles = StyleSheet.create({
danmakuTab: { flex: 1 },
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
relatedHeader: {
paddingLeft: 12,
paddingLeft: 13,
paddingBottom: 8,
paddingTop: 8,
},
@@ -643,15 +654,13 @@ const styles = StyleSheet.create({
gap: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
},
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
sortBtn: {
paddingHorizontal: 14,
paddingVertical: 3,
borderRadius: 20,
borderWidth: 1,
borderColor: "#e0e0e0",
paddingHorizontal: 10,
paddingVertical: 2,
borderRadius: 16,
backgroundColor: "#f0f0f0",
},
sortBtnActive: { borderColor: "#00AEEC", backgroundColor: "#e8f7fd" },
sortBtnTxt: { fontSize: 12, color: "#666" },
sortBtnTxtActive: { color: "#00AEEC", fontWeight: "600" as const },
sortBtnActive: { backgroundColor: "#00AEEC" },
sortBtnTxt: { fontSize: 13, color: "#333", fontWeight: "500" },
sortBtnTxtActive: { color: "#fff", fontWeight: "600" as const },
});