feat: 初始化音乐项目后台管理系统- 新增后台管理相关页面和功能组件
- 实现管理员评论管理、数据统计等功能 - 添加后台布局和路由管理 -配置数据库和Redis连接 - 实现文件上传和访问路径配置 - 添加Sa-Token安全配置
This commit is contained in:
499
music-qianduan/src/views/Singer.vue
Normal file
499
music-qianduan/src/views/Singer.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { getSingerList, getHotSinger, toggleCollectSinger } from '../api/singer'
|
||||
import { getSingerTypeList } from '../api/singerType'
|
||||
import { processImageUrl } from '../utils/imageUtils'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 歌手类型列表
|
||||
const singerTypes = ref([{ id: null, name: '全部' }])
|
||||
|
||||
// 当前选中的歌手类型
|
||||
const activeType = ref(null)
|
||||
|
||||
// 歌手列表
|
||||
const singers = ref([])
|
||||
|
||||
// 分页参数
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索关键词
|
||||
const keyword = ref('')
|
||||
|
||||
// 热门歌手
|
||||
const hotSingers = ref([])
|
||||
const loadingHot = ref(false)
|
||||
|
||||
// 获取歌手类型列表
|
||||
const fetchSingerTypes = async () => {
|
||||
try {
|
||||
const res = await getSingerTypeList()
|
||||
if (res.code === 200) {
|
||||
// 将获取到的类型添加到列表中
|
||||
singerTypes.value = [
|
||||
{ id: null, name: '全部' },
|
||||
...res.data
|
||||
]
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取歌手类型失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌手类型失败:', error)
|
||||
ElMessage.error('获取歌手类型失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换歌手类型
|
||||
const changeType = (typeId) => {
|
||||
activeType.value = typeId
|
||||
currentPage.value = 1
|
||||
fetchSingers()
|
||||
}
|
||||
|
||||
// 获取歌手列表
|
||||
const fetchSingers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSingerList(
|
||||
currentPage.value,
|
||||
pageSize.value,
|
||||
keyword.value,
|
||||
activeType.value
|
||||
)
|
||||
|
||||
if (res.code === 200) {
|
||||
singers.value = res.data.list || res.data.records || []
|
||||
total.value = res.data.total || 0
|
||||
|
||||
// 处理图片URL
|
||||
singers.value = singers.value.map(singer => ({
|
||||
...singer,
|
||||
avatar: processImageUrl(singer.avatar)
|
||||
}))
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取歌手列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌手列表失败:', error)
|
||||
ElMessage.error('获取歌手列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取热门歌手
|
||||
const fetchHotSingers = async () => {
|
||||
loadingHot.value = true
|
||||
try {
|
||||
const res = await getHotSinger(8) // 获取8个热门歌手
|
||||
if (res.code === 200) {
|
||||
hotSingers.value = res.data || []
|
||||
|
||||
// 处理图片URL
|
||||
hotSingers.value = hotSingers.value.map(singer => ({
|
||||
...singer,
|
||||
avatar: processImageUrl(singer.avatar)
|
||||
}))
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取热门歌手失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取热门歌手失败:', error)
|
||||
ElMessage.error('获取热门歌手失败')
|
||||
} finally {
|
||||
loadingHot.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
fetchSingers()
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchSingers()
|
||||
}
|
||||
|
||||
// 处理每页数量变化
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
fetchSingers()
|
||||
}
|
||||
|
||||
// 查看歌手详情
|
||||
const viewSingerDetail = (id) => {
|
||||
router.push(`/singer/${id}`)
|
||||
}
|
||||
|
||||
// 收藏/取消收藏歌手
|
||||
const toggleCollect = async (id, event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (!userStore.isLoggedIn) {
|
||||
ElMessage.warning('请先登录')
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await toggleCollectSinger(id)
|
||||
if (res.code === 200) {
|
||||
// 更新收藏状态
|
||||
const singer = singers.value.find(s => s.id === id)
|
||||
if (singer) {
|
||||
singer.collected = !singer.collected
|
||||
ElMessage.success(singer.collected ? '收藏成功' : '已取消收藏')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error)
|
||||
ElMessage.error('操作失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化粉丝数
|
||||
const formatFans = (count) => {
|
||||
if (count < 10000) {
|
||||
return count.toString()
|
||||
} else if (count < 100000000) {
|
||||
return (count / 10000).toFixed(1) + '万'
|
||||
} else {
|
||||
return (count / 100000000).toFixed(1) + '亿'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSingerTypes()
|
||||
fetchSingers()
|
||||
fetchHotSingers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="singer-container">
|
||||
<div class="singer-header">
|
||||
<h1>歌手</h1>
|
||||
<div class="search-container">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索歌手"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="el-input__icon" @click="handleSearch">
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 歌手类型选择 -->
|
||||
<div class="singer-types">
|
||||
<el-radio-group v-model="activeType" @change="changeType">
|
||||
<el-radio-button
|
||||
v-for="type in singerTypes"
|
||||
:key="type.id"
|
||||
:label="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 热门歌手 -->
|
||||
<div class="hot-singers" v-if="activeType === null && currentPage === 1">
|
||||
<h2>热门歌手</h2>
|
||||
<div class="singer-list" v-loading="loadingHot">
|
||||
<div
|
||||
v-for="singer in hotSingers"
|
||||
:key="singer.id"
|
||||
class="singer-item"
|
||||
@click="viewSingerDetail(singer.id)"
|
||||
>
|
||||
<div class="singer-avatar">
|
||||
<img :src="singer.avatar" :alt="singer.name">
|
||||
<div class="singer-overlay">
|
||||
<el-button
|
||||
circle
|
||||
:icon="singer.collected ? 'StarFilled' : 'Star'"
|
||||
:type="singer.collected ? 'warning' : 'primary'"
|
||||
@click="(event) => toggleCollect(singer.id, event)"
|
||||
></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="singer-info">
|
||||
<div class="singer-name">{{ singer.name }}</div>
|
||||
<div class="singer-stats">
|
||||
<span>{{ singer.musicCount || 0 }}首歌曲</span>
|
||||
|
||||
<span>{{ formatFans(singer.fansCount || 0) }}粉丝</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 歌手列表 -->
|
||||
<div class="all-singers">
|
||||
<h2 v-if="activeType === null && currentPage === 1">全部歌手</h2>
|
||||
<div class="singer-list" v-loading="loading">
|
||||
<div
|
||||
v-for="singer in singers"
|
||||
:key="singer.id"
|
||||
class="singer-item"
|
||||
@click="viewSingerDetail(singer.id)"
|
||||
>
|
||||
<div class="singer-avatar">
|
||||
<img :src="singer.avatar" :alt="singer.name">
|
||||
<div class="singer-overlay">
|
||||
<el-button
|
||||
circle
|
||||
:icon="singer.collected ? 'StarFilled' : 'Star'"
|
||||
:type="singer.collected ? 'warning' : 'primary'"
|
||||
@click="(event) => toggleCollect(singer.id, event)"
|
||||
></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="singer-info">
|
||||
<div class="singer-name">{{ singer.name }}</div>
|
||||
<div class="singer-stats">
|
||||
<span>{{ singer.musicCount || 0 }}首歌曲</span>
|
||||
|
||||
<span>{{ formatFans(singer.fansCount || 0) }}粉丝</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="singers.length === 0 && !loading"
|
||||
description="暂无歌手"
|
||||
></el-empty>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[20, 40, 60, 80]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.singer-container {
|
||||
padding: 20px 20px 80px 20px;
|
||||
min-height: calc(100vh - 140px);
|
||||
background-color: transparent;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.singer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.singer-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
color: #ff6b81;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-container :deep(.el-input__wrapper) {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.search-container .el-icon {
|
||||
color: #ff6b81;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-container .el-icon:hover {
|
||||
color: #ff4757;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.hot-singers,
|
||||
.all-singers {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.hot-singers h2,
|
||||
.all-singers h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
color: #ff6b81;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination :deep(.el-pagination.is-background .btn-next),
|
||||
.pagination :deep(.el-pagination.is-background .btn-prev),
|
||||
.pagination :deep(.el-pagination.is-background .el-pager li) {
|
||||
background-color: #fff9f9;
|
||||
border-radius: 50%;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
|
||||
background-color: #ffb6c1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.singer-types {
|
||||
margin-bottom: 30px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.singer-types :deep(.el-radio-button__inner) {
|
||||
border-radius: 20px !important;
|
||||
margin-right: 10px;
|
||||
border: none;
|
||||
background-color: #fff9f9;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.singer-types :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
|
||||
background-color: #ffb6c1;
|
||||
color: white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.singer-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.singer-item {
|
||||
background-color: #fff9f9;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(255, 107, 129, 0.1);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.singer-item:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 8px 20px rgba(255, 107, 129, 0.2);
|
||||
}
|
||||
|
||||
.singer-avatar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.singer-avatar img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.singer-item:hover .singer-avatar img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.singer-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(255, 182, 193, 0.7));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.singer-item:hover .singer-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.singer-overlay .el-button {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
color: #ff6b81;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.singer-overlay .el-button:hover {
|
||||
background-color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.singer-info {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.singer-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.singer-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.singer-stats span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user