feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,637 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition duration-200 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6"
|
||||
@click.self="close"
|
||||
>
|
||||
<div class="absolute inset-0 bg-bg-primary-60 backdrop-blur-sm" />
|
||||
|
||||
<div
|
||||
class="relative w-full max-w-2xl bg-bg-secondary border border-border-secondary rounded-3xl shadow-2xl flex flex-col max-h-[90vh] overflow-hidden"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between p-8 pb-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
v-if="view === 'songs'"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-bg-tertiary-50 text-text-tertiary hover:bg-bg-tertiary hover:text-text-primary transition-all"
|
||||
@click="backToPlaylists"
|
||||
>
|
||||
<Icon name="arrow-left" :size="20" />
|
||||
</button>
|
||||
<h3
|
||||
class="text-xl font-black text-text-primary tracking-tight truncate max-w-[300px] sm:max-w-md"
|
||||
>
|
||||
{{ view === 'playlists' ? locale.selectPlaylist : selectedPlaylist?.name || locale.playlistDetails }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-bg-tertiary-50 text-text-tertiary hover:bg-bg-tertiary hover:text-text-primary transition-all"
|
||||
@click="close"
|
||||
>
|
||||
<Icon name="x" :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="flex-1 overflow-y-auto p-8 pt-4 custom-scrollbar">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex flex-col items-center justify-center py-20 text-text-tertiary"
|
||||
>
|
||||
<Icon name="loader" :size="48" class="mb-4 animate-spin text-text-tertiary" />
|
||||
<p class="font-medium">{{ locale.processing }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="flex flex-col items-center justify-center py-20 text-center px-8"
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 rounded-2xl bg-error-10 flex items-center justify-center mb-4"
|
||||
>
|
||||
<Icon name="alert-circle" :size="32" class="text-error" />
|
||||
</div>
|
||||
<p class="text-text-tertiary font-medium mb-6">{{ error }}</p>
|
||||
<button
|
||||
class="px-8 py-3 rounded-xl bg-bg-tertiary hover:bg-bg-quaternary text-text-primary text-xs font-black transition-all active:scale-95 uppercase tracking-widest"
|
||||
@click="retry"
|
||||
>
|
||||
{{ locale.retry }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 歌单列表视图 -->
|
||||
<div v-else-if="view === 'playlists'" class="playlist-list space-y-3">
|
||||
<div
|
||||
v-if="playlists.length === 0"
|
||||
class="flex flex-col items-center justify-center py-12 text-text-tertiary"
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 rounded-3xl bg-bg-tertiary-50 flex items-center justify-center mb-4"
|
||||
>
|
||||
<Icon name="music" :size="32" class="opacity-20" />
|
||||
</div>
|
||||
<p class="font-medium">{{ locale.emptyPlaylists }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="playlist in playlists"
|
||||
v-else
|
||||
:key="playlist.id"
|
||||
class="group flex items-center p-4 bg-bg-tertiary-30 border border-border-secondary-50 rounded-3xl hover:bg-bg-tertiary-50 hover:border-border-tertiary transition-all cursor-pointer"
|
||||
@click="selectPlaylist(playlist)"
|
||||
>
|
||||
<div class="w-16 h-16 rounded-2xl overflow-hidden bg-bg-tertiary mr-4 flex-shrink-0">
|
||||
<img
|
||||
v-if="playlist.coverImgUrl"
|
||||
:src="convertToHttps(playlist.coverImgUrl)"
|
||||
alt="cover"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center bg-bg-quaternary-50"
|
||||
>
|
||||
<Icon name="heart" :size="22" class="text-text-disabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4
|
||||
class="font-bold text-text-primary truncate group-hover:text-text-primary transition-colors"
|
||||
>
|
||||
{{ playlist.name }}
|
||||
</h4>
|
||||
<div class="flex items-center gap-3 mt-1 text-xs text-text-tertiary">
|
||||
<span v-if="playlist.trackCount != null" class="flex items-center">
|
||||
<span class="w-1 h-1 rounded-full bg-current mr-1.5 opacity-40" />
|
||||
{{ callLocale('songCount', `${playlist.trackCount} 首`, playlist.trackCount) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="playlist.creator?.nickname"
|
||||
class="flex items-center truncate"
|
||||
>
|
||||
<span class="w-1 h-1 rounded-full bg-current mr-1.5 opacity-40" />
|
||||
by {{ playlist.creator?.nickname }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 text-text-disabled group-hover:text-text-tertiary transition-colors">
|
||||
<Icon name="chevron-right" :size="24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 歌曲列表视图 -->
|
||||
<div v-else class="song-list space-y-3">
|
||||
<div
|
||||
v-if="songs.length === 0"
|
||||
class="flex flex-col items-center justify-center py-12 text-text-tertiary"
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 rounded-3xl bg-bg-tertiary-50 flex items-center justify-center mb-4"
|
||||
>
|
||||
<Icon name="music" :size="32" class="opacity-20" />
|
||||
</div>
|
||||
<p class="font-medium">{{ locale.emptyPlaylist }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="song in songs"
|
||||
v-else
|
||||
:key="song.id"
|
||||
class="group flex items-center p-4 bg-bg-tertiary-30 border border-border-secondary-50 rounded-3xl hover:bg-bg-tertiary-50 hover:border-border-tertiary transition-all"
|
||||
>
|
||||
<!-- 封面与播放叠加层 -->
|
||||
<div
|
||||
class="relative w-14 h-14 rounded-2xl overflow-hidden bg-bg-tertiary mr-4 flex-shrink-0 group/cover cursor-pointer"
|
||||
@click="playSong(song)"
|
||||
>
|
||||
<img
|
||||
:src="convertToHttps(song.al?.picUrl)"
|
||||
alt="cover"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-bg-primary-40 flex items-center justify-center opacity-0 group-hover/cover:opacity-100 transition-opacity"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-bg-secondary-20 backdrop-blur-md flex items-center justify-center"
|
||||
>
|
||||
<Icon name="play" :size="16" class="text-text-primary fill-current" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 歌曲信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4
|
||||
class="font-bold text-text-primary truncate group-hover:text-text-primary transition-colors"
|
||||
>
|
||||
{{ song.name }}
|
||||
</h4>
|
||||
<div class="flex items-center gap-2 mt-0.5 text-xs text-text-tertiary truncate">
|
||||
<span class="truncate">{{ song.ar?.map((a) => a.name).join('/') }}</span>
|
||||
<span v-if="song.al?.name" class="opacity-40 shrink-0">·</span>
|
||||
<span v-if="song.al?.name" class="truncate opacity-60">{{ song.al.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="ml-4 shrink-0 flex items-center gap-3">
|
||||
<div
|
||||
v-if="songsLoadingForSimilar"
|
||||
class="text-xs font-bold text-text-disabled animate-pulse"
|
||||
>
|
||||
{{ locale.processing }}
|
||||
</div>
|
||||
<div v-else-if="getSimilarSong(song)" class="flex flex-col items-end gap-1.5">
|
||||
<span
|
||||
v-if="getSimilarSong(song)?.played"
|
||||
class="px-2 py-0.5 rounded-md bg-error-10 text-error text-[10px] font-black uppercase tracking-wider"
|
||||
>
|
||||
{{ requestLocale.played }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="getSimilarSong(song)?.scheduled"
|
||||
class="px-2 py-0.5 rounded-md bg-warning-10 text-warning text-[10px] font-black uppercase tracking-wider"
|
||||
>
|
||||
{{ requestLocale.scheduled }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="px-2 py-0.5 rounded-md bg-bg-quaternary-50 text-text-tertiary text-[10px] font-black uppercase tracking-wider"
|
||||
>
|
||||
{{ locale.exists }}
|
||||
</span>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-if="getSimilarSong(song)?.played && isSuperAdmin"
|
||||
:disabled="submitting"
|
||||
class="px-3 py-1.5 rounded-xl bg-primary-hover hover:bg-primary text-text-primary text-xs font-black disabled:opacity-50 transition-all active:scale-95 uppercase tracking-widest"
|
||||
@click.stop="selectSong(song)"
|
||||
>
|
||||
{{ submitting && selectedSongId === song.id ? '...' : requestLocale.continueSubmit }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="px-3 py-1.5 rounded-xl text-[10px] font-black transition-all active:scale-95 disabled:cursor-not-allowed uppercase tracking-widest"
|
||||
:class="[
|
||||
getSimilarSong(song)?.voted
|
||||
? 'bg-error-10 text-error border border-error-20'
|
||||
: 'bg-bg-tertiary text-text-tertiary hover:bg-bg-quaternary hover:text-text-primary border border-border-tertiary-50 hover:border-border-tertiary'
|
||||
]"
|
||||
:disabled="
|
||||
getSimilarSong(song)?.played ||
|
||||
getSimilarSong(song)?.scheduled ||
|
||||
getSimilarSong(song)?.voted ||
|
||||
submitting
|
||||
"
|
||||
@click.stop="handleLike(getSimilarSong(song))"
|
||||
>
|
||||
{{ getSimilarSong(song)?.voted ? requestLocale.liked : requestLocale.like }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
:disabled="submitting || songsLoadingForSimilar"
|
||||
class="px-4 py-2 rounded-xl bg-primary-hover hover:bg-primary text-text-primary text-xs font-black disabled:opacity-50 transition-all active:scale-95 shrink-0 uppercase tracking-widest shadow-lg shadow-[var(--primary-glow)]"
|
||||
@click.stop="selectSong(song)"
|
||||
>
|
||||
{{ submitting && selectedSongId === song.id ? requestLocale.submitting : requestLocale.chooseSubmit }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多按钮 -->
|
||||
<div v-if="hasMore" class="pt-6 pb-2 flex justify-center">
|
||||
<button
|
||||
:disabled="moreLoading"
|
||||
class="px-8 py-3 rounded-xl bg-bg-tertiary hover:bg-bg-quaternary text-text-secondary text-xs font-black disabled:opacity-50 transition-all flex items-center gap-2 uppercase tracking-widest"
|
||||
@click="loadMore"
|
||||
>
|
||||
<Icon v-if="moreLoading" name="loader" :size="16" class="animate-spin" />
|
||||
{{ moreLoading ? locale.loadingMore : locale.loadMore }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { getPlaylistTracks, getUserPlaylists } from '~/utils/neteaseApi'
|
||||
import {
|
||||
getQqPlaylistSongs,
|
||||
getQqUserPlaylists,
|
||||
QQ_FAV_PLAYLIST_ID
|
||||
} from '~/utils/qqUserLibrary'
|
||||
import { convertToHttps } from '~/utils/url'
|
||||
import Icon from '../UI/Icon.vue'
|
||||
import { useSongs } from '~/composables/useSongs'
|
||||
import { useAuth } from '~/composables/useAuth'
|
||||
import { useSemesters } from '~/composables/useSemesters'
|
||||
import { useLocale } from '~/utils/locale'
|
||||
import { normalizeForMatch as normalizeString } from '~/utils/song-name-normalize'
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
cookie: String,
|
||||
uid: [String, Number],
|
||||
platform: { type: String, default: 'netease' }
|
||||
})
|
||||
|
||||
const { songs: songsLocale } = useLocale()
|
||||
const requestLocale = computed(() => songsLocale.value?.requestForm || {})
|
||||
const locale = computed(() => requestLocale.value?.playlistModal || {})
|
||||
const { t: callLocale } = useLocaleText(locale)
|
||||
|
||||
const emit = defineEmits(['close', 'submit', 'play'])
|
||||
|
||||
const isTencentPlatform = computed(() => props.platform === 'tencent')
|
||||
|
||||
const view = ref('playlists') // 'playlists' | 'songs'
|
||||
const playlists = ref([])
|
||||
const songs = ref([])
|
||||
const selectedPlaylist = ref(null)
|
||||
const loading = ref(false)
|
||||
const songsLoadingForSimilar = ref(false)
|
||||
const error = ref('')
|
||||
const submitting = ref(false)
|
||||
const selectedSongId = ref(null)
|
||||
|
||||
// 分页状态
|
||||
const limit = ref(100)
|
||||
const offset = ref(0)
|
||||
const hasMore = ref(true)
|
||||
const moreLoading = ref(false)
|
||||
|
||||
const songService = useSongs()
|
||||
const auth = useAuth()
|
||||
const { currentSemester, fetchCurrentSemester } = useSemesters()
|
||||
const isSuperAdmin = computed(() => auth.user.value?.role === 'SUPER_ADMIN')
|
||||
|
||||
// 检查是否已存在相似歌曲
|
||||
const getSimilarSong = (songData) => {
|
||||
if (!songData) return null
|
||||
|
||||
const title = songData.name
|
||||
const artist = songData.ar?.map((a) => a.name).join('/')
|
||||
|
||||
if (!title || !artist) return null
|
||||
|
||||
const normalizedTitle = normalizeString(title)
|
||||
const normalizedArtist = normalizeString(artist)
|
||||
const currentSemesterName = currentSemester.value?.name
|
||||
|
||||
// 如果没有获取到当前学期信息,暂时不进行检查,避免误报
|
||||
if (!currentSemesterName) return null
|
||||
|
||||
return songService.songs.value.find((song) => {
|
||||
const songTitle = normalizeString(song.title)
|
||||
const songArtist = normalizeString(song.artist)
|
||||
const titleMatch = songTitle === normalizedTitle && songArtist === normalizedArtist
|
||||
|
||||
// 必须匹配当前学期
|
||||
return titleMatch && song.semester === currentSemesterName
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUserPlaylists = async () => {
|
||||
if (isTencentPlatform.value ? !props.cookie : (!props.cookie || !props.uid)) return
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
if (isTencentPlatform.value) {
|
||||
const list = await getQqUserPlaylists(props.cookie)
|
||||
// 首部固定注入"我喜欢"入口,歌曲经 playlist-songs 专用链路获取
|
||||
list.unshift({
|
||||
id: QQ_FAV_PLAYLIST_ID,
|
||||
name: locale.value.favSongs || 'Liked Songs',
|
||||
cover: '',
|
||||
count: null,
|
||||
origin: 'liked'
|
||||
})
|
||||
if (list.length > 0) {
|
||||
// 补齐歌单卡片模板所需字段
|
||||
playlists.value = list.map((item) => ({
|
||||
...item,
|
||||
coverImgUrl: item.cover,
|
||||
trackCount: item.count
|
||||
}))
|
||||
} else {
|
||||
error.value = locale.value.fetchPlaylistsFailed
|
||||
}
|
||||
} else {
|
||||
const { code, body, message } = await getUserPlaylists(props.uid, props.cookie)
|
||||
if (code === 200 && body && body.playlist) {
|
||||
playlists.value = body.playlist
|
||||
} else {
|
||||
error.value = message || locale.value.fetchPlaylistsFailed
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = locale.value.networkFailed
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPlaylistSongs = async (playlistId, isLoadMore = false) => {
|
||||
if (isTencentPlatform.value ? !props.cookie : !props.cookie) return
|
||||
|
||||
if (isLoadMore) {
|
||||
moreLoading.value = true
|
||||
} else {
|
||||
loading.value = true
|
||||
songs.value = []
|
||||
offset.value = 0
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
// 当前偏移逻辑:
|
||||
// 首次加载: offset=0, limit=100. 下次加载: offset=100, limit=100
|
||||
// 注意: API 偏移参数如果遵循文档逻辑,应作为页面偏移?
|
||||
// "传递 limit=50&offset=0 得到 1-50。传递 limit=50&offset=50 得到 51-100"
|
||||
// 所以 offset 变量应该跟踪目前为止加载的歌曲数量。
|
||||
|
||||
if (isTencentPlatform.value) {
|
||||
const result = await getQqPlaylistSongs(playlistId, props.cookie, limit.value, offset.value)
|
||||
if (isLoadMore) {
|
||||
songs.value = [...songs.value, ...result.songs]
|
||||
} else {
|
||||
songs.value = result.songs
|
||||
}
|
||||
|
||||
offset.value += result.songs.length
|
||||
hasMore.value = result.hasMore
|
||||
} else {
|
||||
const { code, body, message } = await getPlaylistTracks(
|
||||
playlistId,
|
||||
limit.value,
|
||||
offset.value,
|
||||
props.cookie
|
||||
)
|
||||
if (code === 200 && body && body.songs) {
|
||||
if (isLoadMore) {
|
||||
songs.value = [...songs.value, ...body.songs]
|
||||
} else {
|
||||
songs.value = body.songs
|
||||
}
|
||||
|
||||
// 为下次调用更新偏移
|
||||
offset.value += body.songs.length
|
||||
|
||||
// 检查是否还有更多歌曲
|
||||
if (body.songs.length < limit.value) {
|
||||
hasMore.value = false
|
||||
}
|
||||
} else {
|
||||
error.value = message || locale.value.fetchSongsFailed
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = locale.value.networkFailed
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
moreLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
if (selectedPlaylist.value) {
|
||||
fetchPlaylistSongs(selectedPlaylist.value.id, true)
|
||||
}
|
||||
}
|
||||
|
||||
const selectPlaylist = (playlist) => {
|
||||
selectedPlaylist.value = playlist
|
||||
view.value = 'songs'
|
||||
fetchPlaylistSongs(playlist.id)
|
||||
}
|
||||
|
||||
const backToPlaylists = () => {
|
||||
view.value = 'playlists'
|
||||
selectedPlaylist.value = null
|
||||
songs.value = []
|
||||
offset.value = 0
|
||||
hasMore.value = true
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
const retry = () => {
|
||||
if (view.value === 'playlists') {
|
||||
fetchUserPlaylists()
|
||||
} else if (selectedPlaylist.value) {
|
||||
// 重新从头加载歌曲
|
||||
fetchPlaylistSongs(selectedPlaylist.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
view.value = 'playlists'
|
||||
playlists.value = []
|
||||
songs.value = []
|
||||
selectedPlaylist.value = null
|
||||
offset.value = 0
|
||||
hasMore.value = true
|
||||
|
||||
// 确保当前学期已加载,用于正确检查歌曲状态
|
||||
if (!currentSemester.value) {
|
||||
await fetchCurrentSemester()
|
||||
}
|
||||
|
||||
fetchUserPlaylists()
|
||||
|
||||
// 加载歌曲列表以便检查相似歌曲
|
||||
if (
|
||||
auth.isAuthenticated.value &&
|
||||
(!songService.songs.value || songService.songs.value.length === 0)
|
||||
) {
|
||||
songsLoadingForSimilar.value = true
|
||||
const currentSemesterName = currentSemester.value?.name
|
||||
songService
|
||||
.fetchSongs(true, currentSemesterName)
|
||||
.catch((err) => {
|
||||
console.error('加载歌曲列表失败:', err)
|
||||
})
|
||||
.finally(() => {
|
||||
songsLoadingForSimilar.value = false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 重置状态
|
||||
submitting.value = false
|
||||
selectedSongId.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.uid,
|
||||
(newVal) => {
|
||||
if (props.show && newVal) {
|
||||
fetchUserPlaylists()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleLike = async (song) => {
|
||||
if (!song || song.voted) return
|
||||
|
||||
if (song.played || song.scheduled) {
|
||||
if (window.$showNotification) {
|
||||
const message = song.played
|
||||
? (requestLocale.value.playedCannotLike || '已播放的歌曲不能点赞')
|
||||
: (requestLocale.value.scheduledCannotLike || '已排期的歌曲不能点赞')
|
||||
window.$showNotification(message, 'warning')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await songService.voteSong(song.id)
|
||||
} catch (error) {
|
||||
console.error('点赞失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const playSong = (songData) => {
|
||||
emit('play', {
|
||||
id: songData.id,
|
||||
title: songData.name,
|
||||
artist: songData.ar?.map((a) => a.name).join('/'),
|
||||
cover: songData.al?.picUrl,
|
||||
albumId: songData.al?.id,
|
||||
musicPlatform: isTencentPlatform.value ? 'tencent' : 'netease',
|
||||
musicId: songData.id.toString(),
|
||||
sourceInfo: {
|
||||
source: isTencentPlatform.value ? 'tencent-library' : 'netease-backup',
|
||||
playlistId: selectedPlaylist.value?.id,
|
||||
type: 'song'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const selectSong = (songData) => {
|
||||
selectedSongId.value = songData.id
|
||||
submitting.value = true
|
||||
|
||||
const song = {
|
||||
id: songData.id,
|
||||
title: songData.name,
|
||||
artist: songData.ar?.map((a) => a.name).join('/'),
|
||||
cover: songData.al?.picUrl,
|
||||
album: songData.al?.name,
|
||||
albumId: songData.al?.id,
|
||||
duration: songData.dt,
|
||||
musicPlatform: isTencentPlatform.value ? 'tencent' : 'netease',
|
||||
musicId: songData.id.toString(),
|
||||
sourceInfo: {
|
||||
source: isTencentPlatform.value ? 'tencent-library' : 'netease-backup',
|
||||
playlistId: selectedPlaylist.value?.id,
|
||||
type: 'song'
|
||||
}
|
||||
}
|
||||
|
||||
emit('submit', song)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
resetSubmissionState: () => {
|
||||
submitting.value = false
|
||||
selectedSongId.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--overlay-10);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--overlay-20);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user