feat: 初始化音乐项目后台管理系统- 新增后台管理相关页面和功能组件

- 实现管理员评论管理、数据统计等功能
- 添加后台布局和路由管理
-配置数据库和Redis连接
- 实现文件上传和访问路径配置
- 添加Sa-Token安全配置
This commit is contained in:
ikmkj
2025-04-26 19:32:45 +08:00
commit b275ffab5a
185 changed files with 32763 additions and 0 deletions

View File

@@ -0,0 +1,502 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { usePlayerStore } from '../stores/player'
import { ElMessage } from 'element-plus'
import HomeBanner from '../components/HomeBanner.vue'
import { getLatestMusic } from '../api/music'
import { getLatestPlaylist } from '../api/playlist'
import { getLatestSinger } from '../api/singer'
import { processMusicUrl } from '../api/music'
import { processImageUrl } from '../utils/imageUtils'
const router = useRouter()
const playerStore = usePlayerStore()
// 新歌推荐
const newSongs = ref([])
const loadingNewSongs = ref(false)
// 新歌单推荐
const newPlaylists = ref([])
const loadingNewPlaylists = ref(false)
// 新歌手推荐
const newSingers = ref([])
const loadingNewSingers = ref(false)
// 获取最新音乐
const fetchLatestMusic = async () => {
loadingNewSongs.value = true
try {
const res = await getLatestMusic(8) // 获取8首最新音乐
if (res.code === 200) {
newSongs.value = res.data.map(item => ({
...item,
cover: processMusicUrl(item.cover),
url: processMusicUrl(item.url)
}))
console.log('最新音乐数据:', newSongs.value)
} else {
ElMessage.error(res.message || '获取最新音乐失败')
}
} catch (error) {
console.error('获取最新音乐失败:', error)
ElMessage.error('获取最新音乐失败')
} finally {
loadingNewSongs.value = false
}
}
// 获取最新歌单
const fetchLatestPlaylist = async () => {
loadingNewPlaylists.value = true
try {
const res = await getLatestPlaylist(8) // 获取8个最新歌单
if (res.code === 200) {
newPlaylists.value = res.data
console.log('最新歌单数据:', newPlaylists.value)
} else {
ElMessage.error(res.message || '获取最新歌单失败')
}
} catch (error) {
console.error('获取最新歌单失败:', error)
ElMessage.error('获取最新歌单失败')
} finally {
loadingNewPlaylists.value = false
}
}
// 获取最新歌手
const fetchLatestSinger = async () => {
loadingNewSingers.value = true
try {
const res = await getLatestSinger(8) // 获取8个最新歌手
if (res.code === 200) {
newSingers.value = res.data.map(item => ({
...item,
avatar: processImageUrl(item.avatar)
}))
console.log('最新歌手数据:', newSingers.value)
} else {
ElMessage.error(res.message || '获取最新歌手失败')
}
} catch (error) {
console.error('获取最新歌手失败:', error)
ElMessage.error('获取最新歌手失败')
} finally {
loadingNewSingers.value = false
}
}
// 播放音乐
const playSong = (song) => {
playerStore.setCurrentMusic(song)
}
// 跳转到歌单详情
const goToPlaylist = (id) => {
router.push(`/playlist/${id}`)
}
// 跳转到歌手详情
const goToSinger = (id) => {
router.push(`/singer/${id}`)
}
// 格式化播放次数
const formatPlayCount = (count) => {
if (!count) return '0'
if (count < 10000) {
return count.toString()
} else if (count < 100000000) {
return (count / 10000).toFixed(1) + '万'
} else {
return (count / 100000000).toFixed(1) + '亿'
}
}
onMounted(() => {
// 获取数据
fetchLatestMusic()
fetchLatestPlaylist()
fetchLatestSinger()
})
</script>
<template>
<div class="home-container">
<!-- 轮播图 -->
<div class="banner-section">
<HomeBanner />
</div>
<!-- 新歌推荐 -->
<div class="section new-songs-section" v-loading="loadingNewSongs">
<div class="section-header">
<h2>新歌推荐</h2>
<el-button text @click="router.push('/music')">查看更多</el-button>
</div>
<div class="song-list">
<div v-for="song in newSongs" :key="song.id" class="song-item" @dblclick="playSong(song)">
<div class="song-cover">
<img :src="song.cover" :alt="song.name">
<div class="play-icon" @click="playSong(song)">
<el-icon><video-play /></el-icon>
</div>
</div>
<div class="song-info">
<div class="song-name">{{ song.name }}</div>
<div class="song-singer">{{ song.singer || (song.singers && song.singers.map(s => s.name).join(', ')) }}</div>
</div>
</div>
</div>
</div>
<!-- 新歌单推荐 -->
<div class="section new-playlists-section" v-loading="loadingNewPlaylists">
<div class="section-header">
<h2>新歌单推荐</h2>
<el-button text @click="router.push('/playlist')">查看更多</el-button>
</div>
<div class="playlist-list">
<div v-for="playlist in newPlaylists" :key="playlist.id" class="playlist-item" @click="goToPlaylist(playlist.id)">
<div class="playlist-cover">
<img :src="playlist.cover" :alt="playlist.name">
<div class="playlist-info-overlay">
<div class="playlist-play-count" v-if="playlist.playCount">
<el-icon><video-play /></el-icon>
{{ formatPlayCount(playlist.playCount) }}
</div>
</div>
</div>
<div class="playlist-info">
<div class="playlist-name">{{ playlist.name }}</div>
<div class="playlist-desc">{{ playlist.description }}</div>
</div>
</div>
</div>
</div>
<!-- 新歌手推荐 -->
<div class="section new-singers-section" v-loading="loadingNewSingers">
<div class="section-header">
<h2>新歌手推荐</h2>
<el-button text @click="router.push('/singer')">查看更多</el-button>
</div>
<div class="singer-list">
<div v-for="singer in newSingers" :key="singer.id" class="singer-item" @click="goToSinger(singer.id)">
<div class="singer-avatar">
<img :src="singer.avatar" :alt="singer.name">
</div>
<div class="singer-name">{{ singer.name }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.home-container {
padding: 20px 20px 80px 20px; /* 增加左右内边距,底部填充为播放器留出空间 */
min-height: calc(100vh - 140px); /* 减去顶部导航和底部播放器的高度 */
background-color: transparent; /* 设置为透明,让布局背景透出来 */
border-radius: 15px; /* 更大的圆角 */
box-shadow: none; /* 移除阴影,因为背景是渐变 */
}
/* 轮播图样式 */
.banner-section {
margin-bottom: 30px; /* 增加间距 */
border-radius: 10px; /* 添加圆角 */
overflow: hidden; /* 确保轮播图内容也在圆角内 */
}
/* 各类推荐区域样式 */
.section {
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); /* 稍微调整阴影 */
border: none; /* 移除底部边框 */
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px; /* 增加间距 */
padding: 0; /* 移除内边距,因为 section 已经有内边距 */
}
.section-header h2 {
margin: 0;
font-size: 1.6rem; /* 稍大字体 */
color: #ff6b81; /* 可爱的粉色 */
font-weight: 600; /* 加粗一点 */
}
/* 修改 Element Plus 按钮样式 */
.section-header .el-button {
background-color: #ffb6c1; /* 淡粉色背景 */
color: white;
border: none;
border-radius: 20px; /* 圆润按钮 */
padding: 8px 15px;
transition: background-color 0.3s;
}
.section-header .el-button:hover {
background-color: #ff8a9b; /* 悬停时深一点的粉色 */
}
/* 音乐列表样式 */
.song-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); /* 稍微增大最小宽度 */
gap: 20px; /* 增加间隙 */
padding: 0; /* 移除内边距 */
}
.song-item {
cursor: pointer;
transition: all 0.3s ease-in-out;
background-color: #fff9f9; /* 非常淡的粉色背景 */
border-radius: 10px; /* 圆角 */
padding: 10px;
box-shadow: 0 2px 5px rgba(255, 107, 129, 0.1); /* 粉色阴影 */
}
.song-item:hover {
transform: translateY(-6px) scale(1.03); /* 悬停效果更明显 */
box-shadow: 0 4px 10px rgba(255, 107, 129, 0.2);
}
.song-cover {
position: relative;
width: 100%;
padding-top: 100%;
border-radius: 8px; /* 圆角 */
overflow: hidden;
margin-bottom: 12px; /* 增加间距 */
}
.song-cover img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease; /* 图片缩放过渡 */
}
.song-item:hover .song-cover img {
transform: scale(1.1); /* 悬停时图片放大 */
}
.play-icon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 182, 193, 0.5); /* 淡粉色遮罩 */
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
border-radius: 8px; /* 保持圆角一致 */
}
.play-icon .el-icon {
font-size: 3.5rem; /* 稍大图标 */
color: white; /* 白色图标 */
}
.song-cover:hover .play-icon {
opacity: 1;
}
.song-info {
padding: 0; /* 移除内边距 */
text-align: center; /* 居中显示 */
}
.song-name {
font-weight: 600; /* 加粗 */
margin-bottom: 6px; /* 增加间距 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333; /* 深灰色 */
font-size: 0.95rem;
}
.song-singer {
font-size: 0.8rem;
color: #888; /* 浅灰色 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 歌单列表样式 */
.playlist-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px;
padding: 0;
}
.playlist-item {
cursor: pointer;
transition: all 0.3s ease-in-out;
border-radius: 10px;
overflow: hidden; /* 隐藏溢出的内容 */
box-shadow: 0 2px 5px rgba(173, 216, 230, 0.2); /* 淡蓝色阴影 */
background-color: #f0f8ff; /* 淡蓝色背景 */
}
.playlist-item:hover {
transform: translateY(-6px) scale(1.03);
box-shadow: 0 4px 10px rgba(173, 216, 230, 0.4);
}
.playlist-cover {
position: relative;
width: 100%;
padding-top: 100%;
border-radius: 0; /* 移除这里的圆角让item控制 */
overflow: hidden;
margin-bottom: 0; /* 移除间距 */
}
.playlist-cover img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.playlist-item:hover .playlist-cover img {
transform: scale(1.1);
}
.playlist-info-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.6)); /* 渐变效果 */
opacity: 1; /* 默认显示 */
transition: background-color 0.3s;
display: flex;
flex-direction: column;
justify-content: flex-end; /* 播放次数放底部 */
padding: 10px;
box-sizing: border-box;
}
.playlist-cover:hover .playlist-info-overlay {
background-color: rgba(0, 0, 0, 0.2); /* 悬停时加深一点 */
}
.playlist-play-count {
color: white;
font-size: 0.85rem; /* 稍大字体 */
display: flex;
align-items: center;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5); /* 添加文字阴影 */
}
.playlist-play-count .el-icon {
margin-right: 4px; /* 调整间距 */
font-size: 1rem; /* 调整图标大小 */
}
.playlist-info {
padding: 10px; /* 增加内边距 */
background-color: #f0f8ff; /* 与 item 背景色一致 */
}
.playlist-name {
font-weight: 600;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
font-size: 0.95rem;
}
.playlist-desc {
font-size: 0.8rem;
color: #777; /* 稍深的灰色 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 歌手列表样式 */
.singer-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); /* 调整大小 */
gap: 25px; /* 增加间隙 */
padding: 0;
}
.singer-item {
cursor: pointer;
transition: all 0.3s ease-in-out;
text-align: center;
}
.singer-item:hover {
transform: translateY(-6px) scale(1.05); /* 悬停效果 */
}
.singer-avatar {
width: 100%;
padding-top: 100%;
border-radius: 50%; /* 圆形头像 */
overflow: hidden;
position: relative;
margin-bottom: 12px; /* 增加间距 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 添加阴影 */
border: 3px solid #fff; /* 添加白色边框 */
}
.singer-avatar img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.singer-item:hover .singer-avatar img {
transform: scale(1.15); /* 悬停时放大 */
}
.singer-name {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #555; /* 稍深颜色 */
font-size: 0.9rem;
}
/* Specific section backgrounds - REMOVED */
</style>