Files
graduation-design/music-qianduan/src/views/admin/UserManagement.vue

593 lines
15 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, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Edit, Plus } from '@element-plus/icons-vue'
import { getUserList, deleteUser, updateUserStatus, getUserById, adminUpdateUser, adminUploadUserAvatar } 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' }
]
}
const editFormRef = ref(null)
const editLoading = ref(false)
const avatarUploading = ref(false)
const handleAvatarBeforeUpload = (file) => {
const isAllowedType = ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)
const isLt2M = file.size / 1024 / 1024 < 2
if (!isAllowedType) {
ElMessage.error('头像仅支持 JPG/PNG/WEBP 格式')
return false
}
if (!isLt2M) {
ElMessage.error('头像大小不能超过 2MB')
return false
}
return true
}
const handleAvatarUpload = async ({ file }) => {
if (!editForm.id) {
ElMessage.error('请先选择要编辑的用户')
return
}
avatarUploading.value = true
try {
const res = await adminUploadUserAvatar(editForm.id, file)
if (res.code === 200) {
editForm.avatar = res.data
ElMessage.success('头像上传成功')
} else {
ElMessage.error(res.message || '头像上传失败')
}
} catch (error) {
console.error('上传头像失败:', error)
ElMessage.error('头像上传失败,请稍后再试')
} finally {
avatarUploading.value = false
}
}
const handleAvatarRemove = () => {
editForm.avatar = ''
}
// 获取用户列表
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="头像">
<el-upload
class="avatar-upload"
:http-request="handleAvatarUpload"
:before-upload="handleAvatarBeforeUpload"
:show-file-list="false"
accept=".jpg,.jpeg,.png,.webp"
:disabled="avatarUploading || editLoading"
>
<div class="avatar-uploader-content" v-loading="avatarUploading">
<el-avatar v-if="editForm.avatar" :size="60" :src="editForm.avatar"></el-avatar>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</div>
</el-upload>
<div class="avatar-upload-actions" v-if="editForm.avatar">
<el-button link type="danger" @click="handleAvatarRemove">移除头像</el-button>
</div>
<div class="el-upload__tip">支持 jpg/png/webp大小不超过 2MB</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-upload :deep(.el-upload) {
border: 1px dashed #b3e5fc;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
overflow: hidden;
}
.avatar-upload :deep(.el-upload):hover {
border-color: #4dd0e1;
}
.avatar-uploader-content {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: #f8fdff;
}
.avatar-uploader-icon {
font-size: 24px;
color: #8c939d;
}
.avatar-upload-actions {
margin-top: 8px;
}
</style>