This commit is contained in:
Developer
2026-03-16 16:20:59 +08:00
parent 173b93c1d2
commit 829d175baa
10 changed files with 679 additions and 7 deletions

187
app/live/[roomId].tsx Normal file
View File

@@ -0,0 +1,187 @@
import React from 'react';
import {
View,
Text,
ScrollView,
StyleSheet,
TouchableOpacity,
Image,
ActivityIndicator,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useLiveDetail } from '../../hooks/useLiveDetail';
import { LivePlayer } from '../../components/LivePlayer';
import { formatCount } from '../../utils/format';
import { proxyImageUrl } from '../../utils/imageUrl';
export default function LiveDetailScreen() {
const { roomId } = useLocalSearchParams<{ roomId: string }>();
const router = useRouter();
const id = parseInt(roomId ?? '0', 10);
const { room, anchor, stream, loading, error } = useLiveDetail(id);
const isLive = room?.live_status === 1;
const hlsUrl = stream?.hlsUrl ?? '';
return (
<SafeAreaView style={styles.safe}>
{/* TopBar */}
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Ionicons name="chevron-back" size={24} color="#212121" />
</TouchableOpacity>
<Text style={styles.topTitle} numberOfLines={1}>
{room?.title ?? '直播间'}
</Text>
</View>
{/* Player */}
<LivePlayer hlsUrl={hlsUrl} isLive={isLive} />
{/* Content */}
{loading ? (
<ActivityIndicator style={styles.loader} color="#00AEEC" />
) : error ? (
<Text style={styles.errorText}>{error}</Text>
) : room ? (
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
{/* Room title */}
<View style={styles.titleSection}>
<Text style={styles.title}>{room.title}</Text>
<View style={styles.metaRow}>
{/* Live status */}
{isLive ? (
<View style={styles.livePill}>
<View style={styles.liveDot} />
<Text style={styles.livePillText}></Text>
</View>
) : (
<View style={[styles.livePill, styles.offlinePill]}>
<Text style={[styles.livePillText, styles.offlinePillText]}></Text>
</View>
)}
{/* Online count */}
<View style={styles.onlineRow}>
<Ionicons name="eye-outline" size={13} color="#999" />
<Text style={styles.onlineText}>{formatCount(room.online)}</Text>
</View>
</View>
{/* Area tags */}
<View style={styles.areaRow}>
{room.parent_area_name ? (
<Text style={styles.areaTag}>{room.parent_area_name}</Text>
) : null}
{room.area_name ? (
<Text style={styles.areaTag}>{room.area_name}</Text>
) : null}
</View>
</View>
{/* Divider */}
<View style={styles.divider} />
{/* Anchor row */}
{anchor && (
<View style={styles.anchorRow}>
<Image
source={{ uri: proxyImageUrl(anchor.face) }}
style={styles.avatar}
/>
<Text style={styles.anchorName}>{anchor.uname}</Text>
<TouchableOpacity style={styles.followBtn}>
<Text style={styles.followTxt}>+ </Text>
</TouchableOpacity>
</View>
)}
{/* Room description */}
{!!room.description && (
<View style={styles.descBox}>
<Text style={styles.descText}>{room.description}</Text>
</View>
)}
</ScrollView>
) : null}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: '#fff' },
topBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#eee',
},
backBtn: { padding: 4 },
topTitle: {
flex: 1,
fontSize: 15,
fontWeight: '600',
marginLeft: 4,
color: '#212121',
},
loader: { marginVertical: 30 },
errorText: { textAlign: 'center', color: '#f00', padding: 20 },
scroll: { flex: 1 },
titleSection: { padding: 14 },
title: {
fontSize: 16,
fontWeight: '600',
color: '#212121',
lineHeight: 22,
marginBottom: 8,
},
metaRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 8 },
livePill: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff0f0',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 4,
gap: 4,
},
offlinePill: { backgroundColor: '#f5f5f5' },
liveDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#f00' },
livePillText: { fontSize: 12, color: '#f00', fontWeight: '600' },
offlinePillText: { color: '#999' },
onlineRow: { flexDirection: 'row', alignItems: 'center', gap: 3 },
onlineText: { fontSize: 12, color: '#999' },
areaRow: { flexDirection: 'row', gap: 6 },
areaTag: {
fontSize: 11,
color: '#00AEEC',
backgroundColor: '#e8f7fd',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
},
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#f0f0f0',
marginHorizontal: 14,
},
anchorRow: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 12,
},
avatar: { width: 40, height: 40, borderRadius: 20, marginRight: 10 },
anchorName: { flex: 1, fontSize: 14, color: '#212121', fontWeight: '500' },
followBtn: {
backgroundColor: '#00AEEC',
paddingHorizontal: 14,
paddingVertical: 5,
borderRadius: 14,
},
followTxt: { color: '#fff', fontSize: 12, fontWeight: '600' },
descBox: { padding: 14, paddingTop: 4 },
descText: { fontSize: 14, color: '#555', lineHeight: 22 },
});

5
app/live/_layout.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { Slot } from 'expo-router';
export default function LiveLayout() {
return <Slot />;
}