Files
graduation-design/music-qianduan/src/components/MusicList.vue
ikmkj bd26319b91 feat(music): 添加点赞功能- 在 MusicList 组件中添加点赞按钮,用于切换歌曲点赞状态
- 在 MusicPlayer 组件中添加点赞按钮,用于切换当前播放歌曲的点赞状态
- 实现 toggleLike 方法,用于切换歌曲点赞状态并更新 UI
- 引入 Pointer 图标用于点赞按钮- 添加点赞相关的 API 调用
2025-07-26 00:13:50 +08:00

429 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="music-list">
<div class="list-header">
<div class="header-item index">#</div>
<div class="header-item title">歌曲</div>
<div class="header-item singer">歌手</div>
<div class="header-item album">专辑</div>
<div class="header-item duration">时长</div>
<div class="header-item actions">操作</div>
</div>
<el-empty v-if="!loading && (!list || list.length === 0)" description="暂无音乐" />
<el-skeleton v-if="loading" :rows="5" animated />
<template v-else>
<div
v-for="(item, index) in list"
:key="item.id"
class="list-item"
:class="{ 'active': isPlaying(item.id) }"
@dblclick="playMusic(item)"
>
<div class="item-index">
<span v-if="!isPlaying(item.id)">{{ index + 1 }}</span>
<el-icon v-else class="playing-icon"><VideoPlay /></el-icon>
</div>
<div class="item-title">
<div class="music-cover">
<img :src="item.cover || '/default-cover.jpg'" alt="封面">
</div>
<div class="music-info">
<div class="music-name">{{ item.name }}</div>
<div class="music-type" v-if="item.typeName">{{ item.typeName }}</div>
</div>
</div>
<div class="item-singer">
<template v-if="item.singerNames && item.singerNames.length">
<span v-for="(singer, idx) in item.singerNames" :key="idx">
{{ singer }}{{ idx < item.singerNames.length - 1 ? '' : '' }}
</span>
</template>
<span v-else>{{ item.singer || '未知歌手' }}</span>
</div>
<div class="item-album">{{ item.album || '-' }}</div>
<div class="item-duration">{{ formatDuration(item.duration) }}</div>
<div class="item-actions">
<el-button
type="primary"
circle
size="small"
@click="playMusic(item)"
:title="isPlaying(item.id) ? '暂停' : '播放'"
>
<el-icon>
<component :is="isPlaying(item.id) ? 'VideoPause' : 'VideoPlay'" />
</el-icon>
</el-button>
<el-button
:type="item.collected ? 'danger' : 'default'"
circle
size="small"
@click="toggleCollect(item)"
:title="item.collected ? '取消收藏' : '收藏'"
>
<el-icon><Star /></el-icon>
</el-button>
<el-button
:type="item.liked ? 'success' : 'default'"
circle
size="small"
@click="toggleLike(item)"
:title="item.liked ? '取消点赞' : '点赞'"
>
<el-icon><Pointer /></el-icon>
</el-button>
<el-button
circle
size="small"
@click="goToMusicDetail(item.id)"
title="查看详情"
>
<el-icon><InfoFilled /></el-icon>
</el-button>
<el-dropdown trigger="click" @command="handleCommand($event, item)">
<el-button circle size="small">
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="addToPlaylist">添加到播放列表</el-dropdown-item>
<el-dropdown-item command="playNext">下一首播放</el-dropdown-item>
<el-dropdown-item command="share" v-if="false">分享</el-dropdown-item>
<el-dropdown-item command="download" v-if="false">下载</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<div class="pagination-container" v-if="showPagination && total > 0">
<el-pagination
v-model:current-page="localCurrentPage"
v-model:page-size="localPageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { VideoPlay, VideoPause, Star, More, InfoFilled, Pointer } from '@element-plus/icons-vue'
import { usePlayerStore } from '../stores/player'
import { toggleCollectMusic, toggleLikeMusic } from '../api/music'
import { ElMessage } from 'element-plus'
const props = defineProps({
list: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
},
showPagination: {
type: Boolean,
default: true
},
total: {
type: Number,
default: 0
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
}
})
const emit = defineEmits(['update:currentPage', 'update:pageSize', 'page-change', 'collect-change', 'like-change'])
const router = useRouter()
// 使用本地状态来代替直接绑定到props
const localCurrentPage = ref(props.currentPage)
const localPageSize = ref(props.pageSize)
// 监听props变化更新本地状态
watch(() => props.currentPage, (newVal) => {
localCurrentPage.value = newVal
})
watch(() => props.pageSize, (newVal) => {
localPageSize.value = newVal
})
const playerStore = usePlayerStore()
// 跳转到音乐详情页
const goToMusicDetail = (id) => {
router.push({ name: 'music-detail', params: { id } })
}
// 检查音乐是否正在播放
const isPlaying = (id) => {
return playerStore.getCurrentMusic?.id === id && playerStore.getIsPlaying
}
// 播放音乐
const playMusic = (music) => {
if (isPlaying(music.id)) {
playerStore.togglePlay()
} else {
playerStore.setCurrentMusic(music)
}
}
// 切换收藏状态
const toggleCollect = async (music) => {
try {
const res = await toggleCollectMusic(music.id)
if (res.code === 200) {
music.collected = !music.collected
ElMessage.success(music.collected ? '收藏成功' : '已取消收藏')
emit('collect-change', music)
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('收藏操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
}
// 切换点赞状态
const toggleLike = async (music) => {
try {
const res = await toggleLikeMusic(music.id)
if (res.code === 200) {
music.liked = !music.liked
ElMessage.success(music.liked ? '点赞成功' : '已取消点赞')
emit('like-change', music)
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('点赞操作失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
}
// 处理下拉菜单命令
const handleCommand = (command, music) => {
switch (command) {
case 'addToPlaylist':
playerStore.addToPlaylist(music)
ElMessage.success('已添加到播放列表')
break
case 'playNext':
// 添加到当前播放音乐的后面
const currentIndex = playerStore.getCurrentIndex
if (currentIndex !== -1) {
const playlist = [...playerStore.getPlaylist]
// 先移除如果已存在的相同音乐
const existingIndex = playlist.findIndex(item => item.id === music.id)
if (existingIndex !== -1) {
playlist.splice(existingIndex, 1)
}
// 插入到当前播放音乐的后面
playlist.splice(currentIndex + 1, 0, music)
playerStore.$patch({ playlist })
ElMessage.success('已添加到下一首播放')
} else {
playerStore.addToPlaylist(music)
ElMessage.success('已添加到播放列表')
}
break
case 'share':
ElMessage.info('分享功能开发中')
break
case 'download':
ElMessage.info('下载功能开发中')
break
}
}
// 格式化时长
const formatDuration = (seconds) => {
if (!seconds) return '00:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 处理分页大小变化
const handleSizeChange = (size) => {
localPageSize.value = size
emit('update:pageSize', size)
emit('page-change', { page: localCurrentPage.value, size })
}
// 处理页码变化
const handleCurrentChange = (page) => {
localCurrentPage.value = page
emit('update:currentPage', page)
emit('page-change', { page, size: localPageSize.value })
}
</script>
<style scoped>
.music-list {
width: 100%;
border-radius: 8px;
overflow: hidden;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.list-header {
display: flex;
align-items: center;
padding: 15px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
font-weight: bold;
color: #606266;
}
.list-item {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #ebeef5;
transition: all 0.3s;
}
.list-item:hover {
background-color: #f5f7fa;
}
.list-item.active {
background-color: #ecf5ff;
color: #409eff;
}
.header-item, .item-index, .item-title, .item-singer, .item-album, .item-duration, .item-actions {
padding: 0 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.index, .item-index {
width: 50px;
text-align: center;
}
.title, .item-title {
flex: 2;
display: flex;
align-items: center;
}
.singer, .item-singer {
flex: 1;
}
.album, .item-album {
flex: 1;
}
.duration, .item-duration {
width: 80px;
text-align: center;
}
.actions, .item-actions {
width: 150px;
text-align: right;
display: flex;
justify-content: flex-end;
gap: 5px;
}
.music-cover {
width: 40px;
height: 40px;
border-radius: 4px;
overflow: hidden;
margin-right: 10px;
}
.music-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.music-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.music-name {
font-weight: bold;
margin-bottom: 5px;
}
.music-type {
font-size: 12px;
color: #909399;
}
.playing-icon {
color: #409eff;
animation: pulse 1s infinite;
}
.pagination-container {
padding: 15px;
display: flex;
justify-content: center;
}
@keyframes pulse {
0% {
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}
@media (max-width: 768px) {
.item-album, .header-item.album {
display: none;
}
.item-actions {
width: 100px;
}
}
</style>