用户管理组件(带角色修改功能)存档文档
用户管理组件(带角色修改功能)存档文档
一、组件概述
组件名称:UserManagement.vue
功能描述:企业级用户管理界面,支持用户列表展示、多条件筛选、报表导出及角色修改功能,基于Vue 3 + TypeScript + Quasar框架实现。
核心特性:
- 精细化角色权限控制(5种角色类型)
- 完整的用户生命周期管理(筛选、搜索、分页)
- 安全审计日志集成
- 响应式设计适配多端
- 企业级错误处理与用户反馈
- 角色定义:5种预设角色(隐患排查员、治理员、安全管理员等)
- 权限控制:基于角色的操作权限校验(isAdmin计算属性)
- 角色修改:通过UserRoleEditor组件实现可视化角色分配
- 数据加载:异步获取用户列表(fetchUserList方法)
- 筛选系统:多维度筛选(角色/状态/关键词)
- 分页控制:支持自定义每页条数与页码导航
- 操作日志:关键行为记录(logSecurityEvent)
- 权限校验:防止非管理员访问(前端拦截+后端验证)
- 敏感操作保护:禁止用户修改自身角色
二、核心功能模块
1. 角色管理系统
2. 用户数据管理
3. 安全审计
三、完整代码实现
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from 'stores/auth'
import {
date,
exportFile,
Notify,
QAvatar,
QBadge,
QDialog,
QIcon,
QInput,
QPagination,
QSelect,
QTable,
type QTableColumn,
QTd,
} from 'quasar'
import type { AxiosResponse } from 'axios'
import { type UserProfile, UserRole } from 'src/types/auth/user'
import { apiClient } from 'src/services/axios'
import { extractFilename } from 'src/utils/file/fileDownload'
import { logSecurityEvent } from 'src/services/audit'
import UserRoleEditor from 'src/components/UserRoleEditor.vue'
import type { RoleOption } from 'src/components/types'
// 状态管理
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
const exportLoading = ref(false)
const error = ref<string | null>(null)
const userList = ref<UserProfile[]>([])
const pagination = ref({
page: 1,
rowsPerPage: 10,
rowsNumber: 0,
})
const filterOptions = ref({
role: null as number | null,
status: null as number | null,
department: null as number | null,
search: '',
})
// 角色编辑相关状态
const showRoleEditor = ref(false)
const editingUser = ref<UserProfile | null>(null)
const isUpdatingRole = ref(false)
const roleUpdateError = ref<string | null>(null)
// 角色选项配置(含权限描述)
const roleOptions: RoleOption[] = [
{
label: '隐患排查员',
value: UserRole.HAZARD_INSPECTOR,
icon: 'mdi-magnify',
color: 'blue',
description: '发现和报告安全隐患',
permissions: ['创建隐患报告', '查看自有报告', '上传现场照片'],
restrictions: ['不能分配任务', '不能关闭报告'],
},
{
label: '隐患治理员',
value: UserRole.HAZARD_RECTIFIER,
icon: 'mdi-account-hard-hat',
color: 'teal',
description: '整改验证和闭环管理',
permissions: ['分配整改任务', '验证整改结果', '关闭隐患'],
restrictions: ['不能创建新报告', '不能修改原始描述'],
},
{
label: '安全管理员',
value: UserRole.SECURITY_ADMIN,
icon: 'mdi-shield-account',
color: 'green',
description: '整体安全管理和用户配置',
permissions: ['用户管理', '角色分配', '系统配置'],
restrictions: ['不能操作审计日志', '不能提升自身权限'],
},
{
label: '安全审计员',
value: UserRole.AUDITOR,
icon: 'mdi-clipboard-text-search',
color: 'orange',
description: '独立审计和监督',
permissions: ['查看所有操作日志', '导出审计数据', '标记可疑操作'],
restrictions: ['不能修改业务数据', '不能执行管理操作'],
},
{
label: '系统管理员',
value: UserRole.SYSTEM_ADMIN,
icon: 'mdi-server-security',
color: 'red',
description: '基础设施维护',
permissions: ['服务器管理', '备份恢复', '系统监控'],
restrictions: ['不能访问业务数据', '操作需双重认证'],
},
]
// 状态选项配置
const statusOptions = [
{ label: '正常', value: 0 },
{ label: '已锁定', value: 1 },
{ label: '已停用', value: 2 },
{ label: '密码过期', value: 3 },
{ label: '已删除', value: 4 },
]
// 权限检查计算属性
const isAdmin = computed(() => {
return (
authStore.userRole === UserRole.SYSTEM_ADMIN ||
authStore.userRole === UserRole.SECURITY_ADMIN
)
})
// 表格列定义
const columns: QTableColumn[] = [
{ name: 'avatar', label: '头像', field: 'avatar', align: 'center', sortable: false },
{ name: 'nickname', label: '昵称', field: 'nickname', align: 'left', sortable: true },
{ name: 'role', label: '角色', field: 'role_display', align: 'center', sortable: true },
{
name: 'department',
label: '部门',
field: (row: UserProfile) => row.department_info?.name || '-',
align: 'center',
sortable: true
},
{ name: 'status', label: '状态', field: 'status_display', align: 'center', sortable: true },
{
name: 'last_login',
label: '最后登录',
field: (row: UserProfile) =>
row.last_login ? date.formatDate(row.last_login, 'YYYY-MM-DD HH:mm:ss') : '从未登录',
align: 'center',
sortable: true
},
{ name: 'actions', label: '操作', align: 'center', sortable: false, field: '' },
{ name: 'role_edit', label: '角色修改', align: 'center', sortable: false, field: '' },
]
// 获取用户列表(核心数据接口)
const fetchUserList = async () => {
if (!isAdmin.value) {
error.value = '权限不足:仅管理员可访问用户列表'
loading.value = false
userList.value = []
return
}
try {
loading.value = true
error.value = null
const params: Record<string, unknown> = {
page: pagination.value.page,
page_size: pagination.value.rowsPerPage,
}
if (filterOptions.value.role !== null) params.role = filterOptions.value.role
if (filterOptions.value.status !== null) params.status = filterOptions.value.status
if (filterOptions.value.department !== null) params.department = filterOptions.value.department
if (filterOptions.value.search) params.search = filterOptions.value.search
const response: AxiosResponse<UserProfile[]> = await apiClient.get('/users/profile/', { params })
userList.value = response.data || []
pagination.value.rowsNumber = response.data.length || 0
} catch (err) {
handleListError(err)
userList.value = []
} finally {
loading.value = false
}
}
// 错误处理函数
const handleListError = (err: unknown) => {
let errorMessage = '获取用户列表失败,请稍后重试'
if (typeof err === 'object' && err !== null && 'response' in err) {
const axiosError = err as { response?: { status: number; data?: unknown } }
if (axiosError.response?.status === 403) {
errorMessage = '权限不足:仅管理员可访问用户列表'
} else if (axiosError.response?.data && typeof axiosError.response.data === 'string') {
errorMessage = `服务器错误:${axiosError.response.data}`
}
}
error.value = errorMessage
}
// 状态标签颜色映射
const getStatusColor = (status: string) => {
switch (status) {
case '正常': return 'positive'
case '已锁定': return 'negative'
case '已停用': return 'grey'
case '密码过期': return 'warning'
case '已删除': return 'dark'
default: return 'info'
}
}
// 查看用户详情
const viewUserDetail = async (userId: string) => {
await router.push(`/system/profile/${userId}`)
}
// 重置过滤器
const resetFilters = async () => {
filterOptions.value = { role: null, status: null, department: null, search: '' }
await fetchUserList()
}
// 分页变化处理
const onPaginationChange = (newPagination: { page: number; rowsPerPage: number }) => {
pagination.value.page = newPagination.page
pagination.value.rowsPerPage = newPagination.rowsPerPage
}
// 导出报表功能
const exportReport = async () => {
try {
exportLoading.value = true
const params: Record<string, unknown> = {}
if (filterOptions.value.role !== null) params.role = filterOptions.value.role
if (filterOptions.value.status !== null) params.status = filterOptions.value.status
if (filterOptions.value.department !== null) params.department = filterOptions.value.department
if (filterOptions.value.search) params.search = filterOptions.value.search
const response = await apiClient.get('/users/export/', {
params,
responseType: 'blob',
})
const contentDisposition = response.headers['content-disposition']
const filename = extractFilename('用户列表', contentDisposition)
const status = exportFile(filename, response.data)
if (status === true) {
Notify.create({
type: 'positive',
message: '报表导出成功',
position: 'top-right',
timeout: 3000,
})
logSecurityEvent('REPORT_EXPORTED', { '事件详情': `文件名:${filename}` })
} else {
Notify.create({
type: 'warning',
message: '浏览器阻止了文件下载,请检查弹出窗口设置',
position: 'top-right',
timeout: 5000,
})
}
} catch (err) {
console.error('导出失败:', err)
Notify.create({
type: 'negative',
message: '报表导出失败',
position: 'top-right',
timeout: 5000,
})
} finally {
exportLoading.value = false
}
}
// 打开角色编辑器
const openRoleEditor = (user: UserProfile) => {
editingUser.value = user
showRoleEditor.value = true
roleUpdateError.value = null
}
// 更新用户角色(核心业务逻辑)
const updateUserRole = async () => {
if (!editingUser.value) return
try {
isUpdatingRole.value = true
roleUpdateError.value = null
const updateData = {
role: editingUser.value.role,
update_reason: `由管理员 ${authStore.user?.nickname || authStore.user?.username} 修改角色`
}
await apiClient.patch(`/users/profile/${editingUser.value.id}/`, updateData)
// 本地数据更新
const updatedUser = userList.value.find(u => u.id === editingUser.value?.id)
if (updatedUser) {
updatedUser.role = editingUser.value.role
const roleOption = roleOptions.find(ro => ro.value === editingUser.value?.role)
if (roleOption) updatedUser.role_display = roleOption.label
}
Notify.create({ type: 'positive', message: '用户角色更新成功', position: 'top', timeout: 2000 })
logSecurityEvent('USER_ROLE_UPDATED', {
targetUserId: editingUser.value.id,
updatedBy: authStore.userId,
newRole: editingUser.value.role,
updateReason: updateData.update_reason
})
showRoleEditor.value = false
} catch (err) {
console.error('更新角色失败:', err)
roleUpdateError.value = '更新角色失败,请稍后重试'
if (typeof err === 'object' && err !== null && 'response' in err) {
const axiosError = err as { response?: { status: number; data?: { detail?: string } } }
if (axiosError.response?.status === 403) {
roleUpdateError.value = '权限不足:无法修改用户角色'
} else if (axiosError.response?.data?.detail) {
roleUpdateError.value = axiosError.response.data.detail
}
}
} finally {
isUpdatingRole.value = false
}
}
// 初始化加载
onMounted(async () => {
await fetchUserList()
})
</script>
<template>
<div class="q-pa-lg">
<!-- 权限提示 -->
<div v-if="!isAdmin" class="text-center q-pa-xl">
<q-icon name="security" size="xl" color="negative" />
<div class="text-h6 q-mt-md text-negative">您没有权限访问用户列表</div>
<q-btn label="返回首页" color="primary" class="q-mt-md" @click="router.push('/')" />
</div>
<!-- 管理员视图 -->
<div v-else>
<!-- 标题和统计 -->
<div class="row items-center q-mb-md">
<div class="col">
<div class="text-h5">用户管理</div>
<div class="text-caption text-grey">
共 {{ pagination.rowsNumber }} 名用户
<span v-if="filterOptions.search"> | 搜索: "{{ filterOptions.search }}"</span>
</div>
</div>
<div class="col-auto">
<q-btn
label="导出报表"
color="secondary"
icon="description"
class="q-mr-sm"
@click="exportReport"
:loading="exportLoading"
:disable="exportLoading || loading || pagination.rowsNumber === 0"
>
<template v-slot:loading>
<q-spinner-hourglass class="on-left" />
导出中...
</template>
</q-btn>
</div>
</div>
<!-- 过滤区域 -->
<div class="row q-gutter-md q-mb-lg">
<div class="col-3">
<q-select
v-model="filterOptions.role"
label="按角色筛选"
:options="roleOptions.map(ro => ({ label: ro.label, value: ro.value }))"
emit-value
map-options
clearable
filled
autocomplete=""
/>
</div>
<div class="col-2">
<q-select
v-model="filterOptions.status"
label="按状态筛选"
:options="statusOptions"
emit-value
map-options
clearable
filled
autocomplete=""
/>
</div>
<div class="col-3">
<q-input
v-model="filterOptions.search"
label="搜索用户"
clearable
filled
@update:model-value="fetchUserList"
debounce="500"
autofocus
>
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</div>
<div class="col-auto self-end">
<q-btn label="筛选" color="primary" icon="filter_list" @click="fetchUserList" />
</div>
<div class="col-auto self-end">
<q-btn label="重置" color="grey" icon="refresh" @click="resetFilters" />
</div>
</div>
<!-- 错误提示 -->
<q-banner v-if="error" class="bg-negative text-white q-mb-md">
<template v-slot:avatar>
<q-icon name="error" color="white" />
</template>
{{ error }}
</q-banner>
<!-- 加载状态 -->
<div v-if="loading" class="text-center q-pa-xl">
<q-spinner-gears size="xl" color="primary" />