|
36 | 36 | </template>
|
37 | 37 | {{ t('comp.musicList.addToPlaylist') }}
|
38 | 38 | </n-tooltip>
|
| 39 | + |
| 40 | + <!-- 多选/下载操作 --> |
| 41 | + <div v-if="filteredSongs.length > 0" class="flex items-center gap-2"> |
| 42 | + <n-tooltip v-if="!isSelecting" placement="bottom" trigger="hover"> |
| 43 | + <template #trigger> |
| 44 | + <div class="action-button hover-green" @click="startSelect"> |
| 45 | + <i class="icon iconfont ri-checkbox-multiple-line"></i> |
| 46 | + </div> |
| 47 | + </template> |
| 48 | + {{ t('favorite.batchDownload')}} |
| 49 | + </n-tooltip> |
| 50 | + <div v-else class="flex items-center gap-2"> |
| 51 | + <n-checkbox |
| 52 | + :checked="isAllSelected" |
| 53 | + :indeterminate="isIndeterminate" |
| 54 | + @update:checked="handleSelectAll" |
| 55 | + > |
| 56 | + {{ t('common.selectAll') }} |
| 57 | + </n-checkbox> |
| 58 | + <n-tooltip placement="bottom" trigger="hover"> |
| 59 | + <template #trigger> |
| 60 | + <div |
| 61 | + class="action-button hover-green" |
| 62 | + :class="{ 'opacity-50 pointer-events-none': selectedSongs.length === 0 || isDownloading }" |
| 63 | + @click="selectedSongs.length && !isDownloading && handleBatchDownload()" |
| 64 | + > |
| 65 | + <i class="icon iconfont ri-download-line" :class="{ 'animate-spin': isDownloading }"></i> |
| 66 | + </div> |
| 67 | + </template> |
| 68 | + {{ t('favorite.download', { count: selectedSongs.length }) }} |
| 69 | + </n-tooltip> |
| 70 | + <n-tooltip placement="bottom" trigger="hover"> |
| 71 | + <template #trigger> |
| 72 | + <div class="action-button" @click="cancelSelect"> |
| 73 | + <i class="icon iconfont ri-close-line"></i> |
| 74 | + </div> |
| 75 | + </template> |
| 76 | + {{ t('common.cancel') }} |
| 77 | + </n-tooltip> |
| 78 | + </div> |
| 79 | + </div> |
| 80 | + |
39 | 81 | <!-- 布局切换按钮 -->
|
40 | 82 | <div class="layout-toggle" v-if="!isMobile">
|
41 | 83 | <n-tooltip placement="bottom" trigger="hover">
|
|
132 | 174 | :compact="isCompactLayout"
|
133 | 175 | :item="formatSong(item)"
|
134 | 176 | :can-remove="canRemove"
|
| 177 | + :selectable="isSelecting" |
| 178 | + :selected="selectedSongs.includes(item.id as number)" |
135 | 179 | @play="handlePlay"
|
136 | 180 | @remove-song="handleRemoveSong"
|
| 181 | + @select="(id, selected) => handleSelect(id, selected)" |
137 | 182 | />
|
138 | 183 | </div>
|
139 | 184 | </template>
|
@@ -166,6 +211,7 @@ import PlayBottom from '@/components/common/PlayBottom.vue';
|
166 | 211 | import { useMusicStore, usePlayerStore } from '@/store';
|
167 | 212 | import { SongResult } from '@/type/music';
|
168 | 213 | import { getImgUrl, isMobile, setAnimationClass } from '@/utils';
|
| 214 | +import { useDownload } from '@/hooks/useDownload'; |
169 | 215 |
|
170 | 216 | const { t } = useI18n();
|
171 | 217 | const route = useRoute();
|
@@ -838,6 +884,53 @@ const addToPlaylist = () => {
|
838 | 884 |
|
839 | 885 | message.success(t('comp.musicList.addToPlaylistSuccess', { count: newSongs.length }));
|
840 | 886 | };
|
| 887 | +
|
| 888 | +// 多选下载相关状态和方法 |
| 889 | +const isSelecting = ref(false); |
| 890 | +const selectedSongs = ref<number[]>([]); |
| 891 | +const { isDownloading, batchDownloadMusic } = useDownload(); |
| 892 | +
|
| 893 | +const startSelect = () => { |
| 894 | + isSelecting.value = true; |
| 895 | + selectedSongs.value = []; |
| 896 | +}; |
| 897 | +const cancelSelect = () => { |
| 898 | + isSelecting.value = false; |
| 899 | + selectedSongs.value = []; |
| 900 | +}; |
| 901 | +const handleSelect = (songId: number, selected: boolean) => { |
| 902 | + if (selected) { |
| 903 | + selectedSongs.value.push(songId); |
| 904 | + } else { |
| 905 | + selectedSongs.value = selectedSongs.value.filter((id) => id !== songId); |
| 906 | + } |
| 907 | +}; |
| 908 | +const isAllSelected = computed(() => { |
| 909 | + return ( |
| 910 | + filteredSongs.value.length > 0 && |
| 911 | + selectedSongs.value.length === filteredSongs.value.length |
| 912 | + ); |
| 913 | +}); |
| 914 | +const isIndeterminate = computed(() => { |
| 915 | + return ( |
| 916 | + selectedSongs.value.length > 0 && |
| 917 | + selectedSongs.value.length < filteredSongs.value.length |
| 918 | + ); |
| 919 | +}); |
| 920 | +const handleSelectAll = (checked: boolean) => { |
| 921 | + if (checked) { |
| 922 | + selectedSongs.value = filteredSongs.value.map((song) => song.id as number); |
| 923 | + } else { |
| 924 | + selectedSongs.value = []; |
| 925 | + } |
| 926 | +}; |
| 927 | +const handleBatchDownload = async () => { |
| 928 | + const selectedSongsList = selectedSongs.value |
| 929 | + .map((songId) => filteredSongs.value.find((s) => s.id === songId)) |
| 930 | + .filter((song) => song) as SongResult[]; |
| 931 | + await batchDownloadMusic(selectedSongsList); |
| 932 | + cancelSelect(); |
| 933 | +}; |
841 | 934 | </script>
|
842 | 935 |
|
843 | 936 | <style scoped lang="scss">
|
|
0 commit comments