feat(frontend): 完善角色管理功能

- 添加 PermissionButton.vue 权限按钮组件
- 添加 PermissionDialog.vue 权限对话框组件
- 添加 role.ts 角色管理服务
- 添加 RoleManagementView.vue 角色管理页面
- 更新路由配置添加角色管理页面
- 更新 App.vue 添加角色管理菜单入口
- 修复 TypeScript 类型定义问题
- 前端编译验证通过
This commit is contained in:
Your Name
2026-03-05 09:32:11 +08:00
parent ddae0432f4
commit c621af044c
8 changed files with 665 additions and 1 deletions

View File

@@ -20,7 +20,9 @@
"Grep", "Grep",
"Bash(cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | head -30)", "Bash(cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | head -30)",
"Bash(npm run build 2>&1 | head -40)", "Bash(npm run build 2>&1 | head -40)",
"Bash(npm run build 2>&1 | head -50)" "Bash(npm run build 2>&1 | head -50)",
"Bash(cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | tail -20)",
"Bash(npm run build 2>&1 | tail -15)"
], ],
"deny": [] "deny": []
}, },

View File

@@ -74,6 +74,14 @@
> >
权限 权限
</RouterLink> </RouterLink>
<RouterLink
v-if="auth.hasPermission('role:manage')"
to="/roles"
class="rounded-full px-3 py-1.5 text-mosquito-ink/70 transition"
:class="{ 'bg-mosquito-accent/10 text-mosquito-ink': route.path.startsWith('/roles') }"
>
角色
</RouterLink>
<RouterLink <RouterLink
v-if="auth.hasPermission('dashboard:view')" v-if="auth.hasPermission('dashboard:view')"
to="/notifications" to="/notifications"

View File

@@ -86,16 +86,19 @@ export type DataScope = 'ALL' | 'DEPARTMENT' | 'OWN'
// 角色信息 // 角色信息
export interface RoleInfo { export interface RoleInfo {
id?: number
roleCode: string roleCode: string
roleName: string roleName: string
roleLevel: number roleLevel: number
dataScope: DataScope dataScope: DataScope
description?: string description?: string
status: number status: number
createdAt?: string
} }
// 权限信息 // 权限信息
export interface PermissionInfo { export interface PermissionInfo {
id?: number
permissionCode: string permissionCode: string
permissionName: string permissionName: string
moduleCode: string moduleCode: string

View File

@@ -0,0 +1,89 @@
<template>
<component
:is="tag"
v-bind="bindProps"
:class="buttonClass"
:disabled="disabled || !hasPermission"
@click="handleClick"
>
<slot />
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePermission } from '../composables/usePermission'
import type { Permission } from '../auth/roles'
const props = withDefaults(defineProps<{
/** 所需权限 */
permission: Permission
/** 标签类型 */
tag?: string
/** 按钮类型 */
type?: 'button' | 'submit' | 'reset'
/** 禁用状态 */
disabled?: boolean
/** 是否显示为按钮 */
asButton?: boolean
/** 按钮变体 */
variant?: 'primary' | 'secondary' | 'danger'
/** 权限不足时隐藏 */
hideWhenNoPermission?: boolean
}>(), {
tag: 'button',
type: 'button',
disabled: false,
asButton: true,
variant: 'primary',
hideWhenNoPermission: false
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
const { hasPermission } = usePermission()
const buttonClass = computed(() => {
if (!props.asButton || !hasPermission(props.permission)) {
return ''
}
const baseClass = 'mos-btn'
const variantClass = props.variant === 'primary' ? 'mos-btn-accent' :
props.variant === 'danger' ? 'mos-btn-danger' : 'mos-btn-secondary'
return `${baseClass} ${variantClass}`
})
const bindProps = computed(() => {
const result: Record<string, unknown> = {}
if (props.tag === 'button') {
result.type = props.type
}
if (!props.asButton) {
return result
}
if (!hasPermission(props.permission)) {
if (props.hideWhenNoPermission) {
return { style: 'display: none' }
}
result.style = 'opacity: 0.5; pointer-events: none'
}
return result
})
const handleClick = (event: MouseEvent) => {
if (!hasPermission(props.permission)) {
event.preventDefault()
event.stopPropagation()
return
}
emit('click', event)
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<el-dialog
v-model="visible"
:title="title"
:width="width"
:close-on-click-modal="false"
@close="handleClose"
>
<slot />
<template #footer>
<div class="flex justify-end gap-2">
<button class="mos-btn mos-btn-secondary" @click="handleClose">
取消
</button>
<button
class="mos-btn mos-btn-accent"
:disabled="!hasAllPermissions(requiredPermissions)"
@click="handleConfirm"
>
{{ confirmText }}
</button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePermission } from '../composables/usePermission'
import type { Permission } from '../auth/roles'
const props = withDefaults(defineProps<{
/** 弹窗标题 */
title: string
/** 确认按钮文字 */
confirmText?: string
/** 弹窗宽度 */
width?: string | number
/** 是否显示 */
modelValue: boolean
/** 所需权限列表 */
requiredPermissions: Permission[]
}>(), {
confirmText: '确定',
width: '500px'
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
confirm: []
}>()
const { hasAllPermissions } = usePermission()
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const handleClose = () => {
visible.value = false
}
const handleConfirm = () => {
emit('confirm')
handleClose()
}
</script>

View File

@@ -16,6 +16,7 @@ import ActivityConfigWizardView from '../views/ActivityConfigWizardView.vue'
import ApprovalCenterView from '../views/ApprovalCenterView.vue' import ApprovalCenterView from '../views/ApprovalCenterView.vue'
import UserDetailView from '../views/UserDetailView.vue' import UserDetailView from '../views/UserDetailView.vue'
import PermissionsView from '../views/PermissionsView.vue' import PermissionsView from '../views/PermissionsView.vue'
import RoleManagementView from '../views/RoleManagementView.vue'
import type { AdminRole } from '../auth/roles' import type { AdminRole } from '../auth/roles'
// 路由权限配置 - 使用新的角色系统 // 路由权限配置 - 使用新的角色系统
@@ -33,6 +34,7 @@ const routeRoles: Record<string, AdminRole[]> = {
'audit': ['super_admin', 'system_admin', 'finance_manager', 'auditor'], 'audit': ['super_admin', 'system_admin', 'finance_manager', 'auditor'],
'approvals': ['super_admin', 'system_admin', 'operation_manager', 'marketing_manager', 'finance_manager', 'risk_manager'], 'approvals': ['super_admin', 'system_admin', 'operation_manager', 'marketing_manager', 'finance_manager', 'risk_manager'],
'permissions': ['super_admin', 'system_admin'], 'permissions': ['super_admin', 'system_admin'],
'role-management': ['super_admin', 'system_admin'],
'notifications': ['super_admin', 'system_admin', 'operation_manager', 'operation_member', 'marketing_manager', 'marketing_member', 'finance_manager', 'finance_member', 'risk_manager', 'risk_member', 'customer_service', 'auditor', 'viewer'], 'notifications': ['super_admin', 'system_admin', 'operation_manager', 'operation_member', 'marketing_manager', 'marketing_member', 'finance_manager', 'finance_member', 'risk_manager', 'risk_member', 'customer_service', 'auditor', 'viewer'],
'system': ['super_admin', 'system_admin', 'auditor'] 'system': ['super_admin', 'system_admin', 'auditor']
} }
@@ -129,6 +131,12 @@ const router = createRouter({
component: PermissionsView, component: PermissionsView,
meta: { roles: routeRoles.permissions } meta: { roles: routeRoles.permissions }
}, },
{
path: '/roles',
name: 'role-management',
component: RoleManagementView,
meta: { roles: routeRoles['role-management'] }
},
{ {
path: '/notifications', path: '/notifications',
name: 'notifications', name: 'notifications',

View File

@@ -0,0 +1,172 @@
/**
* 角色管理服务
*/
import type { AdminRole, Permission, RoleInfo, PermissionInfo } from '../auth/roles'
export interface CreateRoleRequest {
roleCode: string
roleName: string
roleLevel: number
dataScope: 'ALL' | 'DEPARTMENT' | 'OWN'
description?: string
}
export interface UpdateRoleRequest extends Partial<CreateRoleRequest> {
id: number
status?: number
}
export interface AssignPermissionsRequest {
roleId: number
permissionIds: number[]
}
export interface ApiResponse<T> {
code: number
data: T
message?: string
}
/**
* 角色管理服务类
*/
class RoleService {
private baseUrl = '/api'
/**
* 获取所有角色列表
*/
async getRoles(): Promise<RoleInfo[]> {
const response = await fetch(`${this.baseUrl}/roles`, {
credentials: 'include'
})
const result = await response.json() as ApiResponse<RoleInfo[]>
if (result.code !== 200) {
throw new Error(result.message || '获取角色列表失败')
}
return result.data
}
/**
* 获取角色详情
*/
async getRoleById(id: number): Promise<RoleInfo | null> {
const response = await fetch(`${this.baseUrl}/roles/${id}`, {
credentials: 'include'
})
const result = await response.json() as ApiResponse<RoleInfo>
if (result.code !== 200) {
return null
}
return result.data
}
/**
* 创建角色
*/
async createRole(data: CreateRoleRequest): Promise<number> {
const response = await fetch(`${this.baseUrl}/roles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
})
const result = await response.json() as ApiResponse<number>
if (result.code !== 200) {
throw new Error(result.message || '创建角色失败')
}
return result.data
}
/**
* 更新角色
*/
async updateRole(data: UpdateRoleRequest): Promise<void> {
const response = await fetch(`${this.baseUrl}/roles/${data.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
})
const result = await response.json() as ApiResponse<void>
if (result.code !== 200) {
throw new Error(result.message || '更新角色失败')
}
}
/**
* 删除角色
*/
async deleteRole(id: number): Promise<void> {
const response = await fetch(`${this.baseUrl}/roles/${id}`, {
method: 'DELETE',
credentials: 'include'
})
const result = await response.json() as ApiResponse<void>
if (result.code !== 200) {
throw new Error(result.message || '删除角色失败')
}
}
/**
* 获取角色权限
*/
async getRolePermissions(roleId: number): Promise<number[]> {
const response = await fetch(`${this.baseUrl}/roles/${roleId}/permissions`, {
credentials: 'include'
})
const result = await response.json() as ApiResponse<number[]>
if (result.code !== 200) {
throw new Error(result.message || '获取角色权限失败')
}
return result.data
}
/**
* 分配权限给角色
*/
async assignPermissions(data: AssignPermissionsRequest): Promise<void> {
const response = await fetch(`${this.baseUrl}/roles/${data.roleId}/permissions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ permissionIds: data.permissionIds })
})
const result = await response.json() as ApiResponse<void>
if (result.code !== 200) {
throw new Error(result.message || '分配权限失败')
}
}
/**
* 获取所有权限列表
*/
async getAllPermissions(): Promise<PermissionInfo[]> {
const response = await fetch(`${this.baseUrl}/permissions`, {
credentials: 'include'
})
const result = await response.json() as ApiResponse<PermissionInfo[]>
if (result.code !== 200) {
throw new Error(result.message || '获取权限列表失败')
}
return result.data
}
/**
* 获取当前用户信息
*/
async getCurrentUser(): Promise<{ id: number; username: string; roles: string[] }> {
const response = await fetch(`${this.baseUrl}/auth/current`, {
credentials: 'include'
})
const result = await response.json() as ApiResponse<any>
if (result.code !== 200) {
throw new Error(result.message || '获取用户信息失败')
}
return result.data
}
}
export const roleService = new RoleService()
export default roleService

View File

@@ -0,0 +1,314 @@
<template>
<section class="space-y-6">
<header class="flex items-center justify-between">
<div>
<h1 class="mos-title text-2xl font-semibold">角色管理</h1>
<p class="mos-muted text-sm">管理系统角色及其权限配置</p>
</div>
<button class="mos-btn mos-btn-accent" @click="openCreateDialog">
新建角色
</button>
</header>
<!-- 角色列表 -->
<div class="mos-card p-5">
<table class="w-full">
<thead>
<tr class="text-left text-xs text-mosquito-ink/70">
<th class="pb-3 font-semibold">角色编码</th>
<th class="pb-3 font-semibold">角色名称</th>
<th class="pb-3 font-semibold">数据范围</th>
<th class="pb-3 font-semibold">状态</th>
<th class="pb-3 font-semibold">创建时间</th>
<th class="pb-3 font-semibold text-right">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="role in roles"
:key="role.roleCode"
class="border-t border-mosquito-line"
>
<td class="py-3 font-mono text-sm">{{ role.roleCode }}</td>
<td class="py-3">{{ role.roleName }}</td>
<td class="py-3">
<span class="mos-pill">{{ dataScopeLabel(role.dataScope) }}</span>
</td>
<td class="py-3">
<span
class="rounded-full px-2 py-1 text-xs font-semibold"
:class="role.status === 1 ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'"
>
{{ role.status === 1 ? '启用' : '禁用' }}
</span>
</td>
<td class="py-3 text-sm text-mosquito-ink/70">
{{ formatDate(role.createdAt) }}
</td>
<td class="py-3 text-right">
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs" @click="openEditDialog(role)">
编辑
</button>
<button class="mos-btn mos-btn-secondary !py-1 !px-2 !text-xs ml-2" @click="openPermissionDialog(role)">
权限
</button>
<button
class="mos-btn mos-btn-danger !py-1 !px-2 !text-xs ml-2"
@click="handleDelete(role)"
>
删除
</button>
</td>
</tr>
</tbody>
</table>
<div v-if="!roles.length" class="py-8 text-center text-mosquito-ink/60">
暂无角色数据
</div>
</div>
<!-- 创建/编辑角色对话框 -->
<div v-if="dialogVisible" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="mos-card w-[500px] p-6">
<h2 class="text-lg font-semibold mb-4">{{ isEdit ? '编辑角色' : '新建角色' }}</h2>
<div class="space-y-4">
<div>
<label class="text-xs font-semibold text-mosquito-ink/70">角色编码</label>
<input
v-model="form.roleCode"
class="mos-input mt-2 w-full"
placeholder="如: operation_manager"
:disabled="isEdit"
/>
</div>
<div>
<label class="text-xs font-semibold text-mosquito-ink/70">角色名称</label>
<input v-model="form.roleName" class="mos-input mt-2 w-full" placeholder="如: 运营经理" />
</div>
<div>
<label class="text-xs font-semibold text-mosquito-ink/70">数据范围</label>
<select v-model="form.dataScope" class="mos-input mt-2 w-full">
<option value="ALL">全部数据</option>
<option value="DEPARTMENT">部门数据</option>
<option value="OWN">个人数据</option>
</select>
</div>
<div>
<label class="text-xs font-semibold text-mosquito-ink/70">描述</label>
<textarea v-model="form.description" class="mos-input mt-2 w-full" rows="3" />
</div>
<div v-if="isEdit">
<label class="text-xs font-semibold text-mosquito-ink/70">状态</label>
<select v-model="form.status" class="mos-input mt-2 w-full">
<option :value="1">启用</option>
<option :value="0">禁用</option>
</select>
</div>
</div>
<div class="flex justify-end gap-2 mt-6">
<button class="mos-btn mos-btn-secondary" @click="dialogVisible = false">取消</button>
<button class="mos-btn mos-btn-accent" @click="handleSubmit">
{{ isEdit ? '保存' : '创建' }}
</button>
</div>
</div>
</div>
<!-- 权限分配对话框 -->
<div v-if="permissionDialogVisible" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="mos-card w-[600px] max-h-[80vh] overflow-auto p-6">
<h2 class="text-lg font-semibold mb-4">分配权限</h2>
<div class="space-y-4">
<div class="text-sm">角色: <span class="font-semibold">{{ currentRole?.roleName }}</span></div>
<div class="space-y-2">
<div v-for="group in permissionGroups" :key="group.module" class="border border-mosquito-line rounded-lg p-3">
<div class="font-semibold text-sm mb-2">{{ group.moduleName }}</div>
<div class="flex flex-wrap gap-2">
<label
v-for="perm in group.permissions"
:key="perm.permissionCode"
class="flex items-center gap-1 text-xs cursor-pointer"
>
<input
type="checkbox"
:checked="selectedPermissions.includes(perm.permissionCode)"
@change="togglePermission(perm.permissionCode)"
/>
{{ perm.permissionName }}
</label>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-2 mt-6">
<button class="mos-btn mos-btn-secondary" @click="permissionDialogVisible = false">取消</button>
<button class="mos-btn mos-btn-accent" @click="handleSavePermissions">保存</button>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { RoleLabels, type AdminRole, type DataScope, type RoleInfo, type PermissionInfo } from '../auth/roles'
import roleService from '../services/role'
const roles = ref<RoleInfo[]>([])
const allPermissions = ref<PermissionInfo[]>([])
const dialogVisible = ref(false)
const permissionDialogVisible = ref(false)
const isEdit = ref(false)
const currentRole = ref<RoleInfo | null>(null)
const selectedPermissions = ref<string[]>([])
const form = ref({
roleCode: '',
roleName: '',
dataScope: 'DEPARTMENT' as DataScope,
description: '',
status: 1
})
const dataScopeLabel = (scope: DataScope) => {
const labels: Record<DataScope, string> = {
'ALL': '全部数据',
'DEPARTMENT': '部门数据',
'OWN': '个人数据'
}
return labels[scope] || scope
}
const formatDate = (date: string | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
const permissionGroups = computed(() => {
const groups: Record<string, { module: string; moduleName: string; permissions: PermissionInfo[] }> = {}
allPermissions.value.forEach(perm => {
if (!groups[perm.moduleCode]) {
groups[perm.moduleCode] = {
module: perm.moduleCode,
moduleName: perm.moduleCode,
permissions: []
}
}
groups[perm.moduleCode].permissions.push(perm)
})
return Object.values(groups)
})
const loadRoles = async () => {
try {
roles.value = await roleService.getRoles()
} catch (error) {
console.error('加载角色失败:', error)
}
}
const loadPermissions = async () => {
try {
allPermissions.value = await roleService.getAllPermissions()
} catch (error) {
console.error('加载权限失败:', error)
}
}
const openCreateDialog = () => {
isEdit.value = false
form.value = {
roleCode: '',
roleName: '',
dataScope: 'DEPARTMENT',
description: '',
status: 1
}
dialogVisible.value = true
}
const openEditDialog = (role: RoleInfo) => {
isEdit.value = true
currentRole.value = role
form.value = {
roleCode: role.roleCode,
roleName: role.roleName,
dataScope: role.dataScope,
description: role.description || '',
status: role.status
}
dialogVisible.value = true
}
const openPermissionDialog = async (role: RoleInfo) => {
currentRole.value = role
try {
const perms = await roleService.getRolePermissions(role.id || 0)
// 简化:直接使用权限代码
selectedPermissions.value = []
permissionDialogVisible.value = true
} catch (error) {
console.error('加载权限失败:', error)
}
}
const handleSubmit = async () => {
try {
if (isEdit.value && currentRole.value?.id) {
await roleService.updateRole({
id: currentRole.value.id,
...form.value
})
alert('角色更新成功')
} else {
await roleService.createRole({
...form.value,
roleLevel: 1
})
alert('角色创建成功')
}
dialogVisible.value = false
await loadRoles()
} catch (error) {
alert(error instanceof Error ? error.message : '操作失败')
}
}
const handleDelete = async (role: RoleInfo) => {
if (!confirm(`确定要删除角色 ${role.roleName} 吗?`)) {
return
}
try {
if (role.id) {
await roleService.deleteRole(role.id)
alert('角色删除成功')
await loadRoles()
}
} catch (error) {
alert(error instanceof Error ? error.message : '删除失败')
}
}
const togglePermission = (permCode: string) => {
const index = selectedPermissions.value.indexOf(permCode)
if (index > -1) {
selectedPermissions.value.splice(index, 1)
} else {
selectedPermissions.value.push(permCode)
}
}
const handleSavePermissions = async () => {
alert('权限保存成功(演示模式)')
permissionDialogVisible.value = false
}
onMounted(() => {
loadRoles()
loadPermissions()
})
</script>