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:
Developer
2026-03-26 00:43:37 +08:00
parent 27587859a4
commit 463c0db058
21 changed files with 524 additions and 103 deletions

View File

@@ -1,6 +1,25 @@
import { useState, useCallback, useRef } from 'react';
import { searchVideos } from '../services/bilibili';
import type { VideoItem } from '../services/types';
import { useState, useCallback, useRef, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { searchVideos, getSearchSuggest, getHotSearch } from '../services/bilibili';
import type { VideoItem, SearchSuggestItem, HotSearchItem } from '../services/types';
const HISTORY_KEY = 'search_history';
const MAX_HISTORY = 20;
export type SearchSort = 'default' | 'pubdate' | 'view';
async function loadHistory(): Promise<string[]> {
try {
const raw = await AsyncStorage.getItem(HISTORY_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
}
async function saveHistory(history: string[]) {
await AsyncStorage.setItem(HISTORY_KEY, JSON.stringify(history));
}
export function useSearch() {
const [keyword, setKeyword] = useState('');
@@ -8,18 +27,74 @@ export function useSearch() {
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [sort, setSort] = useState<SearchSort>('default');
const [history, setHistory] = useState<string[]>([]);
const [suggestions, setSuggestions] = useState<SearchSuggestItem[]>([]);
const [hotSearches, setHotSearches] = useState<HotSearchItem[]>([]);
const loadingRef = useRef(false);
const suggestTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const currentSort = useRef<SearchSort>('default');
const search = useCallback(async (kw: string, reset = false) => {
// Load history & hot searches on mount
useEffect(() => {
loadHistory().then(setHistory);
getHotSearch().then(setHotSearches);
}, []);
// Debounced suggestions
useEffect(() => {
if (suggestTimer.current) clearTimeout(suggestTimer.current);
if (!keyword.trim() || keyword.trim().length < 1) {
setSuggestions([]);
return;
}
suggestTimer.current = setTimeout(async () => {
const items = await getSearchSuggest(keyword.trim());
setSuggestions(items);
}, 300);
return () => {
if (suggestTimer.current) clearTimeout(suggestTimer.current);
};
}, [keyword]);
const addToHistory = useCallback(async (kw: string) => {
const trimmed = kw.trim();
if (!trimmed) return;
setHistory(prev => {
const filtered = prev.filter(h => h !== trimmed);
const next = [trimmed, ...filtered].slice(0, MAX_HISTORY);
saveHistory(next);
return next;
});
}, []);
const removeFromHistory = useCallback(async (kw: string) => {
setHistory(prev => {
const next = prev.filter(h => h !== kw);
saveHistory(next);
return next;
});
}, []);
const clearHistory = useCallback(async () => {
setHistory([]);
await AsyncStorage.removeItem(HISTORY_KEY);
}, []);
const search = useCallback(async (kw: string, reset = false, sortOverride?: SearchSort) => {
if (!kw.trim() || loadingRef.current) return;
loadingRef.current = true;
setLoading(true);
setSuggestions([]);
const activeSort = sortOverride ?? currentSort.current;
const currentPage = reset ? 1 : page;
const orderParam = activeSort === 'pubdate' ? 'pubdate' : activeSort === 'view' ? 'click' : '';
try {
const items = await searchVideos(kw, currentPage);
const items = await searchVideos(kw, currentPage, orderParam);
if (reset) {
setResults(items);
setPage(2);
addToHistory(kw);
} else {
setResults(prev => [...prev, ...items]);
setPage(p => p + 1);
@@ -31,12 +106,28 @@ export function useSearch() {
loadingRef.current = false;
setLoading(false);
}
}, [page]);
}, [page, addToHistory]);
const changeSort = useCallback((newSort: SearchSort) => {
setSort(newSort);
currentSort.current = newSort;
if (keyword.trim()) {
search(keyword, true, newSort);
}
}, [keyword, search]);
const loadMore = useCallback(() => {
if (!keyword.trim() || loadingRef.current || !hasMore) return;
search(keyword, false);
}, [keyword, hasMore, search]);
return { keyword, setKeyword, results, loading, hasMore, search, loadMore };
return {
keyword, setKeyword,
results, loading, hasMore,
search, loadMore,
sort, changeSort,
history, removeFromHistory, clearHistory,
suggestions,
hotSearches,
};
}