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,513 @@
<script setup>
import { ref, onMounted, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Edit } from '@element-plus/icons-vue'
import { getUserList, deleteUser, updateUserStatus, getUserById, adminUpdateUser } from '../../api/admin/user'
// 用户列表
const userList = ref([])
// 总数
const total = ref(0)
// 加载状态
const loading = ref(false)
// 当前页码
const currentPage = ref(1)
// 每页大小
const pageSize = ref(5)
// 搜索关键字
const keyword = ref('')
// 编辑对话框
const editDialogVisible = ref(false)
const editForm = reactive({
id: null,
username: '',
nickname: '',
email: '',
phone: '',
avatar: '',
gender: 0,
introduction: ''
})
const editFormRules = {
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
email: [
{ pattern: /^\S+@\S+\.\S+$/, message: '请输入正确的邮箱地址', trigger: 'blur' }
],
avatar: [
{ pattern: /^(https?:\/\/)?[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?$/, message: '请输入正确的URL地址', trigger: 'blur' }
]
}
const editFormRef = ref(null)
const editLoading = ref(false)
// 获取用户列表
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(currentPage.value, pageSize.value, keyword.value)
if (res.code === 200) {
userList.value = res.data.list
total.value = res.data.total
} else {
ElMessage.error(res.message || '获取用户列表失败')
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败,请稍后再试')
} finally {
loading.value = false
}
}
// 处理页码变化
const handleCurrentChange = (page) => {
currentPage.value = page
fetchUserList()
}
// 处理每页大小变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchUserList()
}
// 处理搜索
const handleSearch = () => {
currentPage.value = 1
fetchUserList()
}
// 处理重置搜索
const handleResetSearch = () => {
keyword.value = ''
currentPage.value = 1
fetchUserList()
}
// 处理删除用户
const handleDeleteUser = (id) => {
ElMessageBox.confirm('确定要删除该用户吗?此操作不可逆', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteUser(id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchUserList()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除用户失败:', error)
ElMessage.error('删除用户失败,请稍后再试')
}
}).catch(() => {
// 取消删除
})
}
// 处理更新用户状态
const handleUpdateUserStatus = async (id, status) => {
try {
const res = await updateUserStatus(id, status)
if (res.code === 200) {
ElMessage.success('更新成功')
fetchUserList()
} else {
ElMessage.error(res.message || '更新失败')
}
} catch (error) {
console.error('更新用户状态失败:', error)
ElMessage.error('更新用户状态失败,请稍后再试')
}
}
// 打开编辑对话框
const handleEdit = async (id) => {
try {
const res = await getUserById(id)
if (res.code === 200) {
const user = res.data
// 填充表单
editForm.id = user.id
editForm.username = user.username
editForm.nickname = user.nickname || ''
editForm.email = user.email || ''
editForm.phone = user.phone || ''
editForm.avatar = user.avatar || ''
editForm.gender = user.gender || 0
editForm.introduction = user.introduction || ''
// 显示对话框
editDialogVisible.value = true
} else {
ElMessage.error(res.message || '获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败,请稍后再试')
}
}
// 提交编辑表单
const submitEditForm = async () => {
if (!editFormRef.value) return
await editFormRef.value.validate(async (valid) => {
if (valid) {
editLoading.value = true
try {
const res = await adminUpdateUser(editForm.id, {
nickname: editForm.nickname,
email: editForm.email,
phone: editForm.phone,
avatar: editForm.avatar,
gender: editForm.gender,
introduction: editForm.introduction
})
if (res.code === 200) {
ElMessage.success('更新成功')
editDialogVisible.value = false
fetchUserList()
} else {
ElMessage.error(res.message || '更新失败')
}
} catch (error) {
console.error('更新用户信息失败:', error)
ElMessage.error('更新用户信息失败,请稍后再试')
} finally {
editLoading.value = false
}
}
})
}
// 格式化角色
const formatRole = (role) => {
return role === 1 ? '管理员' : '普通用户'
}
// 格式化状态
const formatStatus = (status) => {
return status === 1 ? '正常' : '禁用'
}
// 格式化性别
const formatGender = (gender) => {
switch (gender) {
case 1:
return '男'
case 2:
return '女'
default:
return '未知'
}
}
// 格式化日期
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleString()
}
onMounted(() => {
fetchUserList()
})
</script>
<template>
<div class="user-management">
<div class="page-header">
<h2>用户管理</h2>
</div>
<!-- 搜索区域 -->
<div class="search-container">
<el-input
v-model="keyword"
placeholder="搜索用户名、昵称或邮箱"
clearable
@keyup.enter="handleSearch"
>
<template #append>
<el-button @click="handleSearch">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleResetSearch">重置</el-button>
</div>
<!-- 用户列表 -->
<el-table
:data="userList"
border
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="nickname" label="昵称" width="120" />
<el-table-column prop="email" label="邮箱" width="180" />
<el-table-column label="性别" width="80">
<template #default="scope">
{{ formatGender(scope.row.gender) }}
</template>
</el-table-column>
<el-table-column label="角色" width="100">
<template #default="scope">
<el-tag :type="scope.row.role === 1 ? 'danger' : 'info'">
{{ formatRole(scope.row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ formatStatus(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="注册时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleEdit(scope.row.id)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="scope.row.status === 1"
type="warning"
size="small"
@click="handleUpdateUserStatus(scope.row.id, 0)"
>
禁用
</el-button>
<el-button
v-else
type="success"
size="small"
@click="handleUpdateUserStatus(scope.row.id, 1)"
>
启用
</el-button>
<el-button
type="danger"
size="small"
@click="handleDeleteUser(scope.row.id)"
:disabled="scope.row.role === 1"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 15, 20]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 编辑用户对话框 -->
<el-dialog
v-model="editDialogVisible"
title="编辑用户信息"
width="500px"
:close-on-click-modal="false"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="editFormRules"
label-width="100px"
v-loading="editLoading"
>
<el-form-item label="用户名">
<el-input v-model="editForm.username" disabled></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="editForm.nickname" placeholder="请输入昵称"></el-input>
</el-form-item>
<el-form-item label="头像" prop="avatar">
<el-input v-model="editForm.avatar" placeholder="请输入头像地址"></el-input>
<div class="avatar-preview" v-if="editForm.avatar">
<el-avatar :size="60" :src="editForm.avatar"></el-avatar>
</div>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="editForm.gender">
<el-radio :label="0">未知</el-radio>
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="editForm.email" placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="editForm.phone" placeholder="请输入手机号"></el-input>
</el-form-item>
<el-form-item label="个人简介">
<el-input v-model="editForm.introduction" type="textarea" :rows="3" placeholder="请输入个人简介"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEditForm" :loading="editLoading">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<style scoped>
.user-management {
padding: 20px;
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.page-header {
margin-bottom: 20px;
color: #4a4a4a;
text-align: center;
}
.search-container {
display: flex;
margin-bottom: 20px;
gap: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.search-container .el-input {
width: 300px;
}
.search-container .el-input :deep(.el-input__inner) {
border-radius: 8px;
border: 1px solid #b3e5fc;
transition: all 0.3s;
}
.search-container .el-input :deep(.el-input__inner):focus {
border-color: #4dd0e1;
box-shadow: 0 0 0 2px rgba(77, 208, 225, 0.2);
}
.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);
}
.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);
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
}
.avatar-preview {
margin-top: 10px;
display: flex;
justify-content: center;
}
</style>