Files
graduation-design/music-qianduan/src/views/admin/Playlist.vue
ikmkj 134f798a54 refactor(playlist): 简化歌单官方状态的处理逻辑
- 移除了将 isOfficial 转换为数字类型的逻辑
- 直接使用 playlist.isOfficial 的原始值
- 删除了多余的 console.log 语句
2025-07-27 16:07:25 +08:00

1060 lines
26 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.
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getPlaylistList, getPlaylistById, addPlaylist as apiAddPlaylist, updatePlaylist as apiUpdatePlaylist, deletePlaylist as apiDeletePlaylist } from '../../api/playlist'
import { getPlaylistTypeList } from '../../api/playlistType'
import { processMusicUrl } from '../../api/music'
// 歌单列表
const playlistList = ref([])
// 加载状态
const loading = ref(false)
// 分页信息
const pagination = ref({
currentPage: 1,
pageSize: 5,
total: 0
})
// 搜索关键字
const searchKeyword = ref('')
// 类型筛选
const selectedType = ref('')
// 类型列表
const typeList = ref([])
// 添加/编辑对话框
const dialogVisible = ref(false)
// 对话框标题
const dialogTitle = ref('添加歌单')
// 当前编辑的歌单
const currentPlaylist = ref(null)
// 表单数据
const form = ref({
name: '',
description: '',
typeId: '',
isOfficial: 1,
cover: null,
currentCover: null // 当前封面URL用于显示
})
// 获取歌单列表
const fetchPlaylistList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination.value
const res = await getPlaylistList(
currentPage,
pageSize,
searchKeyword.value,
selectedType.value || null
)
console.log('歌单列表响应:', res)
if (res.code === 200) {
// 处理歌单列表数据
const data = res.data
// 兼容不同的数据结构
const records = data.records || data.list || []
playlistList.value = records.map(item => ({
...item,
// 处理封面URL
cover: processMusicUrl(item.cover),
// 确保类型ID是数字
typeId: item.typeId || (item.type ? item.type.id : null),
// 确保音乐数量存在
songCount: item.musicCount || item.songCount || 0
}))
pagination.value.total = data.total || 0
console.log('处理后的歌单列表:', playlistList.value)
} else {
ElMessage.error(res.message || '获取歌单列表失败')
}
} catch (error) {
console.error('获取歌单列表失败:', error)
ElMessage.error('获取歌单列表失败')
} finally {
loading.value = false
}
}
// 获取类型列表
const fetchTypeList = async () => {
try {
const res = await getPlaylistTypeList()
if (res.code === 200) {
typeList.value = res.data || []
console.log('歌单类型列表:', typeList.value)
} else {
ElMessage.error(res.message || '获取类型列表失败')
}
} catch (error) {
console.error('获取类型列表失败:', error)
ElMessage.error('获取类型列表失败')
}
}
// 处理页码变化
const handleCurrentChange = (page) => {
pagination.value.currentPage = page
fetchPlaylistList()
}
// 处理每页条数变化
const handleSizeChange = (size) => {
pagination.value.pageSize = size
pagination.value.currentPage = 1
fetchPlaylistList()
}
// 搜索歌单
const searchPlaylist = () => {
pagination.value.currentPage = 1
fetchPlaylistList()
}
// 重置搜索
const resetSearch = () => {
searchKeyword.value = ''
selectedType.value = ''
pagination.value.currentPage = 1
fetchPlaylistList()
}
// 添加歌单
const addPlaylist = () => {
dialogTitle.value = '添加歌单'
currentPlaylist.value = null
form.value = {
name: '',
description: '',
typeId: '',
isOfficial: 1,
cover: null,
currentCover: null
}
dialogVisible.value = true
}
// 编辑歌单
const editPlaylist = async (row) => {
dialogTitle.value = '编辑歌单'
currentPlaylist.value = row
// 先获取类型列表,确保类型数据已加载
await fetchTypeList()
console.log('类型列表:', typeList.value)
try {
// 获取歌单详情
const res = await getPlaylistById(row.id)
if (res.code === 200) {
const playlist = res.data
console.log('歌单详情:', playlist)
// 处理类型ID
let typeId = null
if (playlist.type && playlist.type.id) {
// 如果是对象形式
typeId = Number(playlist.type.id)
} else if (playlist.typeId) {
// 如果是直接的ID
typeId = Number(playlist.typeId)
}
// 如果没有类型,默认选择第一个类型(如果存在)
if (typeId === null && typeList.value.length > 0) {
typeId = typeList.value[0].id
}
console.log('歌单类型ID:', typeId)
// 设置表单数据
form.value = {
name: playlist.name,
description: playlist.description || '',
typeId: typeId,
isOfficial: playlist.isOfficial,
cover: null,
// 保存当前封面URL用于显示
currentCover: playlist.cover
}
// 强制触发响应式更新
setTimeout(() => {
form.value = { ...form.value }
}, 0)
console.log('设置的表单数据:', form.value)
} else {
form.value = {
name: row.name,
description: row.description || '',
typeId: row.typeId || '',
isOfficial: row.isOfficial || 0,
cover: null,
currentCover: row.cover
}
}
} catch (error) {
console.error('获取歌单详情失败:', error)
ElMessage.error('获取歌单详情失败,使用列表数据')
form.value = {
name: row.name,
description: row.description || '',
typeId: row.typeId || '',
isOfficial: row.isOfficial || 0,
cover: null,
currentCover: row.cover
}
}
dialogVisible.value = true
}
// 删除歌单
const deletePlaylist = (row) => {
ElMessageBox.confirm(`确定要删除歌单 ${row.name} 吗?此操作不可恢复`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await apiDeletePlaylist(row.id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchPlaylistList() // 重新加载列表
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除歌单失败:', error)
ElMessage.error('删除失败,请稍后重试')
}
}).catch(() => {})
}
// 管理音乐对话框相关状态
const musicDialogVisible = ref(false)
const currentPlaylistId = ref(null)
const currentPlaylistName = ref('')
const musicList = ref([])
const selectedMusicIds = ref([])
const musicSearchKeyword = ref('')
const musicLoading = ref(false)
// 获取音乐列表
const fetchMusicList = async () => {
musicLoading.value = true
try {
const res = await getMusicList(1, 100, musicSearchKeyword.value)
if (res.code === 200) {
musicList.value = res.data.list || res.data.records || []
} else {
ElMessage.error(res.message || '获取音乐列表失败')
}
} catch (error) {
console.error('获取音乐列表失败:', error)
ElMessage.error('获取音乐列表失败')
} finally {
musicLoading.value = false
}
}
// 获取歌单中的音乐
const fetchPlaylistMusic = async (playlistId) => {
try {
const res = await getPlaylistById(playlistId)
if (res.code === 200 && res.data.musics) {
selectedMusicIds.value = res.data.musics.map(m => m.id)
}
} catch (error) {
console.error('获取歌单音乐失败:', error)
}
}
// 管理歌单音乐
const managePlaylistMusic = async (row) => {
currentPlaylistId.value = row.id
currentPlaylistName.value = row.name
musicSearchKeyword.value = ''
musicDialogVisible.value = true
// 加载音乐列表和已选音乐
await Promise.all([
fetchMusicList(),
fetchPlaylistMusic(row.id)
])
}
// 添加音乐到歌单
const addMusicToPlaylistLocal = async (musicId) => {
try {
const res = await addMusicToPlaylist(currentPlaylistId.value, musicId)
if (res.code === 200) {
ElMessage.success('添加成功')
await fetchPlaylistMusic(currentPlaylistId.value)
} else {
ElMessage.error(res.message || '添加失败')
}
} catch (error) {
console.error('添加音乐失败:', error)
ElMessage.error('添加失败')
}
}
// 从歌单移除音乐
const removeMusicFromPlaylistLocal = async (musicId) => {
try {
const res = await removeMusicFromPlaylist(currentPlaylistId.value, musicId)
if (res.code === 200) {
ElMessage.success('移除成功')
await fetchPlaylistMusic(currentPlaylistId.value)
} else {
ElMessage.error(res.message || '移除失败')
}
} catch (error) {
console.error('移除音乐失败:', error)
ElMessage.error('移除失败')
}
}
// 搜索音乐
const searchMusic = () => {
fetchMusicList()
}
// 提交表单
const submitForm = async () => {
// 表单验证
if (!form.value.name) {
ElMessage.warning('请输入歌单名称')
return
}
if (!form.value.typeId) {
ElMessage.warning('请选择歌单类型')
return
}
try {
let res
const formData = {
name: form.value.name,
description: form.value.description,
typeId: form.value.typeId,
isOfficial: form.value.isOfficial
}
console.log('提交的歌单数据:', formData)
if (currentPlaylist.value) {
// 编辑歌单
res = await apiUpdatePlaylist(
currentPlaylist.value.id,
formData,
form.value.cover
)
} else {
// 添加歌单
res = await apiAddPlaylist(
formData,
form.value.cover
)
}
if (res.code === 200) {
ElMessage.success(currentPlaylist.value ? '编辑成功' : '添加成功')
dialogVisible.value = false
fetchPlaylistList() // 重新加载列表
} else {
ElMessage.error(res.message || (currentPlaylist.value ? '编辑失败' : '添加失败'))
}
} catch (error) {
console.error(currentPlaylist.value ? '编辑歌单失败:' : '添加歌单失败:', error)
ElMessage.error(currentPlaylist.value ? '编辑失败,请稍后重试' : '添加失败,请稍后重试')
}
}
// 格式化类型
const formatType = (typeId) => {
const type = typeList.value.find(t => t.id === typeId)
return type ? type.name : '-'
}
// 处理封面上传
const handleCoverChange = (file) => {
if (file && file.raw) {
// 检查文件类型
const isImage = file.raw.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return
}
// 检查文件大小 (2MB)
const isLt2M = file.raw.size / 1024 / 1024 < 2
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return
}
form.value.cover = file.raw
}
}
onMounted(() => {
fetchPlaylistList()
fetchTypeList()
})
// 导入音乐相关API
import { getMusicList } from '../../api/music'
import { addMusicToPlaylist, removeMusicFromPlaylist } from '../../api/playlist'
</script>
<template>
<div class="admin-playlist-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="请输入歌单名称/描述"
clearable
@clear="resetSearch"
@keyup.enter="searchPlaylist"
class="search-input"
>
<template #append>
<el-button @click="searchPlaylist">
<el-icon><search /></el-icon>
搜索
</el-button>
</template>
</el-input>
<el-select
v-model="selectedType"
placeholder="选择类型"
clearable
@change="searchPlaylist"
class="type-select"
>
<el-option
v-for="type in typeList"
:key="type.id"
:label="type.name"
:value="type.id"
/>
</el-select>
<el-button type="primary" @click="addPlaylist">
<el-icon><plus /></el-icon>
添加歌单
</el-button>
</div>
<!-- 歌单列表 -->
<el-table
:data="playlistList"
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column label="封面" width="80" align="center">
<template #default="scope">
<el-image
:src="scope.row.cover || '/default-cover.jpg'"
:alt="scope.row.name"
class="playlist-cover"
fit="cover"
:preview-src-list="scope.row.cover ? [scope.row.cover] : []"
:initial-index="0"
preview-teleported
>
<template #error>
<div class="image-error">
<el-icon><picture /></el-icon>
</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="name" label="歌单名称" min-width="150"></el-table-column>
<el-table-column prop="description" label="描述" min-width="200">
<template #default="scope">
<el-tooltip
:content="scope.row.description"
placement="top"
:show-after="500"
>
<div class="description-text">{{ scope.row.description || '-' }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="创建者" width="100">
<template #default="scope">
{{ scope.row.creator ? scope.row.creator.username : '-' }}
</template>
</el-table-column>
<el-table-column prop="typeId" label="类型" width="100">
<template #default="scope">
{{ formatType(scope.row.typeId) }}
</template>
</el-table-column>
<el-table-column prop="isOfficial" label="是否官方" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.isOfficial === 1 ? 'success' : 'info'">
{{ scope.row.isOfficial === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="songCount" label="歌曲数" width="80" align="center"></el-table-column>
<el-table-column prop="playCount" label="播放次数" width="100" align="center"></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="120">
<template #default="scope">
{{ scope.row.createTime ? new Date(scope.row.createTime).toLocaleDateString() : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
size="small"
@click="managePlaylistMusic(scope.row)"
>
管理音乐
</el-button>
<el-button
size="small"
type="primary"
@click="editPlaylist(scope.row)"
>
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="deletePlaylist(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[5, 10, 15, 20]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 添加/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
>
<el-form
:model="form"
label-width="100px"
>
<el-form-item label="歌单名称" required>
<el-input v-model="form.name" placeholder="请输入歌单名称" />
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入歌单描述"
/>
</el-form-item>
<el-form-item label="类型" required>
<el-select
v-model="form.typeId"
placeholder="请选择歌单类型"
style="width: 100%"
>
<el-option
v-for="type in typeList"
:key="type.id"
:label="type.name"
:value="type.id"
/>
</el-select>
</el-form-item>
<el-form-item label="是否官方">
<div class="official-switch-container">
<el-switch
v-model="form.isOfficial"
:active-value="1"
:inactive-value="0"
style="width: 40px"
/>
<span class="switch-value-text">
{{ form.isOfficial === 1 ? '当前状态: 官方歌单' : '当前状态: 非官方歌单' }}
</span>
</div>
</el-form-item>
<el-form-item label="封面">
<div class="cover-container">
<!-- 当前封面预览 -->
<div class="cover-section">
<div class="section-title">当前封面</div>
<div v-if="form.currentCover" class="current-cover">
<el-image
:src="form.currentCover"
fit="cover"
class="cover-preview"
:preview-src-list="[form.currentCover]"
:initial-index="0"
preview-teleported
/>
</div>
<div v-else class="no-cover">
<el-icon><picture /></el-icon>
<span>暂无封面</span>
</div>
</div>
<!-- 上传新封面 -->
<div class="upload-section">
<div class="section-title">上传新封面建议尺寸 300x300 像素</div>
<el-upload
action="#"
:auto-upload="false"
:on-change="handleCoverChange"
:limit="1"
list-type="picture-card"
class="cover-upload"
>
<el-icon><plus /></el-icon>
</el-upload>
<div class="el-upload__tip">
点击上传新封面不上传则保持原封面
</div>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 管理音乐对话框 -->
<el-dialog
v-model="musicDialogVisible"
:title="`管理歌单 ${currentPlaylistName} 的音乐`"
width="800px"
>
<div class="music-management-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="musicSearchKeyword"
placeholder="搜索音乐名称"
clearable
@keyup.enter="searchMusic"
class="search-input"
>
<template #append>
<el-button @click="searchMusic">
<el-icon><search /></el-icon>
搜索
</el-button>
</template>
</el-input>
</div>
<!-- 音乐列表 -->
<div class="music-list-container">
<el-table
:data="musicList"
stripe
style="width: 100%"
v-loading="musicLoading"
>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="音乐名称" min-width="150"></el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button
size="small"
type="primary"
@click="addMusicToPlaylistLocal(scope.row.id)"
:disabled="selectedMusicIds.includes(scope.row.id)"
>
{{ selectedMusicIds.includes(scope.row.id) ? '已添加' : '添加' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 已添加音乐 -->
<div class="selected-music-container">
<h3>已添加的音乐</h3>
<el-table
:data="musicList.filter(m => selectedMusicIds.includes(m.id))"
stripe
style="width: 100%"
empty-text="暂无音乐"
>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="音乐名称" min-width="150"></el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button
size="small"
type="danger"
@click="removeMusicFromPlaylistLocal(scope.row.id)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="musicDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.music-management-container {
padding: 10px;
}
.search-bar {
margin-bottom: 20px;
}
.search-input {
width: 300px;
}
.music-list-container,
.selected-music-container {
margin-bottom: 20px;
}
.selected-music-container h3 {
margin-bottom: 10px;
color: #606266;
}
</style>
<style scoped>
.admin-playlist-container {
padding: 20px;
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.search-bar {
margin-bottom: 20px;
display: flex;
gap: 10px;
align-items: center;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.search-input {
width: 300px;
}
.search-input :deep(.el-input__inner) {
border-radius: 8px;
border: 1px solid #b3e5fc;
transition: all 0.3s;
}
.search-input :deep(.el-input__inner):focus {
border-color: #4dd0e1;
box-shadow: 0 0 0 2px rgba(77, 208, 225, 0.2);
}
.type-select {
width: 150px;
}
.type-select :deep(.el-input__inner) {
border-radius: 8px;
border: 1px solid #b3e5fc;
}
.el-button {
border-radius: 8px !important;
font-weight: bold !important;
transition: all 0.3s !important;
}
.el-button--primary {
background: linear-gradient(135deg, #4dd0e1 0%, #26c6da 100%) !important;
border: none !important;
}
.el-button--primary:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 8px rgba(38, 198, 218, 0.3) !important;
}
.el-table {
border-radius: 12px;
overflow: hidden;
}
.el-table :deep(th) {
background-color: #e1f5fe !important;
color: #0d47a1;
}
.el-table :deep(.el-table__row) {
transition: all 0.3s;
}
.el-table :deep(.el-table__row:hover) {
background-color: #f5f5f5 !important;
transform: scale(1.01);
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
}
.playlist-cover {
width: 50px;
height: 50px;
border-radius: 8px;
object-fit: cover;
transition: all 0.3s;
}
.playlist-cover:hover {
transform: scale(1.1);
}
.image-error {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f7fa;
color: #909399;
font-size: 20px;
border-radius: 8px;
}
.description-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.el-dialog {
border-radius: 16px !important;
}
.el-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, #e1f5fe 0%, #b3e5fc 100%);
border-radius: 16px 16px 0 0 !important;
padding: 15px 20px;
}
.el-dialog :deep(.el-dialog__title) {
color: #0d47a1;
font-weight: bold;
}
.el-form-item :deep(.el-form-item__label) {
color: #4a4a4a;
font-weight: bold;
}
.el-input :deep(.el-input__inner) {
border-radius: 8px;
border: 1px solid #b3e5fc;
}
.el-input :deep(.el-input__inner):focus {
border-color: #4dd0e1;
box-shadow: 0 0 0 2px rgba(77, 208, 225, 0.2);
}
.cover-container {
display: flex;
align-items: flex-start;
gap: 30px;
}
.cover-section,
.upload-section {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.section-title {
font-size: 14px;
color: #4a4a4a;
margin-bottom: 10px;
font-weight: bold;
}
.current-cover {
display: flex;
flex-direction: column;
align-items: center;
}
.cover-preview {
width: 100px;
height: 100px;
border-radius: 8px;
object-fit: cover;
border: 1px solid #b3e5fc;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.cover-preview:hover {
transform: scale(1.05);
}
.no-cover {
width: 100px;
height: 100px;
border-radius: 8px;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
border: 1px dashed #b3e5fc;
transition: all 0.3s;
}
.no-cover:hover {
border-color: #4dd0e1;
}
.no-cover .el-icon {
font-size: 24px;
margin-bottom: 5px;
}
.cover-upload :deep(.el-upload--picture-card) {
width: 100px;
height: 100px;
line-height: 100px;
border-radius: 8px;
border: 1px dashed #b3e5fc;
transition: all 0.3s;
}
.cover-upload :deep(.el-upload--picture-card:hover) {
border-color: #4dd0e1;
}
.official-switch-container {
display: flex;
align-items: center;
}
.switch-value-text {
margin-left: 15px;
font-size: 13px;
color: #4a4a4a;
}
.el-table :deep(.el-button) {
margin-left: 0;
margin-right: 5px;
padding: 4px 8px;
border-radius: 8px !important;
}
.music-management-container {
padding: 10px;
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
}
.music-management-container .search-bar {
background: transparent;
padding: 0;
box-shadow: none;
}
.music-list-container,
.selected-music-container {
margin-bottom: 20px;
}
.selected-music-container h3 {
margin-bottom: 10px;
color: #4a4a4a;
}
</style>