161161 </n-flex >
162162 </template >
163163 </n-modal >
164+
165+ <!-- 批量下载进度通知 -->
166+ <Teleport to =" body" >
167+ <Transition name =" slide-fade" >
168+ <div
169+ v-if =" batchDownloadState.isDownloading"
170+ class =" batch-download-notification"
171+ :class =" { 'is-hovered': isNotificationHovered }"
172+ @mouseenter =" isNotificationHovered = true"
173+ @mouseleave =" isNotificationHovered = false"
174+ >
175+ <n-card content-style =" padding: 16px;" :bordered =" false" >
176+ <n-flex vertical :size =" 12" >
177+ <n-flex justify =" space-between" align =" center" >
178+ <n-text strong >批量下载中</n-text >
179+ <n-text depth =" 3" style =" font-size : 12px " >
180+ {{ batchDownloadState.processed }}/{{ batchDownloadState.total }}
181+ </n-text >
182+ </n-flex >
183+
184+ <n-text style =" font-size : 13px " ellipsis >
185+ 正在下载: {{ batchDownloadState.currentSong }}
186+ </n-text >
187+
188+ <n-progress
189+ type =" line"
190+ :percentage =" batchDownloadState.percent"
191+ :show-indicator =" false"
192+ processing
193+ status =" success"
194+ style =" height : 6px "
195+ />
196+
197+ <n-flex justify =" space-between" style =" font-size : 12px " >
198+ <n-text depth =" 3" >
199+ {{ batchDownloadState.transferred }} / {{ batchDownloadState.totalSize }}
200+ </n-text >
201+ <n-text depth =" 3" >
202+ 成功: {{ batchDownloadState.success }}
203+ </n-text >
204+ </n-flex >
205+ </n-flex >
206+ </n-card >
207+ </div >
208+ </Transition >
209+ </Teleport >
164210 </div >
165211</template >
166212
@@ -180,6 +226,10 @@ import {
180226 NInputGroup ,
181227 NButton ,
182228 NAlert ,
229+ NCard ,
230+ NProgress ,
231+ NText ,
232+ NFlex ,
183233} from " naive-ui" ;
184234import { useLocalStore , useSettingStore } from " @/stores" ;
185235import { isElectron } from " @/utils/env" ;
@@ -219,6 +269,19 @@ const showQualityModal = ref(false);
219269const selectedQuality = ref <SongLevelType >(" h" );
220270const downloadPath = ref <string >(settingStore .downloadPath );
221271
272+ // 批量下载状态
273+ const batchDownloadState = reactive ({
274+ isDownloading: false ,
275+ total: 0 ,
276+ processed: 0 ,
277+ success: 0 ,
278+ currentSong: " " ,
279+ percent: 0 ,
280+ transferred: " 0MB" ,
281+ totalSize: " 0MB" ,
282+ });
283+ const isNotificationHovered = ref (false );
284+
222285// 音质选项
223286const qualityOptions = computed (() => {
224287 // 批量下载时,默认显示所有常用音质选项
@@ -293,7 +356,6 @@ const tableCheck = (keys: DataTableRowKey[]) => {
293356 checkSongData .value = selectedRows .map ((row ) => row .origin ).filter ((song ) => song ) as SongType [];
294357};
295358
296- // 范围选择处理
297359// 范围选择处理
298360const handleRangeSelect = () => {
299361 if (startRange .value === null || endRange .value === null ) {
@@ -411,21 +473,24 @@ const startBatchDownload = () => {
411473const executeBatchDownload = async (songs : SongType []) => {
412474 if (! songs .length ) return ;
413475
414- const total = songs .length ;
415- let processed = 0 ;
416- let successCount = 0 ;
476+ // 重置状态
477+ batchDownloadState .isDownloading = true ;
478+ batchDownloadState .total = songs .length ;
479+ batchDownloadState .processed = 0 ;
480+ batchDownloadState .success = 0 ;
481+ batchDownloadState .percent = 0 ;
482+ batchDownloadState .transferred = " 0MB" ;
483+ batchDownloadState .totalSize = " 0MB" ;
484+
417485 let failCount = 0 ;
418486 const failedSongs: SongType [] = [];
419487
420- const loadingMsg = window .$message .loading (` 正在准备批量下载... 0/${total } ` , { duration: 0 });
421-
422488 // 监听下载进度
423489 const onProgress = (_event : any , progress : { percent: number ; transferredBytes: number ; totalBytes: number }) => {
424490 const { percent, transferredBytes, totalBytes } = progress ;
425- const percentStr = (percent * 100 ).toFixed (0 ) + " %" ;
426- const transferredStr = (transferredBytes / 1024 / 1024 ).toFixed (2 ) + " MB" ;
427- const totalStr = (totalBytes / 1024 / 1024 ).toFixed (2 ) + " MB" ;
428- loadingMsg .content = ` 正在批量下载... ${processed + 1 }/${total } (成功 ${successCount }) - ${percentStr } ${transferredStr }/${totalStr } ` ;
491+ batchDownloadState .percent = Number ((percent * 100 ).toFixed (0 ));
492+ batchDownloadState .transferred = (transferredBytes / 1024 / 1024 ).toFixed (2 ) + " MB" ;
493+ batchDownloadState .totalSize = (totalBytes / 1024 / 1024 ).toFixed (2 ) + " MB" ;
429494 };
430495
431496 if (isElectron ) {
@@ -434,15 +499,24 @@ const executeBatchDownload = async (songs: SongType[]) => {
434499
435500 try {
436501 for (const song of songs ) {
502+ batchDownloadState .currentSong = song .name ;
437503 try {
438504 const result = await downloadSong ({
439505 song ,
440506 quality: selectedQuality .value ,
441507 downloadPath: downloadPath .value ,
508+ skipIfExist: true ,
442509 });
443510
444511 if (result .success ) {
445- successCount ++ ;
512+ batchDownloadState .success ++ ;
513+ if (result .skipped ) {
514+ window .$notification .create ({
515+ title: " 已跳过重复文件" ,
516+ content: ` ${song .name } 已存在 ` ,
517+ duration: 2000 ,
518+ });
519+ }
446520 if (! isElectron ) {
447521 // Browser download delay
448522 await new Promise ((resolve ) => setTimeout (resolve , 500 ));
@@ -457,33 +531,41 @@ const executeBatchDownload = async (songs: SongType[]) => {
457531 failCount ++ ;
458532 failedSongs .push (song );
459533 } finally {
460- processed ++ ;
461- loadingMsg .content = ` 正在批量下载... ${processed }/${total } (成功 ${successCount }) ` ;
534+ batchDownloadState .processed ++ ;
535+ // Reset progress for next song
536+ batchDownloadState .percent = 0 ;
537+ batchDownloadState .transferred = " 0MB" ;
538+ batchDownloadState .totalSize = " 0MB" ;
462539 }
463540 }
464541
465542 if (failCount > 0 ) {
466543 window .$dialog .warning ({
467544 title: " 下载完成,但有部分失败" ,
468- content: ` ${successCount } 首下载成功,${failCount } 首下载失败。是否重试失败的歌曲? ` ,
545+ content : () => h (" div" , [
546+ h (" div" , { style: " margin-bottom: 10px" }, ` ${batchDownloadState .success } 首下载成功,${failCount } 首下载失败。 ` ),
547+ h (" div" , { style: " max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.05); padding: 8px; border-radius: 4px;" }, [
548+ h (" div" , { style: " font-weight: bold; margin-bottom: 4px" }, " 失败列表:" ),
549+ ... failedSongs .map (s => h (" div" , { style: " font-size: 12px" }, ` ${s .name } - ${isArray (s .artists ) ? s .artists [0 ]?.name : s .artists || ' 未知歌手' } ` ))
550+ ])
551+ ]),
469552 positiveText: " 重试失败歌曲" ,
470553 negativeText: " 取消" ,
471554 onPositiveClick : () => {
472555 executeBatchDownload (failedSongs );
473556 },
474557 });
475558 } else {
476- window .$message .success (` 批量下载完成,共 ${successCount } 首 ` );
559+ window .$message .success (` 批量下载完成,共 ${batchDownloadState . success } 首 ` );
477560 }
478561 } catch (error ) {
479562 console .error (" Batch download error:" , error );
480563 window .$message .error (" 批量下载过程中出现错误" );
481- window .$message .error (" 批量下载过程中出现错误" );
482564 } finally {
483565 if (isElectron ) {
484566 window .electron .ipcRenderer .removeListener (" download-progress" , onProgress );
485567 }
486- loadingMsg . destroy () ;
568+ batchDownloadState . isDownloading = false ;
487569 }
488570};
489571 </script >
@@ -495,4 +577,35 @@ const executeBatchDownload = async (songs: SongType[]) => {
495577.range-input {
496578 width : 100px ;
497579}
580+
581+ .batch-download-notification {
582+ position : fixed ;
583+ bottom : 24px ;
584+ right : 24px ;
585+ z-index : 9999 ;
586+ width : 320px ;
587+ transition : all 0.3s ease ;
588+
589+ & .is-hovered {
590+ opacity : 0 ;
591+ transform : translateY (10px );
592+ pointer-events : none ;
593+ }
594+
595+ :deep (.n-card ) {
596+ box-shadow : 0 8px 32px rgba (0 , 0 , 0 , 0.12 );
597+ border : 1px solid rgba (255 , 255 , 255 , 0.1 );
598+ }
599+ }
600+
601+ .slide-fade-enter-active ,
602+ .slide-fade-leave-active {
603+ transition : all 0.3s ease ;
604+ }
605+
606+ .slide-fade-enter-from ,
607+ .slide-fade-leave-to {
608+ transform : translateY (20px );
609+ opacity : 0 ;
610+ }
498611 </style >
0 commit comments