| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- import { Barrage } from "@/components/Barrage";
- import { CouponModal } from "@/components/CouponModal";
- import { Colors } from "@/constants/Colors";
- import { Images } from "@/constants/images";
- import { CouponItem, getNewbieGiftBagInfo, listValidCoupon } from "@/services/activity";
- import { getFeedbackList, getPoolList, PoolItem } from "@/services/award";
- import { getToken } from "@/services/http";
- import { Image } from "expo-image";
- import { useRouter } from "expo-router";
- import React, { useCallback, useEffect, useMemo, useState } from "react";
- import {
- ActivityIndicator,
- Dimensions,
- FlatList,
- RefreshControl,
- StatusBar,
- StyleSheet,
- Text,
- TextInput,
- TouchableOpacity,
- View,
- } from "react-native";
- import { useSafeAreaInsets } from "react-native-safe-area-context";
- const { width: SCREEN_WIDTH } = Dimensions.get("window");
- const typeList = [
- { label: "全部", value: "", type: 0 },
- { label: "高保赏", value: "UNLIMITED", type: 2 },
- { label: "高爆赏", value: "UNLIMITED", type: 1 },
- { label: "擂台赏", value: "YFS_PRO", type: 7 },
- { label: "一番赏", value: "YFS_PRO", type: 0 },
- ];
- interface BarrageItem {
- id: string;
- content: string;
- nickname?: string;
- avatar: string;
- poolName?: string;
- type?: string;
- text?: string;
- poolId?: string;
- }
- // Static Header Component
- const StaticHeader = React.memo(
- ({ barrageList }: { barrageList: BarrageItem[] }) => (
- <View>
- <View style={{ height: 60 }} />
- {/* Featured Banner Area */}
- <View style={styles.bannerContainer}>
- {/* Replace image with a styled cyber container for now */}
- <View style={styles.cyberBanner}>
- <Text style={styles.bannerTitle}>MATRIX</Text>
- <Text style={styles.bannerSubtitle}>SUPPLY CRATE</Text>
- {/* Visual tech lines */}
- <View style={styles.techLine1} />
- <View style={styles.techLine2} />
- </View>
- </View>
- {/* Barrage Area */}
- {barrageList && barrageList.length > 0 && (
- <View style={styles.barrageSection}>
- <Barrage
- data={barrageList.slice(0, Math.ceil(barrageList.length / 2))}
- />
- <View style={{ height: 8 }} />
- <Barrage
- data={barrageList.slice(Math.ceil(barrageList.length / 2))}
- speed={35}
- />
- </View>
- )}
- </View>
- ),
- );
- // Type Selector Component
- const TypeSelector = React.memo(
- ({
- typeIndex,
- priceSort,
- onTypeChange,
- onSortChange,
- }: {
- typeIndex: number;
- priceSort: number;
- onTypeChange: (index: number) => void;
- onSortChange: () => void;
- }) => (
- <View style={styles.typeSection}>
- <View style={styles.typeListContainer}>
- <FlatList
- horizontal
- showsHorizontalScrollIndicator={false}
- data={typeList}
- keyExtractor={(_, index) => index.toString()}
- renderItem={({ item, index }) => {
- const isActive = typeIndex === index;
- return (
- <TouchableOpacity
- style={[styles.typeItem, isActive && styles.typeItemActive]}
- onPress={() => onTypeChange(index)}
- activeOpacity={0.7}
- >
- <Text
- style={[styles.typeText, isActive && styles.typeTextActive]}
- >
- {item.label}
- </Text>
- </TouchableOpacity>
- );
- }}
- contentContainerStyle={styles.typeListContent}
- />
- </View>
- <TouchableOpacity
- style={styles.sortBtn}
- onPress={onSortChange}
- activeOpacity={0.7}
- >
- <Text style={styles.sortBtnText}>
- 价格 {priceSort === 0 ? "-" : priceSort === 1 ? "↑" : "↓"}
- </Text>
- </TouchableOpacity>
- </View>
- ),
- );
- export default function BoxScreen() {
- const router = useRouter();
- const insets = useSafeAreaInsets();
- const [keyword, setKeyword] = useState("");
- const [typeIndex, setTypeIndex] = useState(0);
- const [priceSort, setPriceSort] = useState(0);
- const [list, setList] = useState<PoolItem[]>([]);
- const [loading, setLoading] = useState(false);
- const [refreshing, setRefreshing] = useState(false);
- const [current, setCurrent] = useState(1);
- const [hasMore, setHasMore] = useState(true);
- const [barrageList, setBarrageList] = useState<BarrageItem[]>([]);
- // 新人优惠券弹窗
- const [couponVisible, setCouponVisible] = useState(false);
- const [couponList, setCouponList] = useState<CouponItem[]>([]);
- const [newPeopleChecked, setNewPeopleChecked] = useState(false);
- // 加载弹幕
- const loadBarrage = useCallback(async () => {
- try {
- const res = await getFeedbackList();
- if (res.data) {
- setBarrageList(res.data);
- }
- } catch (error) {
- console.error("加载弹幕失败:", error);
- }
- }, []);
- // 新人大礼包检测(Vue: initNewPeople)
- const initNewPeople = useCallback(async () => {
- if (newPeopleChecked) return;
- const token = getToken();
- if (!token) return; // 未登录不检测
- try {
- const data = await getNewbieGiftBagInfo();
- if (data) {
- setNewPeopleChecked(true);
- // Vue 原项目只是展示一张海报图片,这里跳过(无具体交互)
- }
- } catch (e) {
- // 静默失败
- }
- }, [newPeopleChecked]);
- // 优惠券检测(Vue: initCoupon)
- const initCoupon = useCallback(async () => {
- const token = getToken();
- if (!token) return;
- try {
- const coupons = await listValidCoupon("LUCK");
- if (coupons && coupons.length > 0) {
- setCouponList(coupons);
- setCouponVisible(true);
- }
- } catch (e) {
- // 静默失败
- }
- }, []);
- const loadData = useCallback(
- async (isRefresh = false) => {
- if (loading) return;
- const page = isRefresh ? 1 : current;
- if (!isRefresh && !hasMore) return;
- setLoading(true);
- try {
- const selectedType = typeList[typeIndex];
- const res = await getPoolList({
- current: page,
- size: 10,
- mode: selectedType.value || undefined,
- type: selectedType.type,
- keyword: keyword || undefined,
- priceSort: priceSort || undefined,
- });
- if (res.success && res.data) {
- const newList = isRefresh ? res.data : [...list, ...res.data];
- setList(newList);
- setCurrent(page + 1);
- setHasMore(newList.length < (res.count || 0));
- }
- } catch (error) {
- console.error("加载奖池列表失败:", error);
- }
- setLoading(false);
- setRefreshing(false);
- },
- [current, hasMore, loading, typeIndex, list, keyword, priceSort],
- );
- useEffect(() => {
- loadData(true);
- loadBarrage();
- initNewPeople();
- initCoupon();
- }, [typeIndex, priceSort]);
- // 执行搜索
- const handleSearch = () => {
- setList([]);
- setCurrent(1);
- setHasMore(true);
- setTimeout(() => {
- loadData(true);
- }, 100);
- };
- const handleRefresh = () => {
- setRefreshing(true);
- loadData(true);
- };
- const handleLoadMore = () => {
- if (!loading && hasMore) {
- loadData(false);
- }
- };
- const handleTypeChange = useCallback((index: number) => {
- setTypeIndex(index);
- setList([]);
- setCurrent(1);
- setHasMore(true);
- }, []);
- const handlePriceSort = useCallback(() => {
- setPriceSort((prev) => (prev + 1) % 3);
- }, []);
- const handleItemPress = useCallback(
- (item: PoolItem) => {
- if (item.status !== undefined && item.status !== 1) return;
- if (item.type === 7) {
- router.push({
- pathname: "/boxInBox",
- params: { poolId: item.id },
- } as any);
- } else if (item.mode === "UNLIMITED") {
- router.push({
- pathname: "/award-detail",
- params: { poolId: item.id },
- } as any);
- } else if (item.mode === "YFS_PRO") {
- router.push({
- pathname: "/award-detail-yfs",
- params: { poolId: item.id },
- } as any);
- } else {
- router.push(`/product/${item.id}` as any);
- }
- },
- [router],
- );
- const renderItem = useCallback(
- ({ item }: { item: PoolItem }) => (
- <TouchableOpacity
- style={styles.itemContainer}
- onPress={() => handleItemPress(item)}
- activeOpacity={0.8}
- >
- <View style={styles.itemCard}>
- <View style={styles.itemImageContainer}>
- <Image
- source={{ uri: item.cover }}
- style={styles.itemImage}
- contentFit="cover"
- />
- {/* Cyber Border Overlay */}
- <View style={styles.itemCyberOverlay} />
- </View>
- <View style={styles.itemInfo}>
- <Text style={styles.itemName} numberOfLines={1}>
- {item.name}
- </Text>
- <Text style={styles.itemPrice}>
- <Text style={styles.priceUnit}>¥</Text>
- {item.price} <Text style={styles.priceStart}>起</Text>
- </Text>
- </View>
- </View>
- </TouchableOpacity>
- ),
- [handleItemPress],
- );
- const ListHeader = useMemo(
- () => (
- <View>
- <StaticHeader barrageList={barrageList} />
- <TypeSelector
- typeIndex={typeIndex}
- priceSort={priceSort}
- onTypeChange={handleTypeChange}
- onSortChange={handlePriceSort}
- />
- </View>
- ),
- [barrageList, typeIndex, priceSort, handleTypeChange, handlePriceSort],
- );
- const renderFooter = useCallback(() => {
- if (!loading) return null;
- return (
- <View style={styles.footer}>
- <ActivityIndicator size="small" color={Colors.neonBlue} />
- <Text style={styles.footerText}>数据传输中...</Text>
- </View>
- );
- }, [loading]);
- const renderEmpty = useCallback(() => {
- if (loading) return null;
- return (
- <View style={styles.empty}>
- <Text style={styles.emptyText}>暂无数据</Text>
- </View>
- );
- }, [loading]);
- return (
- <View style={styles.container}>
- <StatusBar barStyle="light-content" />
- <View style={styles.background}>
- {/* Header */}
- <View style={[styles.header, { paddingTop: insets.top + 10 }]}>
- <View style={styles.logoArea}>
- <Text style={styles.logoText}>
- CYBER<Text style={styles.logoHighlight}>BOX</Text>
- </Text>
- </View>
- <View style={styles.searchBar}>
- <Image
- source={{ uri: Images.home.search }}
- style={styles.searchIcon}
- contentFit="contain"
- tintColor={Colors.neonBlue}
- />
- <TextInput
- style={styles.searchInput}
- value={keyword}
- onChangeText={setKeyword}
- placeholder="搜索奖池"
- placeholderTextColor={Colors.textTertiary}
- returnKeyType="search"
- onSubmitEditing={handleSearch}
- />
- </View>
- </View>
- {/* List */}
- <FlatList
- data={list}
- renderItem={renderItem}
- keyExtractor={(item) => item.id}
- ListHeaderComponent={ListHeader}
- ListFooterComponent={renderFooter}
- ListEmptyComponent={renderEmpty}
- contentContainerStyle={styles.listContent}
- showsVerticalScrollIndicator={false}
- refreshControl={
- <RefreshControl
- refreshing={refreshing}
- onRefresh={handleRefresh}
- tintColor={Colors.neonBlue}
- />
- }
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.3}
- />
- {/* 新人优惠券弹窗 */}
- <CouponModal
- visible={couponVisible}
- coupons={couponList}
- onClose={() => setCouponVisible(false)}
- onSuccess={() => {
- setCouponList([]);
- loadData(true);
- }}
- />
- </View>
- </View>
- );
- }
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors.darkBg,
- },
- background: {
- flex: 1,
- backgroundColor: Colors.darkBg,
- },
- header: {
- position: "relative",
- zIndex: 11,
- flexDirection: "row",
- alignItems: "center",
- paddingHorizontal: 15,
- paddingBottom: 10,
- backgroundColor: Colors.darkBg,
- },
- logoArea: {
- marginRight: 15,
- },
- logoText: {
- color: "#fff",
- fontWeight: "bold",
- fontSize: 18,
- fontStyle: "italic",
- },
- logoHighlight: {
- color: Colors.neonPink,
- },
- searchBar: {
- flex: 1,
- flexDirection: "row",
- alignItems: "center",
- backgroundColor: "rgba(255,255,255,0.05)",
- borderRadius: 20,
- paddingHorizontal: 15,
- height: 32,
- borderWidth: 1,
- borderColor: "rgba(0, 243, 255, 0.3)",
- },
- searchIcon: {
- width: 14,
- height: 14,
- marginRight: 8,
- },
- searchInput: {
- flex: 1,
- color: "#fff",
- fontSize: 12,
- padding: 0,
- },
- // Banner
- bannerContainer: {
- height: 180,
- marginHorizontal: 10,
- marginTop: 10,
- marginBottom: 20,
- borderRadius: 12,
- overflow: "hidden",
- },
- cyberBanner: {
- flex: 1,
- backgroundColor: Colors.darkCard,
- justifyContent: "center",
- alignItems: "center",
- borderWidth: 1,
- borderColor: Colors.neonPink,
- position: "relative",
- },
- bannerTitle: {
- fontSize: 32,
- fontWeight: "bold",
- color: "#fff",
- textShadowColor: Colors.neonPink,
- textShadowRadius: 10,
- fontStyle: "italic",
- },
- bannerSubtitle: {
- fontSize: 14,
- color: Colors.neonBlue,
- letterSpacing: 4,
- marginTop: 5,
- },
- techLine1: {
- position: "absolute",
- top: 10,
- left: 10,
- width: 20,
- height: 2,
- backgroundColor: Colors.neonBlue,
- },
- techLine2: {
- position: "absolute",
- bottom: 10,
- right: 10,
- width: 20,
- height: 2,
- backgroundColor: Colors.neonBlue,
- },
- // Type Selector
- typeSection: {
- flexDirection: "row",
- alignItems: "center",
- paddingHorizontal: 10,
- paddingVertical: 10,
- zIndex: 10,
- },
- typeListContainer: {
- flex: 1,
- marginRight: 5,
- },
- typeListContent: {
- paddingRight: 10,
- },
- typeItem: {
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 15,
- backgroundColor: "rgba(255,255,255,0.05)",
- marginRight: 8,
- borderWidth: 1,
- borderColor: "transparent",
- },
- typeItemActive: {
- backgroundColor: "rgba(0, 243, 255, 0.15)",
- borderColor: Colors.neonBlue,
- },
- typeText: {
- color: Colors.textSecondary,
- fontSize: 12,
- fontWeight: "600",
- },
- typeTextActive: {
- color: "#fff",
- textShadowColor: Colors.neonBlue,
- textShadowRadius: 5,
- },
- sortBtn: {
- paddingHorizontal: 8,
- paddingVertical: 6,
- alignItems: "center",
- justifyContent: "center",
- backgroundColor: "rgba(255,255,255,0.05)",
- borderRadius: 15,
- },
- sortBtnText: {
- color: Colors.textTertiary,
- fontSize: 12,
- },
- // List
- listContent: {
- paddingHorizontal: 10,
- paddingBottom: 100,
- },
- itemContainer: {
- marginBottom: 12,
- },
- itemCard: {
- width: "100%",
- backgroundColor: Colors.darkCard,
- borderRadius: 8,
- overflow: "hidden",
- borderWidth: 1,
- borderColor: "rgba(0, 243, 255, 0.2)",
- },
- itemImageContainer: {
- height: 160,
- width: "100%",
- position: "relative",
- },
- itemImage: {
- width: "100%",
- height: "100%",
- },
- itemCyberOverlay: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: "rgba(0,0,0,0.1)", // Slight dark overlay
- borderBottomWidth: 1,
- borderBottomColor: Colors.neonBlue,
- },
- itemInfo: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- padding: 12,
- },
- itemName: {
- flex: 1,
- color: Colors.textPrimary,
- fontSize: 14,
- fontWeight: "bold",
- },
- itemPrice: {
- color: Colors.neonBlue,
- fontSize: 16,
- fontWeight: "bold",
- marginLeft: 10,
- textShadowColor: Colors.neonBlue,
- textShadowRadius: 5,
- },
- priceUnit: {
- fontSize: 12,
- marginRight: 2,
- },
- priceStart: {
- fontSize: 10,
- color: Colors.textTertiary,
- fontWeight: "normal",
- },
- // Footer
- footer: {
- paddingVertical: 15,
- alignItems: "center",
- flexDirection: "row",
- justifyContent: "center",
- },
- footerText: {
- color: Colors.textTertiary,
- fontSize: 12,
- marginLeft: 8,
- },
- empty: {
- alignItems: "center",
- paddingVertical: 50,
- },
- emptyText: {
- color: Colors.textTertiary,
- fontSize: 14,
- },
- // Barrage
- barrageSection: {
- marginVertical: 10,
- paddingHorizontal: 5, // Edge to edge
- },
- });
|