优化play页面首次加载性能
This commit is contained in:
@@ -732,6 +732,7 @@ function PlayPageClient() {
|
||||
const [sourceSearchError, setSourceSearchError] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [backgroundSourcesLoading, setBackgroundSourcesLoading] = useState(false);
|
||||
|
||||
// 优选和测速开关
|
||||
const [optimizationEnabled] = useState<boolean>(() => {
|
||||
@@ -1966,6 +1967,43 @@ function PlayPageClient() {
|
||||
const fetchSourcesData = async (query: string): Promise<SearchResult[]> => {
|
||||
// 根据搜索词获取全部源信息
|
||||
try {
|
||||
// 先检查 sessionStorage 中是否有缓存
|
||||
const cacheKey = `search_cache_${query.trim()}`;
|
||||
let results: SearchResult[] = [];
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const cached = sessionStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
console.log('[Play] 使用 sessionStorage 缓存的搜索结果');
|
||||
const cachedData = JSON.parse(cached);
|
||||
|
||||
// 处理缓存的搜索结果,根据规则过滤
|
||||
results = cachedData.filter(
|
||||
(result: SearchResult) =>
|
||||
result.title.replaceAll(' ', '').toLowerCase() ===
|
||||
videoTitleRef.current.replaceAll(' ', '').toLowerCase() &&
|
||||
(videoYearRef.current
|
||||
? result.year.toLowerCase() === videoYearRef.current.toLowerCase()
|
||||
: true) &&
|
||||
(searchType
|
||||
? // openlist 源跳过 episodes 长度检查,因为搜索时不返回详细播放列表
|
||||
result.source === 'openlist' ||
|
||||
(searchType === 'tv' && result.episodes.length > 1) ||
|
||||
(searchType === 'movie' && result.episodes.length === 1)
|
||||
: true)
|
||||
);
|
||||
|
||||
setAvailableSources(results);
|
||||
return results;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Play] 读取缓存失败:', error);
|
||||
// 继续执行 API 调用
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有缓存,调用 API
|
||||
const response = await fetch(
|
||||
`/api/search?q=${encodeURIComponent(query.trim())}`
|
||||
);
|
||||
@@ -1975,7 +2013,7 @@ function PlayPageClient() {
|
||||
const data = await response.json();
|
||||
|
||||
// 处理搜索结果,根据规则过滤
|
||||
const results = data.results.filter(
|
||||
results = data.results.filter(
|
||||
(result: SearchResult) =>
|
||||
result.title.replaceAll(' ', '').toLowerCase() ===
|
||||
videoTitleRef.current.replaceAll(' ', '').toLowerCase() &&
|
||||
@@ -2014,23 +2052,53 @@ function PlayPageClient() {
|
||||
: '🔍 正在搜索播放源...'
|
||||
);
|
||||
|
||||
let sourcesInfo = await fetchSourcesData(searchTitle || videoTitle);
|
||||
if (
|
||||
currentSource &&
|
||||
currentId &&
|
||||
!sourcesInfo.some(
|
||||
(source) => source.source === currentSource && source.id === currentId
|
||||
)
|
||||
) {
|
||||
sourcesInfo = await fetchSourceDetail(currentSource, currentId);
|
||||
// 如果已经有了source和id,优先通过单个详情接口快速获取
|
||||
let detailData: SearchResult | null = null;
|
||||
let sourcesInfo: SearchResult[] = [];
|
||||
|
||||
if (currentSource && currentId) {
|
||||
// 先快速获取当前源的详情
|
||||
try {
|
||||
const currentSourceDetail = await fetchSourceDetail(currentSource, currentId);
|
||||
if (currentSourceDetail.length > 0) {
|
||||
detailData = currentSourceDetail[0];
|
||||
sourcesInfo = currentSourceDetail;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取当前源详情失败:', err);
|
||||
}
|
||||
|
||||
// 异步获取其他源信息,不阻塞播放
|
||||
setBackgroundSourcesLoading(true);
|
||||
fetchSourcesData(searchTitle || videoTitle).then((sources) => {
|
||||
// 合并当前源和搜索到的其他源
|
||||
const allSources = [...sourcesInfo];
|
||||
sources.forEach((source) => {
|
||||
// 避免重复添加当前源
|
||||
if (!(source.source === currentSource && source.id === currentId)) {
|
||||
allSources.push(source);
|
||||
}
|
||||
});
|
||||
setAvailableSources(allSources);
|
||||
setBackgroundSourcesLoading(false);
|
||||
}).catch((err) => {
|
||||
console.error('异步获取其他源失败:', err);
|
||||
setBackgroundSourcesLoading(false);
|
||||
});
|
||||
} else {
|
||||
// 没有source和id,正常搜索流程
|
||||
sourcesInfo = await fetchSourcesData(searchTitle || videoTitle);
|
||||
}
|
||||
if (sourcesInfo.length === 0) {
|
||||
|
||||
if (!detailData && sourcesInfo.length === 0) {
|
||||
setError('未找到匹配结果');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let detailData: SearchResult = sourcesInfo[0];
|
||||
if (!detailData) {
|
||||
detailData = sourcesInfo[0];
|
||||
}
|
||||
// 指定源和id且无需优选
|
||||
if (currentSource && currentId && !needPreferRef.current) {
|
||||
const target = sourcesInfo.find(
|
||||
@@ -5057,7 +5125,7 @@ function PlayPageClient() {
|
||||
<div className='mt-3 px-2 lg:flex-shrink-0'>
|
||||
<div className='bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm rounded-lg p-2 border border-gray-200/50 dark:border-gray-700/50 w-full lg:w-auto overflow-x-auto'>
|
||||
<div className='flex gap-1.5 flex-nowrap lg:flex-wrap items-center'>
|
||||
<div className='flex gap-1.5 flex-nowrap lg:flex-wrap'>
|
||||
<div className='flex gap-1.5 flex-nowrap lg:flex-wrap lg:justify-end lg:flex-1'>
|
||||
{/* 下载按钮 */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -5306,6 +5374,7 @@ function PlayPageClient() {
|
||||
availableSources={availableSources}
|
||||
sourceSearchLoading={sourceSearchLoading}
|
||||
sourceSearchError={sourceSearchError}
|
||||
backgroundSourcesLoading={backgroundSourcesLoading}
|
||||
precomputedVideoInfo={precomputedVideoInfo}
|
||||
onDanmakuSelect={handleDanmakuSelect}
|
||||
currentDanmakuSelection={currentDanmakuSelection}
|
||||
|
||||
@@ -44,6 +44,8 @@ interface EpisodeSelectorProps {
|
||||
availableSources?: SearchResult[];
|
||||
sourceSearchLoading?: boolean;
|
||||
sourceSearchError?: string | null;
|
||||
/** 后台源加载状态 */
|
||||
backgroundSourcesLoading?: boolean;
|
||||
/** 预计算的测速结果,避免重复测速 */
|
||||
precomputedVideoInfo?: Map<string, VideoInfo>;
|
||||
/** 弹幕相关 */
|
||||
@@ -73,6 +75,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
availableSources = [],
|
||||
sourceSearchLoading = false,
|
||||
sourceSearchError = null,
|
||||
backgroundSourcesLoading = false,
|
||||
precomputedVideoInfo,
|
||||
onDanmakuSelect,
|
||||
currentDanmakuSelection,
|
||||
@@ -296,6 +299,42 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
// 依赖项保持与之前一致
|
||||
}, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
|
||||
|
||||
// 监听后台加载完成,触发自动测速
|
||||
const prevBackgroundLoadingRef = useRef<boolean>(false);
|
||||
useEffect(() => {
|
||||
// 当后台加载从 true 变为 false 时(即加载完成)
|
||||
if (prevBackgroundLoadingRef.current && !backgroundSourcesLoading) {
|
||||
// 如果当前选项卡在换源位置,触发测速
|
||||
if (activeTab === 'sources' && optimizationEnabled && currentSource !== 'openlist') {
|
||||
// 筛选出尚未测速的播放源
|
||||
const pendingSources = availableSources.filter((source) => {
|
||||
const sourceKey = `${source.source}-${source.id}`;
|
||||
return !attemptedSourcesRef.current.has(sourceKey);
|
||||
});
|
||||
|
||||
if (pendingSources.length > 0) {
|
||||
const batchSize = Math.ceil(pendingSources.length / 2);
|
||||
|
||||
const fetchInBatches = async () => {
|
||||
for (let start = 0; start < pendingSources.length; start += batchSize) {
|
||||
const batch = pendingSources.slice(start, start + batchSize);
|
||||
await Promise.all(batch.map(getVideoInfo));
|
||||
}
|
||||
|
||||
if (!initialTestingCompleted) {
|
||||
setInitialTestingCompleted(true);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInBatches();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新前一次的加载状态
|
||||
prevBackgroundLoadingRef.current = backgroundSourcesLoading;
|
||||
}, [backgroundSourcesLoading, activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
|
||||
|
||||
// 升序分页标签
|
||||
const categoriesAsc = useMemo(() => {
|
||||
return Array.from({ length: pageCount }, (_, i) => {
|
||||
@@ -881,6 +920,15 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 后台加载提示 */}
|
||||
{backgroundSourcesLoading && (
|
||||
<div className='flex items-center justify-center py-6 border-t border-gray-300 dark:border-gray-700'>
|
||||
<div className='animate-spin rounded-full h-6 w-6 border-b-2 border-green-500'></div>
|
||||
<span className='ml-2 text-sm text-gray-600 dark:text-gray-300'>
|
||||
正在加载更多播放源...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex-shrink-0 mt-auto pt-2 border-t border-gray-400 dark:border-gray-700'>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
Reference in New Issue
Block a user