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

494 lines
14 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, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '../stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getCurrentUser, updateUserInfo, uploadAvatar } from '../api/user'
import { getCollectedMusic } from '../api/music'
import { getCollectedPlaylist, getUserPlaylist } from '../api/playlist'
import { getCollectedSinger } from '../api/singer'
import { processImageUrl } from '../utils/imageUtils'
const router = useRouter()
const userStore = useUserStore()
// 用户信息
const userInfo = ref(null)
// 用户统计数据
const userStats = ref({
collectMusicCount: 0,
collectPlaylistCount: 0,
collectSingerCount: 0,
createPlaylistCount: 0
})
// 加载状态
const loading = ref(false)
// 编辑表单
const editForm = ref({
nickname: '',
gender: 0,
email: '',
phone: '',
introduction: ''
})
// 是否显示编辑对话框
const showEditDialog = ref(false)
// 头像上传URL
const avatarUploadUrl = ref(`${import.meta.env.VITE_API_BASE_URL || ''}/api/user/upload/avatar`)
// 头像上传headers
const uploadHeaders = ref({
[localStorage.getItem('tokenName') || 'satoken']: userStore.token
})
// 当前激活的菜单
const activeMenu = computed(() => {
const path = router.currentRoute.value.path
if (path.includes('/user/collect/music')) return 'collect-music'
if (path.includes('/user/collect/playlist')) return 'collect-playlist'
if (path.includes('/user/collect/singer')) return 'collect-singer'
if (path.includes('/user/playlist')) return 'playlist'
if (path.includes('/user/password')) return 'password'
return 'profile'
})
// 打开编辑对话框
const openEditDialog = () => {
if (!userInfo.value) return
editForm.value = {
nickname: userInfo.value.nickname || '',
gender: userInfo.value.gender || 0,
email: userInfo.value.email || '',
phone: userInfo.value.phone || '',
introduction: userInfo.value.introduction || ''
}
showEditDialog.value = true
}
// 提交编辑表单
const submitEditForm = async () => {
try {
const res = await updateUserInfo(editForm.value)
if (res.code === 200) {
// 更新成功,重新获取用户信息
await fetchUserInfo()
showEditDialog.value = false
ElMessage.success('个人信息更新成功')
} else {
ElMessage.error(res.message || '更新失败')
}
} catch (error) {
console.error('更新用户信息失败:', error)
ElMessage.error('更新失败,请稍后重试')
}
}
// 头像上传成功
const handleAvatarSuccess = async (response) => {
if (response.code === 200) {
// 上传成功,重新获取用户信息
await fetchUserInfo()
ElMessage.success('头像上传成功')
} else {
ElMessage.error(response.message || '头像上传失败')
}
}
// 头像上传前的处理
const beforeAvatarUpload = (file) => {
const isJPG = file.type === 'image/jpeg'
const isPNG = file.type === 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG && !isPNG) {
ElMessage.error('头像只能是JPG或PNG格式!')
return false
}
if (!isLt2M) {
ElMessage.error('头像大小不能超过2MB!')
return false
}
return true
}
// 导航到子页面
const navigateTo = (path) => {
router.push(path)
}
// 退出登录
const logout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logout()
router.push('/')
ElMessage.success('已退出登录')
}).catch(() => {})
}
// 获取用户信息
const fetchUserInfo = async () => {
loading.value = true
try {
const res = await getCurrentUser()
if (res.code === 200) {
userInfo.value = res.data
// 处理头像图片URL
if (userInfo.value.avatar) {
userInfo.value.avatar = processImageUrl(userInfo.value.avatar)
}
// 更新用户存储
userStore.user = userInfo.value
localStorage.setItem('user', JSON.stringify(userInfo.value))
} else {
ElMessage.error(res.message || '获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败')
} finally {
loading.value = false
}
}
// 获取用户统计数据
const fetchUserStats = async () => {
try {
// 获取收藏的音乐数量
const musicRes = await getCollectedMusic()
if (musicRes.code === 200) {
userStats.value.collectMusicCount = musicRes.data.total || musicRes.data.length || 0
}
// 获取收藏的歌单数量
const playlistRes = await getCollectedPlaylist()
if (playlistRes.code === 200) {
userStats.value.collectPlaylistCount = playlistRes.data.total || playlistRes.data.length || 0
}
// 获取收藏的歌手数量
const singerRes = await getCollectedSinger()
if (singerRes.code === 200) {
userStats.value.collectSingerCount = singerRes.data.total || singerRes.data.length || 0
}
// 获取创建的歌单数量
const userPlaylistRes = await getUserPlaylist()
if (userPlaylistRes.code === 200) {
userStats.value.createPlaylistCount = userPlaylistRes.data.total || userPlaylistRes.data.length || 0
}
} catch (error) {
console.error('获取用户统计数据失败:', error)
}
}
onMounted(async () => {
await fetchUserInfo()
await fetchUserStats()
})
</script>
<template>
<div class="user-center-container" v-loading="loading">
<el-row :gutter="20" style="margin-bottom: 0;">
<!-- 左侧菜单 -->
<el-col :span="6">
<div class="user-sidebar">
<div class="user-avatar-container">
<el-avatar :size="100" :src="userInfo?.avatar || 'https://via.placeholder.com/150?text=用户'" />
<h3 class="user-nickname">{{ userInfo?.nickname || '用户' }}</h3>
</div>
<el-menu
:default-active="activeMenu"
class="user-menu"
>
<el-menu-item index="profile" @click="navigateTo('/user')">
<el-icon><user /></el-icon>
<span>个人资料</span>
</el-menu-item>
<el-menu-item index="collect-music" @click="navigateTo('/user/collect/music')">
<el-icon><headset /></el-icon>
<span>我收藏的音乐</span>
</el-menu-item>
<el-menu-item index="collect-playlist" @click="navigateTo('/user/collect/playlist')">
<el-icon><files /></el-icon>
<span>我收藏的歌单</span>
</el-menu-item>
<el-menu-item index="collect-singer" @click="navigateTo('/user/collect/singer')">
<el-icon><avatar /></el-icon>
<span>我收藏的歌手</span>
</el-menu-item>
<el-menu-item index="playlist" @click="navigateTo('/user/playlist')">
<el-icon><folder /></el-icon>
<span>我的歌单</span>
</el-menu-item>
<el-menu-item index="password" @click="navigateTo('/user/password')">
<el-icon><lock /></el-icon>
<span>修改密码</span>
</el-menu-item>
<el-menu-item index="logout" @click="logout">
<el-icon><switch-button /></el-icon>
<span>退出登录</span>
</el-menu-item>
</el-menu>
</div>
</el-col>
<!-- 右侧内容 -->
<el-col :span="18">
<div class="user-content">
<!-- 如果没有子路由显示个人资料 -->
<div v-if="$route.path === '/user'" class="user-profile">
<div class="profile-header">
<h2>个人资料</h2>
<el-button type="primary" @click="openEditDialog">编辑资料</el-button>
</div>
<el-row :gutter="20" class="profile-stats">
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-item">
<div class="stat-value">{{ userStats.collectMusicCount }}</div>
<div class="stat-label">收藏音乐</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-item">
<div class="stat-value">{{ userStats.collectPlaylistCount }}</div>
<div class="stat-label">收藏歌单</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-item">
<div class="stat-value">{{ userStats.collectSingerCount }}</div>
<div class="stat-label">收藏歌手</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="stat-item">
<div class="stat-value">{{ userStats.createPlaylistCount }}</div>
<div class="stat-label">创建歌单</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card class="profile-info" shadow="never">
<template #header>
<div class="card-header">
<span>基本信息</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="用户名">{{ userInfo?.username || '未设置' }}</el-descriptions-item>
<el-descriptions-item label="昵称">{{ userInfo?.nickname || '未设置' }}</el-descriptions-item>
<el-descriptions-item label="性别">
<el-tag size="small" :type="userInfo?.gender === 1 ? 'primary' : userInfo?.gender === 2 ? 'danger' : 'info'">
{{ userInfo?.gender === 1 ? '男' : userInfo?.gender === 2 ? '女' : '未知' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ userInfo?.email || '未设置' }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ userInfo?.phone || '未设置' }}</el-descriptions-item>
<el-descriptions-item label="个人简介">{{ userInfo?.introduction || '这个人很懒,什么都没留下' }}</el-descriptions-item>
<el-descriptions-item label="注册时间">{{ userInfo?.createTime || '未知' }}</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 子路由内容 -->
<router-view v-else></router-view>
</div>
</el-col>
</el-row>
<!-- 编辑资料对话框 -->
<el-dialog
v-model="showEditDialog"
title="编辑个人资料"
width="500px"
>
<el-form :model="editForm" label-width="80px">
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
:action="avatarUploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="userInfo?.avatar" :src="userInfo.avatar" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><plus /></el-icon>
<div class="avatar-tip">点击上传</div>
</el-upload>
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="editForm.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="editForm.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
<el-radio :label="0">保密</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="editForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机">
<el-input v-model="editForm.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="个人简介">
<el-input
v-model="editForm.introduction"
type="textarea"
:rows="4"
placeholder="请输入个人简介"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" @click="submitEditForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.user-center-container {
padding: 20px 20px 100px 20px; /* 增加底部填充,为播放器留出空间 */
}
.user-sidebar {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
}
.user-avatar-container {
padding: 30px 0;
text-align: center;
background-color: #f5f7fa;
}
.user-nickname {
margin-top: 15px;
margin-bottom: 0;
font-size: 1.2rem;
}
.user-menu {
border-right: none;
margin-bottom: 0;
}
.user-content {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 20px;
min-height: 500px;
margin-bottom: 20px;
}
.profile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.profile-header h2 {
margin: 0;
font-size: 1.5rem;
color: #333;
}
.profile-stats {
margin-bottom: 30px;
}
.stat-item {
text-align: center;
padding: 10px 0;
}
.stat-value {
font-size: 1.8rem;
font-weight: bold;
color: #409eff;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
.profile-info {
margin-bottom: 20px;
}
.card-header {
font-weight: bold;
}
.avatar-uploader {
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
border: 1px dashed #d9d9d9;
border-radius: 50%;
}
.avatar-tip {
margin-top: 5px;
font-size: 0.8rem;
color: #999;
}
</style>