mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
feat: 性能优化 + Bug修复 + 搜索增强
- expo-image 替换 RN Image(VideoCard/LiveCard/BigVideoCard/CommentItem,recyclingKey) - DanmakuList Animated.Value 对象池,减少 GC 压力 - FlatList 性能参数:windowSize=7 / maxToRenderPerBatch=6 / removeClippedSubviews - bilibili.ts 请求去重(getVideoDetail/getPlayUrl) - SESSDATA 迁移至 expo-secure-store(utils/secureStorage.ts,启动自动迁移) - 主题系统扩展:新增 sheetBg/modalBg/modalText/placeholder/iconDefault/danger 等 token - 多组件深色模式适配:DownloadSheet/LivePlayer/NativeVideoPlayer/DownloadProgressBtn/CommentItem/LoginModal - 搜索页增强:搜索建议 + 热搜榜(hooks/useSearch.ts + app/search.tsx) - LoginModal 修复轮询竞态(cancelled flag + try-catch) - 修复 downloads.tsx 冗余三元表达式
This commit is contained in:
@@ -202,12 +202,12 @@ function DownloadRow({
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
onPress={isDownloading ? onDelete : onDelete}
|
||||
onPress={onDelete}
|
||||
>
|
||||
<Ionicons
|
||||
name={isDownloading ? 'close-circle-outline' : 'trash-outline'}
|
||||
size={20}
|
||||
color={isDownloading ? '#bbb' : '#bbb'}
|
||||
color="#bbb"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -334,6 +334,9 @@ export default function HomeScreen() {
|
||||
}
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={16}
|
||||
windowSize={7}
|
||||
maxToRenderPerBatch={6}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -402,6 +405,9 @@ export default function HomeScreen() {
|
||||
}
|
||||
onScroll={onLiveScroll}
|
||||
scrollEventThrottle={16}
|
||||
windowSize={7}
|
||||
maxToRenderPerBatch={6}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</View>
|
||||
</PagerView>
|
||||
|
||||
@@ -24,7 +24,6 @@ type Tab = "intro" | "danmaku";
|
||||
|
||||
export default function LiveDetailScreen() {
|
||||
const { roomId } = useLocalSearchParams<{ roomId: string }>();
|
||||
console.log("LiveDetailScreen params:", { roomId });
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
const id = parseInt(roomId ?? "0", 10);
|
||||
|
||||
268
app/search.tsx
268
app/search.tsx
@@ -7,26 +7,50 @@ import {
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { VideoCard } from '../components/VideoCard';
|
||||
import { useSearch } from '../hooks/useSearch';
|
||||
import { useSearch, SearchSort } from '../hooks/useSearch';
|
||||
import { useTheme } from '../utils/theme';
|
||||
import type { VideoItem } from '../services/types';
|
||||
|
||||
const SORT_OPTIONS: { key: SearchSort; label: string }[] = [
|
||||
{ key: 'default', label: '综合排序' },
|
||||
{ key: 'pubdate', label: '最新发布' },
|
||||
{ key: 'view', label: '最多播放' },
|
||||
];
|
||||
|
||||
export default function SearchScreen() {
|
||||
const router = useRouter();
|
||||
const { keyword, setKeyword, results, loading, hasMore, search, loadMore } = useSearch();
|
||||
const {
|
||||
keyword, setKeyword,
|
||||
results, loading, hasMore,
|
||||
search, loadMore,
|
||||
sort, changeSort,
|
||||
history, removeFromHistory, clearHistory,
|
||||
suggestions,
|
||||
hotSearches,
|
||||
} = useSearch();
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const hasResults = results.length > 0;
|
||||
const hasSearched = hasResults || (loading && results.length === 0);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (keyword.trim()) {
|
||||
search(keyword, true);
|
||||
const handleSearch = useCallback((kw?: string) => {
|
||||
const term = (kw ?? keyword).trim();
|
||||
if (term) {
|
||||
if (kw) setKeyword(kw);
|
||||
search(kw ?? keyword, true);
|
||||
}
|
||||
}, [keyword, search]);
|
||||
}, [keyword, search, setKeyword]);
|
||||
|
||||
const handleSuggestionPress = useCallback((value: string) => {
|
||||
setKeyword(value);
|
||||
search(value, true);
|
||||
}, [search, setKeyword]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: VideoItem; index: number }) => {
|
||||
@@ -61,16 +85,37 @@ export default function SearchScreen() {
|
||||
[],
|
||||
);
|
||||
|
||||
// Show pre-search panel (history + hot searches + suggestions)
|
||||
const showPreSearch = !hasSearched && !loading;
|
||||
const showSuggestions = suggestions.length > 0 && keyword.trim().length > 0 && !hasResults;
|
||||
|
||||
const ListHeaderComponent = useCallback(() => {
|
||||
if (!hasResults) return null;
|
||||
return (
|
||||
<View style={[styles.sortBar, { backgroundColor: theme.card }]}>
|
||||
{SORT_OPTIONS.map(opt => (
|
||||
<TouchableOpacity
|
||||
key={opt.key}
|
||||
style={[styles.sortBtn, sort === opt.key && styles.sortBtnActive]}
|
||||
onPress={() => changeSort(opt.key)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.sortBtnText, sort === opt.key && styles.sortBtnTextActive]}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}, [hasResults, sort, changeSort, theme.card]);
|
||||
|
||||
const ListEmptyComponent = () => {
|
||||
if (loading) return null;
|
||||
if (!keyword.trim()) return null;
|
||||
return (
|
||||
<View style={styles.emptyBox}>
|
||||
<Ionicons name="search-outline" size={48} color="#ddd" />
|
||||
<Text style={[styles.emptyText, { color: theme.textSub }]}>
|
||||
{results.length === 0 && keyword.trim()
|
||||
? '没有找到相关视频'
|
||||
: '输入关键词搜索'}
|
||||
</Text>
|
||||
<Text style={[styles.emptyText, { color: theme.textSub }]}>没有找到相关视频</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -90,7 +135,7 @@ export default function SearchScreen() {
|
||||
placeholderTextColor="#999"
|
||||
value={keyword}
|
||||
onChangeText={setKeyword}
|
||||
onSubmitEditing={handleSearch}
|
||||
onSubmitEditing={() => handleSearch()}
|
||||
returnKeyType="search"
|
||||
autoFocus
|
||||
autoCapitalize="none"
|
||||
@@ -102,29 +147,109 @@ export default function SearchScreen() {
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity style={styles.searchBtn} onPress={handleSearch}>
|
||||
<TouchableOpacity style={styles.searchBtn} onPress={() => handleSearch()} activeOpacity={0.85}>
|
||||
<Text style={styles.searchBtnText}>搜索</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Results */}
|
||||
<FlatList
|
||||
data={results}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListEmptyComponent={<ListEmptyComponent />}
|
||||
ListFooterComponent={
|
||||
loading && results.length > 0 ? (
|
||||
<View style={styles.footer}>
|
||||
<ActivityIndicator color="#00AEEC" />
|
||||
{/* Suggestions dropdown */}
|
||||
{showSuggestions && (
|
||||
<View style={[styles.suggestPanel, { backgroundColor: theme.card }]}>
|
||||
{suggestions.map((s, i) => (
|
||||
<TouchableOpacity
|
||||
key={`${s.value}-${i}`}
|
||||
style={[styles.suggestItem, { borderBottomColor: theme.border }]}
|
||||
onPress={() => handleSuggestionPress(s.value)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Ionicons name="search-outline" size={14} color="#bbb" style={styles.suggestIcon} />
|
||||
<Text style={[styles.suggestText, { color: theme.text }]} numberOfLines={1}>{s.value}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Pre-search: history + hot searches */}
|
||||
{showPreSearch && !showSuggestions ? (
|
||||
<ScrollView style={styles.preSearch} keyboardShouldPersistTaps="handled">
|
||||
{/* Search history */}
|
||||
{history.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>搜索历史</Text>
|
||||
<TouchableOpacity onPress={clearHistory} activeOpacity={0.85}>
|
||||
<Ionicons name="trash-outline" size={16} color={theme.textSub} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.tagWrap}>
|
||||
{history.map(h => (
|
||||
<TouchableOpacity
|
||||
key={h}
|
||||
style={[styles.tag, { backgroundColor: theme.inputBg }]}
|
||||
onPress={() => handleSearch(h)}
|
||||
onLongPress={() => removeFromHistory(h)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.tagText, { color: theme.text }]} numberOfLines={1}>{h}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Hot searches */}
|
||||
{hotSearches.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>热搜榜</Text>
|
||||
{hotSearches.map((item, idx) => (
|
||||
<TouchableOpacity
|
||||
key={item.keyword}
|
||||
style={[styles.hotItem, { borderBottomColor: theme.border }]}
|
||||
onPress={() => handleSearch(item.keyword)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[
|
||||
styles.hotIndex,
|
||||
idx < 3 && styles.hotIndexTop,
|
||||
]}>
|
||||
{idx + 1}
|
||||
</Text>
|
||||
<Text style={[styles.hotText, { color: theme.text }]} numberOfLines={1}>
|
||||
{item.show_name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{history.length === 0 && hotSearches.length === 0 && (
|
||||
<View style={styles.emptyBox}>
|
||||
<Ionicons name="search-outline" size={48} color="#ddd" />
|
||||
<Text style={[styles.emptyText, { color: theme.textSub }]}>输入关键词搜索</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
) : (
|
||||
/* Results list */
|
||||
<FlatList
|
||||
data={results}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListHeaderComponent={<ListHeaderComponent />}
|
||||
ListEmptyComponent={<ListEmptyComponent />}
|
||||
ListFooterComponent={
|
||||
loading && results.length > 0 ? (
|
||||
<View style={styles.footer}>
|
||||
<ActivityIndicator color="#00AEEC" />
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +288,88 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 6,
|
||||
},
|
||||
searchBtnText: { fontSize: 14, color: '#00AEEC', fontWeight: '600' },
|
||||
listContent: { paddingTop: 6, paddingBottom: 20 },
|
||||
|
||||
// Sort bar
|
||||
sortBar: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
gap: 12,
|
||||
},
|
||||
sortBtn: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 14,
|
||||
},
|
||||
sortBtnActive: {
|
||||
backgroundColor: '#00AEEC',
|
||||
},
|
||||
sortBtnText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
},
|
||||
sortBtnTextActive: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
// Suggestions
|
||||
suggestPanel: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
suggestItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
suggestIcon: { marginRight: 8 },
|
||||
suggestText: { fontSize: 14, flex: 1 },
|
||||
|
||||
// Pre-search
|
||||
preSearch: { flex: 1, paddingHorizontal: 16, paddingTop: 12 },
|
||||
section: { marginBottom: 20 },
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: { fontSize: 15, fontWeight: '600', color: '#212121', marginBottom: 2 },
|
||||
tagWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||
tag: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#f0f0f0',
|
||||
maxWidth: '45%',
|
||||
},
|
||||
tagText: { fontSize: 13, color: '#212121' },
|
||||
|
||||
// Hot search list
|
||||
hotItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
hotIndex: {
|
||||
width: 22,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#999',
|
||||
textAlign: 'center',
|
||||
marginRight: 10,
|
||||
},
|
||||
hotIndexTop: { color: '#00AEEC' },
|
||||
hotText: { fontSize: 14, flex: 1 },
|
||||
|
||||
// Results
|
||||
listContent: { paddingTop: 0, paddingBottom: 20 },
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 1,
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function VideoDetailScreen() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!video?.cid) return;
|
||||
getDanmaku(video.cid).then(setDanmakus);
|
||||
getDanmaku(video.cid).then(setDanmakus).catch(() => {});
|
||||
}, [video?.cid]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -197,7 +197,11 @@ export default function VideoDetailScreen() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
<View style={styles.upRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.upRow}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => router.push(`/creator/${video.owner.mid}` as any)}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: proxyImageUrl(video.owner.face) }}
|
||||
style={styles.avatar}
|
||||
@@ -213,10 +217,10 @@ export default function VideoDetailScreen() {
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity style={styles.followBtn}>
|
||||
<Text style={styles.followTxt}>+ 关注</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.followBtn}>
|
||||
<Text style={styles.followTxt}>查看主页</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
style={[
|
||||
styles.titleSection,
|
||||
@@ -661,6 +665,7 @@ const styles = StyleSheet.create({
|
||||
sortRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
gap: 8,
|
||||
|
||||
Reference in New Issue
Block a user