Files
graduation-design/music-qianduan/src/components/MusicList.vue
ikmkj b275ffab5a feat: 初始化音乐项目后台管理系统- 新增后台管理相关页面和功能组件
- 实现管理员评论管理、数据统计等功能
- 添加后台布局和路由管理
-配置数据库和Redis连接
- 实现文件上传和访问路径配置
- 添加Sa-Token安全配置
2025-04-26 19:32:45 +08:00

385 lines
9.2 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-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 { VideoPlay, VideoPause, Star, More } from '@element-plus/icons-vue'
import { usePlayerStore } from '../stores/player'
import { toggleCollectMusic } 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'])
// 使用本地状态来代替直接绑定到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 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 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>