eagleye

用户管理组件(带角色修改功能)存档文档

用户管理组件(带角色修改功能)存档文档

一、组件概述

组件名称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" />

 

posted on 2025-08-07 16:38  GoGrid  阅读(15)  评论(0)    收藏  举报

导航