Files
graduation-design/music-qianduan/src/views/admin/Singer.vue
ikmkj 5b30905577 feat(music): 增加歌单和歌手的收藏数和评论数字段
- 在 PlaylistDTO 和 SingerDTO 中添加 collectCount 和 commentCount 字段
- 更新前端 Singer 组件,使用 getSingerById API 获取歌手信息
2025-07-26 13:00:38 +08:00

854 lines
20 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 { getSingerList, addSinger as apiAddSinger, updateSinger, deleteSinger, getSingerById } from '../../api/singer'
import { getSingerTypeList } from '../../api/admin/type'
import { getMusicList } from '../../api/music'
import { addMusicToSinger, removeMusicFromSinger } from '../../api/singer'
// 歌手列表
const singerList = 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 currentSinger = ref(null)
// 表单数据
const form = ref({
name: '',
typeId: '',
introduction: '',
avatar: null
})
// 获取歌手列表
const fetchSingerList = async () => {
loading.value = true
try {
const params = {
page: pagination.value.currentPage,
size: pagination.value.pageSize,
keyword: searchKeyword.value,
typeId: selectedType.value || null
}
const res = await getSingerList(params.page, params.size, params.keyword, params.typeId)
if (res.code === 200) {
singerList.value = res.data.list
console.log(66666,singerList.value)
pagination.value.total = res.data.total
} else {
ElMessage.error(res.message || '获取歌手列表失败')
}
} catch (error) {
console.error('获取歌手列表错误:', error)
ElMessage.error('获取歌手列表失败')
} finally {
loading.value = false
}
}
// 获取类型列表
const fetchTypeList = async () => {
try {
const res = await getSingerTypeList()
if (res.code === 200) {
typeList.value = res.data
} else {
ElMessage.error(res.message || '获取歌手类型列表失败')
}
} catch (error) {
console.error('获取歌手类型列表错误:', error)
ElMessage.error('获取歌手类型列表失败')
}
}
// 处理页码变化
const handleCurrentChange = (page) => {
pagination.value.currentPage = page
fetchSingerList()
}
// 处理每页条数变化
const handleSizeChange = (size) => {
pagination.value.pageSize = size
pagination.value.currentPage = 1
fetchSingerList()
}
// 搜索歌手
const searchSinger = () => {
pagination.value.currentPage = 1
fetchSingerList()
}
// 重置搜索
const resetSearch = () => {
searchKeyword.value = ''
selectedType.value = ''
pagination.value.currentPage = 1
fetchSingerList()
}
// 添加歌手
const addSinger = () => {
dialogTitle.value = '添加歌手'
currentSinger.value = null
form.value = {
name: '',
typeId: '',
introduction: '',
avatar: null
}
dialogVisible.value = true
}
// 编辑歌手
const editSinger = (row) => {
dialogTitle.value = '编辑歌手'
currentSinger.value = row
form.value = {
name: row.name,
typeId: row.type ? row.type.id : row.typeId,
introduction: row.introduction || '',
avatar: null
}
dialogVisible.value = true
}
// 删除歌手
const handleDeleteSinger = (row) => {
ElMessageBox.confirm(`确定要删除歌手 ${row.name} 吗?此操作不可恢复`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteSinger(row.id)
if (res.code === 200) {
ElMessage.success(res.message || '删除成功')
// 刷新歌手列表
fetchSingerList()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除歌手错误:', error)
ElMessage.error('删除歌手失败')
}
}).catch(() => {})
}
// 管理音乐对话框相关状态
const musicDialogVisible = ref(false)
const currentSingerId = ref(null)
const currentSingerName = 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 fetchSingerMusic = async (singerId) => {
try {
const res = await getSingerById(singerId)
if (res.code === 200 && res.data.musics) {
selectedMusicIds.value = res.data.musics.map(m => m.id)
}
} catch (error) {
console.error('获取歌手音乐失败:', error)
}
}
// 管理歌手音乐
const manageSingerMusic = async (row) => {
currentSingerId.value = row.id
currentSingerName.value = row.name
musicSearchKeyword.value = ''
musicDialogVisible.value = true
// 加载音乐列表和已选音乐
await Promise.all([
fetchMusicList(),
fetchSingerMusic(row.id)
])
}
// 添加音乐到歌手
const addMusicToSingerLocal = async (musicId) => {
try {
const res = await addMusicToSinger(currentSingerId.value, musicId)
if (res.code === 200) {
ElMessage.success('添加成功')
await fetchSingerMusic(currentSingerId.value)
} else {
ElMessage.error(res.message || '添加失败')
}
} catch (error) {
console.error('添加音乐失败:', error)
ElMessage.error('添加失败')
}
}
// 从歌手移除音乐
const removeMusicFromSingerLocal = async (musicId) => {
try {
const res = await removeMusicFromSinger(currentSingerId.value, musicId)
if (res.code === 200) {
ElMessage.success('移除成功')
await fetchSingerMusic(currentSingerId.value)
} else {
ElMessage.error(res.message || '移除失败')
}
} catch (error) {
console.error('移除音乐失败:', error)
ElMessage.error('移除失败')
}
}
// 搜索音乐
const searchMusic = () => {
fetchMusicList()
}
// 提交表单
const submitForm = async () => {
try {
// 构建歌手数据
const singerData = {
name: form.value.name,
introduction: form.value.introduction,
type: form.value.typeId ? { id: form.value.typeId } : null
}
let res
if (currentSinger.value) {
// 编辑歌手
res = await updateSinger(currentSinger.value.id, singerData, form.value.avatar)
} else {
// 添加歌手
res = await apiAddSinger(singerData, form.value.avatar)
}
if (res.code === 200) {
ElMessage.success(res.message || (currentSinger.value ? '编辑成功' : '添加成功'))
dialogVisible.value = false
// 刷新歌手列表
fetchSingerList()
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
console.error('提交歌手表单错误:', error)
ElMessage.error('操作失败')
}
}
// 格式化类型
const formatType = (typeId) => {
const type = typeList.value.find(t => t.id === typeId)
return type ? type.name : '-'
}
// 格式化粉丝数
const formatFans = (count) => {
if (count < 10000) {
return count.toString()
} else if (count < 100000000) {
return (count / 10000).toFixed(1) + '万'
} else {
return (count / 100000000).toFixed(1) + '亿'
}
}
// 处理头像上传
const handleAvatarChange = (file) => {
if (file && file.raw) {
// 验证文件类型
const isImage = file.raw.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return
}
// 验证文件大小
const isLt2M = file.raw.size / 1024 / 1024 < 2
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return
}
form.value.avatar = file.raw
}
}
onMounted(() => {
fetchSingerList()
fetchTypeList()
})
</script>
<template>
<div class="admin-singer-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="请输入歌手名称/简介"
clearable
@clear="resetSearch"
@keyup.enter="searchSinger"
class="search-input"
>
<template #append>
<el-button @click="searchSinger">
<el-icon><search /></el-icon>
搜索
</el-button>
</template>
</el-input>
<el-select
v-model="selectedType"
placeholder="选择类型"
clearable
@change="searchSinger"
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="addSinger">
<el-icon><plus /></el-icon>
添加歌手
</el-button>
</div>
<!-- 歌手列表 -->
<el-table
:data="singerList"
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">
<img
:src="scope.row.avatar"
:alt="scope.row.name"
class="singer-avatar"
>
</template>
</el-table-column>
<el-table-column prop="name" label="歌手名称" min-width="150"></el-table-column>
<el-table-column label="类型" width="120">
<template #default="scope">
{{ formatType(scope.row.type ? scope.row.type.id : scope.row.typeId) }}
</template>
</el-table-column>
<el-table-column prop="introduction" label="简介" min-width="200">
<template #default="scope">
<el-tooltip
:content="scope.row.introduction"
placement="top"
:show-after="500"
>
<div class="introduction-text">{{ scope.row.introduction }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="musicCount" label="单曲数" width="80" align="center"></el-table-column>
<el-table-column prop="fansCount" label="粉丝数" width="100" align="center">
<template #default="scope">
{{ formatFans(scope.row.fansCount || 0) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="120"></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
size="small"
@click="manageSingerMusic(scope.row)"
>
管理音乐
</el-button>
<el-button
size="small"
type="primary"
@click="editSinger(scope.row)"
>
编辑
</el-button>
<el-button
size="small"
type="danger"
@click="handleDeleteSinger(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="类型" 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="简介">
<el-input
v-model="form.introduction"
type="textarea"
:rows="3"
placeholder="请输入歌手简介"
/>
</el-form-item>
<el-form-item label="头像">
<!-- 当前头像预览 -->
<div v-if="currentSinger && currentSinger.avatar" class="current-avatar">
<el-image
:src="currentSinger.avatar"
fit="cover"
class="avatar-preview"
:preview-src-list="[currentSinger.avatar]"
:initial-index="0"
preview-teleported
/>
<div class="avatar-tip">当前头像</div>
</div>
<el-upload
action="#"
:auto-upload="false"
:on-change="handleAvatarChange"
:limit="1"
list-type="picture-card"
>
<el-icon><plus /></el-icon>
</el-upload>
<div class="el-upload__tip">
点击上传新头像不上传则保持原头像
</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="`管理歌手 ${currentSingerName} 的音乐`"
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="addMusicToSingerLocal(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="removeMusicFromSingerLocal(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-singer-container {
padding: 20px;
background: linear-gradient(135deg, #e1f5fe 0%, #b3e5fc 100%);
}
.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;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
}
.singer-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
transition: all 0.3s;
}
.singer-avatar:hover {
transform: scale(1.1);
}
.introduction-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.el-table {
--el-table-border-color: #b3e5fc;
--el-table-header-bg-color: #e1f5fe;
--el-table-row-hover-bg-color: #b3e5fc;
border-radius: 8px;
overflow: hidden;
}
.el-table th.el-table__cell {
background-color: #e1f5fe !important;
color: #0d47a1 !important;
font-weight: bold !important;
}
.el-table .el-table__row {
background-color: rgba(255, 255, 255, 0.9) !important;
transition: all 0.3s;
}
.el-table .el-table__row:hover {
background-color: #b3e5fc !important;
transform: scale(1.01);
}
.el-table :deep(.el-button) {
margin-left: 0;
margin-right: 5px;
padding: 4px 8px;
border-radius: 8px !important;
}
.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);
}
.current-avatar {
margin-bottom: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-preview {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 5px;
border: 1px solid #b3e5fc;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.avatar-preview:hover {
transform: scale(1.05);
}
.avatar-tip {
font-size: 12px;
color: #4a4a4a;
}
.el-upload :deep(.el-upload--picture-card) {
border-radius: 8px;
border: 1px dashed #b3e5fc;
transition: all 0.3s;
}
.el-upload :deep(.el-upload--picture-card:hover) {
border-color: #4dd0e1;
}
.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>