mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
为尊重知识产权及相关法律法规,本仓库即日起停止后续维护与更新,不再接受新的 Issue 和 Pull Request。 现有代码仅作学习参考保留,请勿将本项目用于任何商业或违法用途。 感谢所有支持过本项目的朋友。
249 lines
7.3 KiB
TypeScript
249 lines
7.3 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
||
import {
|
||
View,
|
||
Text,
|
||
Modal,
|
||
TouchableOpacity,
|
||
Animated,
|
||
StyleSheet,
|
||
FlatList,
|
||
ActivityIndicator,
|
||
InteractionManager,
|
||
useWindowDimensions,
|
||
} from "react-native";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import { CommentItem } from "./CommentItem";
|
||
import DanmakuList from "./DanmakuList";
|
||
import { useComments } from "../hooks/useComments";
|
||
import { useTheme } from "../utils/theme";
|
||
import { formatCount } from "../utils/format";
|
||
import { useSheetTransition } from "../utils/useSheetTransition";
|
||
import type { DanmakuItem } from "../services/types";
|
||
|
||
export type EngagementTab = "comments" | "danmaku";
|
||
|
||
interface Props {
|
||
visible: boolean;
|
||
onClose: () => void;
|
||
/** Sheet 顶部对齐到屏幕的绝对 Y(一般是播放器底部) */
|
||
topOffset: number;
|
||
initialTab: EngagementTab;
|
||
aid: number;
|
||
replyCount?: number;
|
||
danmakus: DanmakuItem[];
|
||
currentTime: number;
|
||
}
|
||
|
||
export function EngagementSheet({
|
||
visible,
|
||
onClose,
|
||
topOffset,
|
||
initialTab,
|
||
aid,
|
||
replyCount,
|
||
danmakus,
|
||
currentTime,
|
||
}: Props) {
|
||
const theme = useTheme();
|
||
const { height } = useWindowDimensions();
|
||
const sheetH = Math.max(120, height - topOffset);
|
||
const { rendered, slideAnim } = useSheetTransition(visible, sheetH);
|
||
|
||
const [tab, setTab] = useState<EngagementTab>(initialTab);
|
||
const [commentSort, setCommentSort] = useState<0 | 2>(2);
|
||
const { comments, loading, hasMore, load } = useComments(aid, commentSort);
|
||
|
||
// 每次打开根据外部 initialTab 切到对应 Tab
|
||
useEffect(() => {
|
||
if (visible) setTab(initialTab);
|
||
}, [visible, initialTab]);
|
||
|
||
// 评论懒加载:仅当 Sheet 可见且选中评论 Tab 时拉
|
||
useEffect(() => {
|
||
if (!visible || !aid || tab !== "comments") return;
|
||
const handle = InteractionManager.runAfterInteractions(() => load());
|
||
return () => handle.cancel();
|
||
}, [visible, aid, commentSort, tab, load]);
|
||
|
||
if (!rendered) return null;
|
||
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
animationType="none"
|
||
statusBarTranslucent
|
||
onRequestClose={onClose}
|
||
>
|
||
<Animated.View
|
||
style={[
|
||
styles.sheet,
|
||
{
|
||
top: topOffset,
|
||
backgroundColor: theme.sheetBg,
|
||
transform: [{ translateY: slideAnim }],
|
||
},
|
||
]}
|
||
>
|
||
{/* Tab 栏 + 关闭按钮 */}
|
||
<View style={[styles.tabBar, { borderBottomColor: theme.modalBorder }]}>
|
||
{(["comments", "danmaku"] as EngagementTab[]).map((t) => {
|
||
const label =
|
||
t === "comments"
|
||
? `评论${replyCount ? ` ${formatCount(replyCount)}` : ""}`
|
||
: `弹幕${danmakus.length ? ` ${formatCount(danmakus.length)}` : ""}`;
|
||
const active = tab === t;
|
||
return (
|
||
<TouchableOpacity
|
||
key={t}
|
||
style={styles.tabItem}
|
||
onPress={() => setTab(t)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text
|
||
style={[
|
||
styles.tabLabel,
|
||
{ color: theme.modalTextSub },
|
||
active && { color: theme.modalText, fontWeight: "700" },
|
||
]}
|
||
>
|
||
{label}
|
||
</Text>
|
||
{active && <View style={styles.tabUnderline} />}
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
<View style={{ flex: 1 }} />
|
||
<TouchableOpacity onPress={onClose} style={styles.closeBtn} hitSlop={8}>
|
||
<Ionicons name="close" size={22} color={theme.modalTextSub} />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* 评论 Tab */}
|
||
{tab === "comments" && (
|
||
<>
|
||
<View style={[styles.sortRow, { borderBottomColor: theme.modalBorder }]}>
|
||
{([2, 0] as const).map((sort) => (
|
||
<TouchableOpacity
|
||
key={sort}
|
||
style={[
|
||
styles.sortBtn,
|
||
{ backgroundColor: theme.inputBg },
|
||
commentSort === sort && styles.sortBtnActive,
|
||
]}
|
||
onPress={() => setCommentSort(sort)}
|
||
>
|
||
<Text
|
||
style={[
|
||
styles.sortBtnTxt,
|
||
{ color: theme.modalTextSub },
|
||
commentSort === sort && styles.sortBtnTxtActive,
|
||
]}
|
||
>
|
||
{sort === 2 ? "热门" : "最新"}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
<FlatList
|
||
style={styles.list}
|
||
data={comments}
|
||
keyExtractor={(c) => String(c.rpid)}
|
||
renderItem={({ item }) => <CommentItem item={item} />}
|
||
onEndReached={() => {
|
||
if (hasMore && !loading) load();
|
||
}}
|
||
onEndReachedThreshold={0.3}
|
||
showsVerticalScrollIndicator={false}
|
||
ListFooterComponent={
|
||
loading ? (
|
||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||
) : !hasMore && comments.length > 0 ? (
|
||
<Text style={[styles.emptyTxt, { color: theme.modalTextSub }]}>
|
||
已加载全部评论
|
||
</Text>
|
||
) : null
|
||
}
|
||
ListEmptyComponent={
|
||
!loading ? (
|
||
<Text style={[styles.emptyTxt, { color: theme.modalTextSub }]}>
|
||
暂无评论
|
||
</Text>
|
||
) : null
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{/* 弹幕 Tab —— hideHeader=true 让弹幕列表填满,header 由本 Sheet 顶部 Tab 提供 */}
|
||
{tab === "danmaku" && (
|
||
<DanmakuList
|
||
danmakus={danmakus}
|
||
currentTime={currentTime}
|
||
visible
|
||
onToggle={() => {}}
|
||
hideHeader
|
||
style={styles.danmakuList}
|
||
/>
|
||
)}
|
||
</Animated.View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
sheet: {
|
||
position: "absolute",
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
borderTopLeftRadius: 12,
|
||
borderTopRightRadius: 12,
|
||
overflow: "hidden",
|
||
},
|
||
tabBar: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||
paddingHorizontal: 4,
|
||
},
|
||
tabItem: {
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 14,
|
||
position: "relative",
|
||
alignItems: "center",
|
||
},
|
||
tabLabel: { fontSize: 14 },
|
||
tabUnderline: {
|
||
position: "absolute",
|
||
bottom: 0,
|
||
left: 14,
|
||
right: 14,
|
||
height: 2,
|
||
backgroundColor: "#00AEEC",
|
||
borderRadius: 1,
|
||
},
|
||
closeBtn: { padding: 10 },
|
||
sortRow: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
justifyContent: "flex-end",
|
||
paddingHorizontal: 14,
|
||
paddingVertical: 8,
|
||
gap: 8,
|
||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||
},
|
||
sortBtn: {
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 3,
|
||
borderRadius: 16,
|
||
},
|
||
sortBtnActive: { backgroundColor: "#00AEEC" },
|
||
sortBtnTxt: { fontSize: 13, fontWeight: "500" },
|
||
sortBtnTxtActive: { color: "#fff", fontWeight: "600" },
|
||
list: { flex: 1 },
|
||
loader: { marginVertical: 30 },
|
||
emptyTxt: { textAlign: "center", padding: 30, fontSize: 13 },
|
||
danmakuList: { flex: 1, borderTopWidth: 0 },
|
||
});
|