11<template >
22 <div class =" download-page h-full w-full bg-white dark:bg-black transition-colors duration-500" >
3- <n-scrollbar class =" h-full" >
4- <div class =" download-content pb-32 " >
3+ <n-scrollbar ref = " scrollbarRef " class =" h-full" @scroll = " handleDownloadScroll " >
4+ <div class =" download-content" :style = " { paddingBottom: contentPaddingBottom } " >
55 <!-- Hero Section -->
66 <section class =" hero-section relative overflow-hidden rounded-tl-2xl" >
77 <!-- Background with Blur -->
210210 </p >
211211 </div >
212212 <div v-else class =" space-y-2" >
213- <div
214- v-for =" (item, index) in downloadStore.completedList"
215- :key =" item.path || item.filePath"
216- class =" downloaded-item group animate-item p-3 rounded-2xl flex items-center gap-4 hover:bg-neutral-100 dark:hover:bg-neutral-900 transition-all"
217- :style =" { animationDelay: `${index * 0.03}s` }"
218- >
213+ <div class =" downloaded-list-section" >
219214 <div
220- class =" relative w-12 h-12 rounded-xl overflow-hidden shadow-lg flex-shrink-0"
215+ v-for =" (item, index) in renderedDownloaded"
216+ :key =" item.path || item.filePath"
217+ class =" downloaded-item group p-3 rounded-2xl flex items-center gap-4 hover:bg-neutral-100 dark:hover:bg-neutral-900 transition-all"
218+ :class =" { 'animate-item': index < 20 }"
219+ :style =" index < 20 ? { animationDelay: `${index * 0.03}s` } : undefined"
221220 >
222- <img
223- :src =" getImgUrl(item.picUrl, '100y100')"
224- class =" w-full h-full object-cover"
225- @error =" handleCoverError"
226- />
227221 <div
228- class =" absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-all cursor-pointer"
229- @click =" handlePlayMusic(item)"
222+ class =" relative w-12 h-12 rounded-xl overflow-hidden shadow-lg flex-shrink-0"
230223 >
231- <i class =" ri-play-fill text-white text-xl" />
232- </div >
233- </div >
234-
235- <div class =" flex-1 min-w-0" >
236- <div class =" flex items-center gap-2" >
237- <span class =" text-sm font-bold text-neutral-900 dark:text-white truncate" >{{
238- item.displayName || item.filename
239- }}</span >
240- <span class =" text-xs text-neutral-400 flex-shrink-0" >{{
241- formatSize(item.size)
242- }}</span >
243- </div >
244- <div class =" flex items-center gap-4 mt-1" >
245- <span class =" text-xs text-neutral-500 truncate max-w-[150px]" >{{
246- item.ar?.map((a) => a.name).join(', ')
247- }}</span >
224+ <img
225+ :src =" getImgUrl(item.picUrl, '100y100')"
226+ class =" w-full h-full object-cover"
227+ @error =" handleCoverError"
228+ />
248229 <div
249- class =" hidden md:flex items-center gap-1 text-[10px] text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-2 py-0.5 rounded-full truncate"
230+ class =" absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-all cursor-pointer"
231+ @click =" handlePlayMusic(item)"
250232 >
251- <i class =" ri-folder-line" />
252- <span class =" truncate" >{{ shortenPath(item.path || item.filePath) }}</span >
233+ <i class =" ri-play-fill text-white text-xl" />
253234 </div >
254235 </div >
255- </div >
256236
257- <div class =" flex items-center gap-1" >
258- <n-tooltip trigger =" hover" >
259- <template #trigger >
260- <button
261- class =" w-8 h-8 rounded-full flex items-center justify-center text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
262- @click =" copyPath(item.path || item.filePath)"
263- >
264- <i class =" ri-file-copy-line" />
265- </button >
266- </template >
267- {{ t('download.path.copy') || '复制路径' }}
268- </n-tooltip >
269- <n-tooltip trigger =" hover" >
270- <template #trigger >
271- <button
272- class =" w-8 h-8 rounded-full flex items-center justify-center text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
273- @click =" openDirectory(item.path || item.filePath)"
274- >
275- <i class =" ri-folder-open-line" />
276- </button >
277- </template >
278- {{ t('download.settingsPanel.open') }}
279- </n-tooltip >
280- <n-tooltip trigger =" hover" >
281- <template #trigger >
282- <button
283- class =" w-8 h-8 rounded-full flex items-center justify-center text-neutral-400 hover:text-red-500 hover:bg-red-500/10 transition-all"
284- @click =" handleDelete(item)"
237+ <div class =" flex-1 min-w-0" >
238+ <div class =" flex items-center gap-2" >
239+ <span class =" text-sm font-bold text-neutral-900 dark:text-white truncate" >{{
240+ item.displayName || item.filename
241+ }}</span >
242+ <span class =" text-xs text-neutral-400 flex-shrink-0" >{{
243+ formatSize(item.size)
244+ }}</span >
245+ </div >
246+ <div class =" flex items-center gap-4 mt-1" >
247+ <span class =" text-xs text-neutral-500 truncate max-w-[150px]" >{{
248+ item.ar?.map((a) => a.name).join(', ')
249+ }}</span >
250+ <div
251+ class =" hidden md:flex items-center gap-1 text-[10px] text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-2 py-0.5 rounded-full truncate"
285252 >
286- <i class =" ri-delete-bin-line" />
287- </button >
288- </template >
289- {{ t('common.delete') }}
290- </n-tooltip >
253+ <i class =" ri-folder-line" />
254+ <span class =" truncate" >{{
255+ shortenPath(item.path || item.filePath)
256+ }}</span >
257+ </div >
258+ </div >
259+ </div >
260+
261+ <div class =" flex items-center gap-1" >
262+ <n-tooltip trigger =" hover" >
263+ <template #trigger >
264+ <button
265+ class =" w-8 h-8 rounded-full flex items-center justify-center text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
266+ @click =" copyPath(item.path || item.filePath)"
267+ >
268+ <i class =" ri-file-copy-line" />
269+ </button >
270+ </template >
271+ {{ t('download.path.copy') || '复制路径' }}
272+ </n-tooltip >
273+ <n-tooltip trigger =" hover" >
274+ <template #trigger >
275+ <button
276+ class =" w-8 h-8 rounded-full flex items-center justify-center text-neutral-400 hover:text-primary hover:bg-primary/10 transition-all"
277+ @click =" openDirectory(item.path || item.filePath)"
278+ >
279+ <i class =" ri-folder-open-line" />
280+ </button >
281+ </template >
282+ {{ t('download.settingsPanel.open') }}
283+ </n-tooltip >
284+ <n-tooltip trigger =" hover" >
285+ <template #trigger >
286+ <button
287+ class =" w-8 h-8 rounded-full flex items-center justify-center text-neutral-400 hover:text-red-500 hover:bg-red-500/10 transition-all"
288+ @click =" handleDelete(item)"
289+ >
290+ <i class =" ri-delete-bin-line" />
291+ </button >
292+ </template >
293+ {{ t('common.delete') }}
294+ </n-tooltip >
295+ </div >
291296 </div >
292297 </div >
298+ <!-- 未渲染项占位 -->
299+ <div
300+ v-if =" downloadedPlaceholderHeight > 0"
301+ :style =" { height: downloadedPlaceholderHeight + 'px' }"
302+ />
293303 </div >
294304 </n-spin >
295305 </div >
@@ -540,6 +550,7 @@ import { useMessage } from 'naive-ui';
540550import { computed , onMounted , ref , watch } from ' vue' ;
541551import { useI18n } from ' vue-i18n' ;
542552
553+ import { useProgressiveRender } from ' @/hooks/useProgressiveRender' ;
543554import { useDownloadStore } from ' @/store/modules/download' ;
544555import { usePlayerStore } from ' @/store/modules/player' ;
545556import type { SongResult } from ' @/types/music' ;
@@ -551,8 +562,23 @@ const { t } = useI18n();
551562const playerStore = usePlayerStore ();
552563const downloadStore = useDownloadStore ();
553564const message = useMessage ();
565+ const scrollbarRef = ref ();
566+
567+ const completedList = computed (() => downloadStore .completedList );
568+
569+ const {
570+ renderedItems : renderedDownloaded,
571+ placeholderHeight : downloadedPlaceholderHeight,
572+ contentPaddingBottom,
573+ handleScroll : handleDownloadScroll
574+ } = useProgressiveRender ({
575+ items: completedList ,
576+ itemHeight: 72 ,
577+ listSelector: ' .downloaded-list-section' ,
578+ initialCount: 40
579+ });
554580
555- const tabName = ref (' downloading' );
581+ const tabName = ref (downloadStore . downloadingList . length > 0 ? ' downloading' : ' downloaded ' );
556582
557583// ── Status helpers ──────────────────────────────────────────────────────────
558584
0 commit comments