226 lines
7.7 KiB
JavaScript
226 lines
7.7 KiB
JavaScript
import {
|
||
View,
|
||
RefreshControl,
|
||
ActivityIndicator,
|
||
Animated,
|
||
Easing,
|
||
TouchableOpacity,
|
||
} from "react-native";
|
||
import React, { useState, useRef, useEffect } from "react";
|
||
import StreamerCard from "../../../components/StreamerCard";
|
||
import Empty from "../../../components/Empty";
|
||
import { useTailwind } from "tailwind-rn";
|
||
import baseRequest from "../../../utils/baseRequest";
|
||
import Toast from "react-native-toast-message";
|
||
import { generateSignature } from "../../../utils/crypto";
|
||
import { FlashList } from "@shopify/flash-list";
|
||
import { Svg, Path } from "react-native-svg";
|
||
|
||
export default function FeedStream() {
|
||
//获取推荐的主播mid
|
||
const [recommMids, setRecommMids] = useState([]);
|
||
const getRecommMids = async () => {
|
||
if (!isEnd) return;
|
||
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||
try {
|
||
const base = await baseRequest();
|
||
const signature = await generateSignature({
|
||
...base,
|
||
});
|
||
const response = await fetch(
|
||
`${apiUrl}/api/streamer/recomm_list?signature=${signature}`,
|
||
{
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
...base,
|
||
}),
|
||
}
|
||
);
|
||
const recommData = await response.json();
|
||
if (recommData.ret === -1) {
|
||
Toast.show({
|
||
type: "error",
|
||
text1: recommData.msg,
|
||
topOffset: 60,
|
||
});
|
||
return;
|
||
}
|
||
setIsEnd(false);
|
||
setRecommMids(recommData.data.recomm_list);
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
};
|
||
|
||
//查询的起始索引
|
||
const [startIndex, setStartIndex] = useState(0);
|
||
// 定义每次取多少个用户ID
|
||
const batchSize = 4;
|
||
//获取新数据
|
||
const [data, setData] = useState([]);
|
||
const [isEnd, setIsEnd] = useState(true);
|
||
const [isActivityIndicatorShow, setIsActivityIndicatorShow] = useState(true);
|
||
const [isFetching, setIsFetching] = useState(false);
|
||
const getData = async (type) => {
|
||
if (isFetching) return;
|
||
if (isEnd) return;
|
||
if (recommMids.length === 0) return;
|
||
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||
const batchStreamerMids = recommMids.slice(
|
||
startIndex,
|
||
startIndex + batchSize
|
||
);
|
||
if (batchStreamerMids.length === 0) {
|
||
if (type !== "top") {
|
||
setIsActivityIndicatorShow(false);
|
||
return;
|
||
}
|
||
setIsActivityIndicatorShow(true);
|
||
setIsEnd(true);
|
||
setStartIndex(0);
|
||
return;
|
||
}
|
||
setIsFetching((prev) => true);
|
||
try {
|
||
const base = await baseRequest();
|
||
const signature = await generateSignature({
|
||
mids: batchStreamerMids,
|
||
...base,
|
||
});
|
||
const detailResponse = await fetch(
|
||
`${apiUrl}/api/streamer/list_ext_by_mids?signature=${signature}`,
|
||
{
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
mids: batchStreamerMids,
|
||
...base,
|
||
}),
|
||
}
|
||
);
|
||
const detailData = await detailResponse.json();
|
||
if (detailData.ret === -1) {
|
||
Toast.show({
|
||
type: "error",
|
||
text1: detailData.msg,
|
||
topOffset: 60,
|
||
});
|
||
return;
|
||
}
|
||
const sortedData = detailData.data.list.sort((a, b) => b.fans - a.fans);
|
||
if (type === "top") {
|
||
setData((prev) => {
|
||
if (startIndex === 0) {
|
||
return sortedData;
|
||
} else return [...sortedData, ...prev];
|
||
});
|
||
} else {
|
||
setData((prev) => [...prev, ...sortedData]);
|
||
}
|
||
// 更新下一次查询的起始索引
|
||
setStartIndex(startIndex + batchSize);
|
||
setIsFetching((prev) => false);
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
};
|
||
|
||
//获取推荐mid
|
||
useEffect(() => {
|
||
getRecommMids();
|
||
}, [isEnd]);
|
||
|
||
//当推荐的mid改变,获取新数据
|
||
useEffect(() => {
|
||
getData("top");
|
||
}, [recommMids]);
|
||
|
||
const tailwind = useTailwind();
|
||
const renderItem = ({ item }) => <StreamerCard data={item} />;
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
//刷新动画
|
||
const flatListRef = useRef(null);
|
||
const [rotateValue] = useState(new Animated.Value(0));
|
||
const startRotation = () => {
|
||
Animated.timing(rotateValue, {
|
||
toValue: 1,
|
||
duration: 500,
|
||
easing: Easing.linear,
|
||
useNativeDriver: true,
|
||
}).start(() => {
|
||
rotateValue.setValue(0);
|
||
});
|
||
};
|
||
const spin = rotateValue.interpolate({
|
||
inputRange: [0, 1],
|
||
outputRange: ["0deg", "360deg"],
|
||
});
|
||
//下拉刷新
|
||
const handleRefresh = async () => {
|
||
startRotation();
|
||
flatListRef.current.scrollToOffset({ animated: false, offset: 0 });
|
||
setRefreshing(true);
|
||
await getData("top");
|
||
setRefreshing(false);
|
||
};
|
||
|
||
return (
|
||
<View style={tailwind("flex-1")}>
|
||
<FlashList
|
||
ref={flatListRef}
|
||
data={data}
|
||
renderItem={renderItem}
|
||
estimatedItemSize={287}
|
||
initialNumToRender={4}
|
||
refreshControl={
|
||
<RefreshControl
|
||
colors={["#FF669E"]}
|
||
tintColor="white"
|
||
refreshing={refreshing}
|
||
onRefresh={() => handleRefresh()}
|
||
/>
|
||
}
|
||
onEndReached={() => getData("bottom")}
|
||
ListEmptyComponent={<Empty type="nodata" />}
|
||
ListFooterComponent={
|
||
<View>
|
||
{isActivityIndicatorShow && data.length !== 0 && (
|
||
<ActivityIndicator style={tailwind("my-4")} size="large" />
|
||
)}
|
||
</View>
|
||
}
|
||
keyExtractor={(item) => item.mid}
|
||
/>
|
||
<TouchableOpacity
|
||
style={tailwind(
|
||
"absolute right-5 bottom-5 bg-[#13121F] p-1 rounded-full"
|
||
)}
|
||
onPress={handleRefresh}
|
||
>
|
||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||
<Svg viewBox="0 0 1024 1024" width="30" height="30">
|
||
<Path
|
||
d="M855.7 546.3c-16.6 0-30-13.5-30-30.1 0.1-72.8-25.1-143.9-71-200.4l-1-1.3c-31.5-38.5-72.1-69.2-117.3-88.9l-0.6-0.2-2.2-1c-6.8-2.9-14.3-5.6-22.9-8.4l-0.6-0.2-6.5-2.3c-7.1-2.1-14.7-3.9-20.6-5.3-1.2-0.3-2.3-0.5-3.5-0.8-2.1-0.5-4-1-5.6-1.3l-0.4-0.1c-6.8-1.4-14.1-2.5-21.7-3.3l-0.8-0.1-8.4-1.2c-73.7-7.1-147.4 12.6-207.8 55.6-13.5 9.6-32.2 6.5-41.8-7-9.6-13.5-6.5-32.2 7-41.8 72.3-51.5 160.8-75.1 249.1-66.4l1.3 0.2 8.7 1.2c9 1 17.8 2.4 26 4 3.1 0.6 6 1.3 8.6 1.9 1 0.2 1.9 0.5 2.9 0.7 7 1.6 16.2 3.8 25.2 6.5l1.2 0.4 6.9 2.4c10.2 3.4 19.3 6.7 27.7 10.3l0.8 0.4 2.4 1.1c54.1 23.6 102.4 60.4 139.9 106.3l1 1.4c54.4 67.1 84.2 151.5 84.1 237.9-0.1 16.4-13.6 29.8-30.1 29.8z"
|
||
fill="#ffffff"
|
||
></Path>
|
||
<Path
|
||
d="M872.5 661.6l78.9-114.5c9-13.1-0.3-30.9-16.3-30.9H777.4c-15.9 0-25.3 17.8-16.3 30.9L840 661.6c7.9 11.4 24.7 11.4 32.5 0zM513.3 884.1c-12 0-24-0.6-36-1.8l-1.3-0.2-8.6-1.2c-9-1-17.8-2.4-26-4-3.1-0.6-6-1.3-8.6-1.9-1-0.2-1.9-0.5-2.9-0.7-7-1.6-16.2-3.8-25.2-6.5l-1.2-0.4-6.9-2.4c-10.2-3.4-19.3-6.7-27.7-10.3l-0.8-0.4-2.4-1.1c-54.1-23.6-102.4-60.4-139.9-106.3l-1-1.4c-54.4-67.1-84.2-151.5-84.1-237.9 0-16.6 13.5-29.9 30-29.9h0.1c16.6 0 30 13.5 29.9 30.1-0.1 72.8 25.1 143.9 71 200.4l1 1.3c31.5 38.5 72.1 69.2 117.3 88.9l2.8 1.3c6.8 2.9 14.3 5.6 22.9 8.4l0.6 0.2 6.5 2.3c7.1 2.1 14.7 3.9 20.6 5.3 1.2 0.3 2.3 0.5 3.5 0.8 2.1 0.5 4 1 5.6 1.3l0.4 0.1c6.8 1.3 14.1 2.5 21.7 3.3l0.8 0.1 8.4 1.2c73.7 7.1 147.4-12.6 207.8-55.6 13.5-9.6 32.2-6.5 41.8 7 9.6 13.5 6.5 32.2-7 41.8-62.5 44.5-137 68.2-213.1 68.2z"
|
||
fill="#ffffff"
|
||
></Path>
|
||
<Path
|
||
d="M153.8 362.4L74.9 476.9c-9 13.1 0.3 30.9 16.3 30.9H249c15.9 0 25.3-17.8 16.3-30.9l-78.9-114.5c-7.9-11.4-24.7-11.4-32.6 0z"
|
||
fill="#ffffff"
|
||
></Path>
|
||
</Svg>
|
||
</Animated.View>
|
||
</TouchableOpacity>
|
||
</View>
|
||
);
|
||
}
|