Compare commits

...

2 Commits

Author SHA1 Message Date
ca4c17042d
首页菜单按权限过滤 2026-04-02 20:47:29 +08:00
a4f7db415e
权限系统重构:
- 角色模型改为 permissions 字段,移除 methods/includeUri/excludeUri
- 角色管理页面支持权限树选择,保存时只传最上级权限
- /common/verifyToken 返回 permissions,存入 vuex
- 新增 v-permission 指令,所有操作按钮和菜单均按权限控制显示
- 菜单按 list 权限过滤
- 各业务页面按钮加权限指令
- 角色管理列表只显示名称/描述/时间,不显示权限列,权限必选
2026-04-02 20:38:14 +08:00
17 changed files with 281 additions and 106 deletions

View File

@ -1,24 +1,39 @@
export default [ export interface MenuItem {
title: string
path: string
permission: string // 需要的权限标识list类型
}
export interface MenuGroup {
name: string
title: string
icon: string
child: MenuItem[]
}
const menus: MenuGroup[] = [
{ {
name: 'system', name: 'system',
title: '系统管理', title: '系统管理',
icon: 'Operation', icon: 'Operation',
child: [ child: [
{ title: '系统配置', path: '/system/config' }, { title: '系统配置', path: '/system/config', permission: 'config:list' },
{ title: '用户管理', path: '/system/user' }, { title: '用户管理', path: '/system/user', permission: 'user:list' },
{ title: '角色管理', path: '/system/role' }, { title: '角色管理', path: '/system/role', permission: 'role:list' },
{ title: '博客文章', path: '/system/article' }, { title: '博客文章', path: '/system/article', permission: 'article:list' },
{ title: '分析统计', path: '/system/statistics' } { title: '分析统计', path: '/system/statistics', permission: 'article:list' }
] ]
},{ },{
name: 'api', name: 'api',
title: 'API数据', title: 'API数据',
icon: 'Histogram', icon: 'Histogram',
child: [ child: [
{ title: '一言', path: '/api/hitokoto' }, { title: '一言', path: '/api/hitokoto', permission: 'hitokoto:list' },
{ title: '照片墙', path: '/api/photoWall' }, { title: '照片墙', path: '/api/photoWall', permission: 'photoWall:list' },
{ title: '图片资源库', path: '/api/sourceImage' }, { title: '图片资源库', path: '/api/sourceImage', permission: 'sourceImage:list' },
{ title: '歌曲库', path: '/api/music' } { title: '歌曲库', path: '/api/music', permission: 'music:list' }
] ]
} }
] ]
export default menus

View File

@ -10,6 +10,7 @@ import 'element-plus/theme-chalk/el-message-box.css'
import 'element-plus/theme-chalk/el-image-viewer.css' import 'element-plus/theme-chalk/el-image-viewer.css'
import 'element-plus/theme-chalk/dark/css-vars.css' import 'element-plus/theme-chalk/dark/css-vars.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { permissionDirective } from '@/utils/permission'
// 全局路由导航前置守卫 // 全局路由导航前置守卫
router.beforeEach(function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { router.beforeEach(function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
@ -32,4 +33,5 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.use(router) app.use(router)
.use(store) .use(store)
.directive('loading', ElLoading.directive) .directive('loading', ElLoading.directive)
.directive('permission', permissionDirective)
.mount('#app') .mount('#app')

View File

@ -2,9 +2,15 @@ export interface SystemRoleModel {
_id: string | null _id: string | null
name: string | null // 角色名称 name: string | null // 角色名称
description: string | null // 描述 description: string | null // 描述
methods: string[] // 允许的请求类型(优先级3) permissions: string[] // 权限标识列表
includeUri: string[] // 包含的URI(优先级2)
excludeUri: string[] // 排除的URI(优先级1)
createdAt?: Date // 创建时间 createdAt?: Date // 创建时间
updatedAt?: Date // 更新时间 updatedAt?: Date // 更新时间
} }
export interface PermissionTreeNode {
code: string
name: string
description: string
leaf: boolean
children: PermissionTreeNode[]
}

View File

@ -8,7 +8,8 @@ class Store {
state: StateType = { state: StateType = {
loginInfo: { loginInfo: {
userInfo: null, userInfo: null,
token: localStorage.getItem('login_token') token: localStorage.getItem('login_token'),
permissions: []
}, },
tabs: [], tabs: [],
activeTab: null, activeTab: null,
@ -21,12 +22,15 @@ class Store {
* @param {Object} state * @param {Object} state
* @param {UserInfo} data * @param {UserInfo} data
*/ */
login(state: StateType, data: {token?: string, userInfo: UserInfo}): void { login(state: StateType, data: {token?: string, userInfo: UserInfo, permissions?: string[]}): void {
if (data.token) { if (data.token) {
localStorage.setItem('login_token', data.token) localStorage.setItem('login_token', data.token)
state.loginInfo.token = data.token state.loginInfo.token = data.token
} }
state.loginInfo.userInfo = data.userInfo state.loginInfo.userInfo = data.userInfo
if (data.permissions) {
state.loginInfo.permissions = data.permissions
}
}, },
/** /**
* *
@ -36,6 +40,7 @@ class Store {
localStorage.removeItem('login_token') localStorage.removeItem('login_token')
state.loginInfo.token = null state.loginInfo.token = null
state.loginInfo.userInfo = null state.loginInfo.userInfo = null
state.loginInfo.permissions = []
}, },
/** /**
* *

View File

@ -15,6 +15,7 @@ export interface StateType {
loginInfo: { // 登录信息 loginInfo: { // 登录信息
userInfo: null | UserInfo userInfo: null | UserInfo
token: null | string token: null | string
permissions: string[] // 用户权限列表
} }
tabs: TabItem[] // 导航页签 tabs: TabItem[] // 导航页签
activeTab: string | null // 当前激活的页签 activeTab: string | null // 当前激活的页签

View File

@ -43,11 +43,7 @@ http.interceptors.response.use(res => {
ElMessage.error('服务器内部错误') ElMessage.error('服务器内部错误')
} else if (err.response?.status >= 400) { } else if (err.response?.status >= 400) {
const message = err.response.data?.message const message = err.response.data?.message
if (typeof message === 'string') { message && ElMessage.error(message)
ElMessage.error(message)
} else if (Array.isArray(message)) {
ElMessage.error(message.join('<br/>'))
}
if (err.response.status === 403) { if (err.response.status === 403) {
router.push('/login') router.push('/login')
} }

37
src/utils/permission.ts Normal file
View File

@ -0,0 +1,37 @@
import type { Directive } from 'vue'
import store from '@/store'
/**
*
* 'user' 'user:list'
*/
export function hasPermission(permission: string): boolean {
const permissions: string[] = store.state.loginInfo.permissions
if (!permissions || !permissions.length) return false
// 直接匹配
if (permissions.includes(permission)) return true
// 上级权限匹配:如果权限是 'user:list',检查用户是否拥有 'user'
const colonIndex = permission.indexOf(':')
if (colonIndex !== -1) {
const parent = permission.substring(0, colonIndex)
if (permissions.includes(parent)) return true
}
return false
}
/**
* v-permission
* v-permission="'user:save'" v-permission="['user:save', 'user:delete']"
*
*/
export const permissionDirective: Directive<HTMLElement, string | string[]> = {
mounted(el, binding) {
const value = binding.value
if (!value) return
const permissions = Array.isArray(value) ? value : [value]
const hasPerm = permissions.some(p => hasPermission(p))
if (!hasPerm) {
el.parentNode?.removeChild(el)
}
}
}

View File

@ -81,10 +81,12 @@
import { ref, reactive, computed, nextTick } from 'vue' import { ref, reactive, computed, nextTick } from 'vue'
import { useStore } from 'vuex' import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import menus from '../config/menu' import allMenus from '../config/menu'
import type { MenuGroup } from '../config/menu'
import http from '@/utils/http' import http from '@/utils/http'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { VForm } from '@/types' import { VForm } from '@/types'
import { hasPermission } from '@/utils/permission'
const store = useStore() const store = useStore()
const router = useRouter() const router = useRouter()
@ -97,6 +99,16 @@ const openMenuNames = ref<string[]>([])
const mainViewReady = ref(false) const mainViewReady = ref(false)
const isDark = ref(false) const isDark = ref(false)
const menus = computed((): MenuGroup[] => {
return allMenus
.map(group => ({
...group,
child: group.child.filter(item => hasPermission(item.permission))
}))
.filter(group => group.child.length > 0)
})
function applyDarkMode(dark: boolean) { function applyDarkMode(dark: boolean) {
isDark.value = dark isDark.value = dark
document.documentElement.classList.toggle('dark', dark) document.documentElement.classList.toggle('dark', dark)
@ -170,7 +182,7 @@ if (!store.state.loginInfo.token) {
router.push('/login') router.push('/login')
} else { } else {
http.post<{token: string}, any>('/common/verifyToken', {token: store.state.loginInfo.token}).then(data => { http.post<{token: string}, any>('/common/verifyToken', {token: store.state.loginInfo.token}).then(data => {
store.commit('login', {userInfo: data.userInfo}) store.commit('login', {userInfo: data.userInfo, permissions: data.permissions})
mainViewReady.value = true mainViewReady.value = true
}).catch(() => { }).catch(() => {
router.push('/login') router.push('/login')

View File

@ -26,7 +26,19 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import menus from '../config/menu' import { computed } from 'vue'
import allMenus from '../config/menu'
import type { MenuGroup } from '../config/menu'
import { hasPermission } from '@/utils/permission'
const menus = computed((): MenuGroup[] => {
return allMenus
.map(group => ({
...group,
child: group.child.filter(item => hasPermission(item.permission))
}))
.filter(group => group.child.length > 0)
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.welcome-page { .welcome-page {

View File

@ -19,8 +19,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="btn-container"> <div class="btn-container">
<el-button type="primary" @click="addModal = true">添加</el-button> <el-button type="primary" @click="addModal = true" v-permission="'hitokoto:save'">添加</el-button>
<el-button type="danger" @click="deleteAll">删除</el-button> <el-button type="danger" @click="deleteAll" v-permission="'hitokoto:delete'">删除</el-button>
<div class="search-btn"> <div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button> <el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button> <el-button @click="reset" icon="RefreshLeft">重置</el-button>

View File

@ -23,8 +23,8 @@
</el-form> </el-form>
<div class="btn-container"> <div class="btn-container">
<el-button type="success" plain icon="VideoPlay" @click="playMusic">播放</el-button> <el-button type="success" plain icon="VideoPlay" @click="playMusic">播放</el-button>
<el-button type="primary" icon="Upload" @click="openUploadModal">上传音乐</el-button> <el-button type="primary" icon="Upload" @click="openUploadModal" v-permission="'music:save'">上传音乐</el-button>
<el-button type="warning" plain icon="Notebook" @click="libDrawerVisible = true">歌单管理</el-button> <el-button type="warning" plain icon="Notebook" @click="libDrawerVisible = true" v-permission="'music:save'">歌单管理</el-button>
<div class="search-btn"> <div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button> <el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button> <el-button @click="reset" icon="RefreshLeft">重置</el-button>
@ -72,9 +72,9 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="150" > <el-table-column label="操作" width="150" >
<template #default="scope"> <template #default="scope">
<el-button link icon="Document" @click="updateLyric(scope.row)" title="歌词"></el-button> <el-button link icon="Document" @click="updateLyric(scope.row)" title="歌词" v-permission="'music:save'"></el-button>
<el-button link icon="Download" @click="download(scope.row)" title="下载"></el-button> <el-button link icon="Download" @click="download(scope.row)" title="下载"></el-button>
<el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除"></el-button> <el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除" v-permission="'music:delete'"></el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@ -41,9 +41,9 @@
auto-upload auto-upload
:show-file-list="false" :show-file-list="false"
style="display: inline-block;margin-right: 10px;"> style="display: inline-block;margin-right: 10px;">
<el-button type="primary" icon="Upload" :loading="isUploading">上传图片</el-button> <el-button type="primary" icon="Upload" :loading="isUploading" v-permission="'photoWall:save'">上传图片</el-button>
</el-upload> </el-upload>
<el-button type="danger" @click="deleteAll" style="vertical-align: bottom">批量删除</el-button> <el-button type="danger" @click="deleteAll" style="vertical-align: bottom" v-permission="'photoWall:delete'">批量删除</el-button>
<div class="search-btn"> <div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button> <el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button> <el-button @click="reset" icon="RefreshLeft">重置</el-button>

View File

@ -16,9 +16,9 @@
auto-upload auto-upload
:show-file-list="false" :show-file-list="false"
style="display: inline-block;margin-right: 10px;"> style="display: inline-block;margin-right: 10px;">
<el-button type="primary" icon="Upload" :loading="isUploading">上传图片</el-button> <el-button type="primary" icon="Upload" :loading="isUploading" v-permission="'sourceImage:save'">上传图片</el-button>
</el-upload> </el-upload>
<el-button type="danger" @click="deleteAll" style="vertical-align: bottom">批量删除</el-button> <el-button type="danger" @click="deleteAll" style="vertical-align: bottom" v-permission="'sourceImage:delete'">批量删除</el-button>
</div> </div>
<el-table class="table-container" :data="sourceImageData" v-loading="loading" stripe @selection-change="dataSelect"> <el-table class="table-container" :data="sourceImageData" v-loading="loading" stripe @selection-change="dataSelect">
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
@ -41,7 +41,7 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" > <el-table-column label="操作" >
<template #default="scope"> <template #default="scope">
<el-button link icon="EditPen" type="primary" plain @click="modifyTags(scope.row)" title="修改标签"></el-button> <el-button link icon="EditPen" type="primary" plain @click="modifyTags(scope.row)" title="修改标签" v-permission="'sourceImage:save'"></el-button>
<el-button link icon="View" plain @click="preview(scope.row)" title="预览"></el-button> <el-button link icon="View" plain @click="preview(scope.row)" title="预览"></el-button>
</template> </template>
</el-table-column> </el-table-column>

View File

@ -30,8 +30,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="btn-container"> <div class="btn-container">
<el-button type="primary" @click="splitWord" style="vertical-align: bottom">分词处理</el-button> <el-button type="primary" @click="splitWord" style="vertical-align: bottom" v-permission="'article:edit'">分词处理</el-button>
<el-button @click="pullArticles" style="vertical-align: bottom">拉取文章</el-button> <el-button @click="pullArticles" style="vertical-align: bottom" v-permission="'article:edit'">拉取文章</el-button>
<el-upload <el-upload
:action="`${apiBase}/system/deployBlog`" :action="`${apiBase}/system/deployBlog`"
accept="application/zip" accept="application/zip"
@ -42,7 +42,8 @@
:on-error="uploadError" :on-error="uploadError"
auto-upload auto-upload
:show-file-list="false" :show-file-list="false"
style="display: inline-block;margin-left: 10px;"> style="display: inline-block;margin-left: 10px;"
v-permission="'system:deploy'">
<el-button type="primary" icon="Upload" :loading="isUploading">发布博客</el-button> <el-button type="primary" icon="Upload" :loading="isUploading">发布博客</el-button>
</el-upload> </el-upload>
<div class="search-btn"> <div class="search-btn">

View File

@ -6,7 +6,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="btn-container"> <div class="btn-container">
<el-button type="primary" @click="add">添加</el-button> <el-button type="primary" @click="add" v-permission="'config:save'">添加</el-button>
<div class="search-btn"> <div class="search-btn">
<el-button type="primary" @click="loadData" icon="Search">搜索</el-button> <el-button type="primary" @click="loadData" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button> <el-button @click="reset" icon="RefreshLeft">重置</el-button>
@ -37,8 +37,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" > <el-table-column label="操作" >
<template #default="scope"> <template #default="scope">
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button> <el-button link icon="Edit" @click="update(scope.row)" title="修改" v-permission="'config:save'"></el-button>
<el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除"></el-button> <el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除" v-permission="'config:delete'"></el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@ -6,7 +6,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="btn-container"> <div class="btn-container">
<el-button type="primary" @click="add">添加</el-button> <el-button type="primary" @click="add" v-permission="'role:save'">添加</el-button>
<div class="search-btn"> <div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button> <el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button> <el-button @click="reset" icon="RefreshLeft">重置</el-button>
@ -14,11 +14,7 @@
</div> </div>
<el-table class="table-container" :data="systemRoleData" v-loading="loading" stripe> <el-table class="table-container" :data="systemRoleData" v-loading="loading" stripe>
<el-table-column prop="name" label="角色名称" /> <el-table-column prop="name" label="角色名称" />
<el-table-column prop="methods" label="允许的请求类型" > <el-table-column prop="description" label="描述" />
<template #default="scope">
<el-tag type="info" v-for="method in scope.row.methods" :key="method">{{method}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" > <el-table-column prop="createdAt" label="创建时间" >
<template #default="scope"> <template #default="scope">
{{ datetimeFormat(scope.row.createdAt) }} {{ datetimeFormat(scope.row.createdAt) }}
@ -31,8 +27,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" > <el-table-column label="操作" >
<template #default="scope"> <template #default="scope">
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button> <el-button link icon="Edit" @click="update(scope.row)" title="修改" v-permission="'role:save'"></el-button>
<el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除"></el-button> <el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除" v-permission="'role:delete'"></el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -54,29 +50,17 @@
<el-form-item label="描述"> <el-form-item label="描述">
<el-input v-model="formData.description" /> <el-input v-model="formData.description" />
</el-form-item> </el-form-item>
<el-form-item label="允许的请求类型"> <el-form-item label="权限" prop="permissions">
<el-select v-model="formData.methods" multiple collapse-tags> <el-tree
<el-option value="GET">GET</el-option> ref="permTree"
<el-option value="POST">POST</el-option> :data="permissionTreeData"
<el-option value="PUT">PUT</el-option> :props="treeProps"
<el-option value="DELETE">DELETE</el-option> show-checkbox
</el-select> node-key="code"
</el-form-item> :default-checked-keys="formData.permissions"
<el-form-item label="允许的URI"> :check-strictly="true"
<el-input v-model="uri.include"> @check="handlePermCheckWithValidate"
<template #append> />
<el-button icon="Plus" @click="addUri('includeUri', uri.include)"></el-button>
</template>
</el-input>
<el-tag v-for="uri in formData.includeUri" :key="uri" closable @close="removeUri('includeUri', uri)">{{uri}}</el-tag>
</el-form-item>
<el-form-item label="禁止的URI">
<el-input v-model="uri.exclude">
<template #append>
<el-button icon="Plus" @click="addUri('excludeUri', uri.exclude)"></el-button>
</template>
</el-input>
<el-tag v-for="uri in formData.excludeUri" :key="uri" closable @close="removeUri('excludeUri', uri)">{{uri}}</el-tag>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -95,9 +79,9 @@ import { useStore } from 'vuex'
import { useBaseList } from '@/model/baselist' import { useBaseList } from '@/model/baselist'
import { Page } from '@/model/common.dto' import { Page } from '@/model/common.dto'
import http from '@/utils/http' import http from '@/utils/http'
import { SystemRoleModel } from '@/model/system/system-role' import { SystemRoleModel, PermissionTreeNode } from '@/model/system/system-role'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { VForm } from '@/types' import type { VForm } from '@/types'
const store = useStore() const store = useStore()
@ -115,26 +99,33 @@ const ruleValidate = {
name: [ name: [
{ required: true, message: '请输入角色名称', trigger: 'blur' } { required: true, message: '请输入角色名称', trigger: 'blur' }
], ],
permissions: [
{ validator: (rule: any, value: any, callback: any) => {
const permissions = getPermissionsToSave()
if (permissions.length === 0) {
callback(new Error('至少需要勾选一项权限'))
} else {
callback()
}
}, trigger: 'change' }
]
} }
const systemRoleData = ref<SystemRoleModel[]>([]) const systemRoleData = ref<SystemRoleModel[]>([])
const addModal = ref(false) const addModal = ref(false)
const modalTitle = ref<string | null>(null) const modalTitle = ref<string | null>(null)
const uri = reactive<{ const permissionTreeData = ref<PermissionTreeNode[]>([])
include: string | null, const treeProps = {
exclude: string | null children: 'children',
}>({ label: 'name'
include: null, }
exclude: null
})
const formData = reactive<SystemRoleModel>({ const formData = reactive<SystemRoleModel>({
_id: null, _id: null,
name: null, name: null,
description: null, description: null,
methods: [], permissions: []
includeUri: [],
excludeUri: []
}) })
const roleForm = ref<VForm>() const roleForm = ref<VForm>()
const permTree = ref<any>()
async function loadData() { async function loadData() {
loading.value = true loading.value = true
@ -146,49 +137,142 @@ async function loadData() {
setLoadData(loadData) setLoadData(loadData)
function add() { function add() {
uri.include = null
uri.exclude = null
Object.assign(formData, { Object.assign(formData, {
_id: null, _id: null,
name: null, name: null,
description: null, description: null,
methods: [], permissions: []
includeUri: [],
excludeUri: []
}) })
modalTitle.value = '新增角色' modalTitle.value = '新增角色'
addModal.value = true addModal.value = true
clearValidate() clearValidate()
nextTick(() => {
permTree.value?.setCheckedKeys([])
})
} }
function addUri(fieldName: 'includeUri' | 'excludeUri', uriValue: string | null) { /**
if(!uriValue) return * 处理权限树勾选
if(formData[fieldName].indexOf(uriValue) === -1) { * - 勾选父节点时自动勾选所有子节点
formData[fieldName].push(uriValue) * - 取消父节点时自动取消所有子节点
* - 所有子节点都被勾选时自动勾选父节点并取消子节点勾选只保留父code
*/
function handlePermCheck(nodeData: PermissionTreeNode, checkState: { checkedKeys: string[], halfCheckedKeys: string[] }) {
const tree = permTree.value
if (!tree) return
const node = tree.getNode(nodeData.code)
const isChecked = node.checked
if (!nodeData.leaf) {
//
const childCodes = nodeData.children.map((c: PermissionTreeNode) => c.code)
if (isChecked) {
//
childCodes.forEach((code: string) => tree.setChecked(code, true, false))
} else {
//
childCodes.forEach((code: string) => tree.setChecked(code, false, false))
}
} else {
//
const parentNode = findParentNode(permissionTreeData.value, nodeData.code)
if (parentNode) {
const allChildCodes = parentNode.children.map(c => c.code)
const allChecked = allChildCodes.every(code => tree.getNode(code)?.checked)
if (allChecked) {
//
tree.setChecked(parentNode.code, true, false)
} else {
//
tree.setChecked(parentNode.code, false, false)
}
}
} }
} }
function removeUri(fieldName: 'includeUri' | 'excludeUri', uriValue: string) { function handlePermCheckWithValidate(nodeData: PermissionTreeNode, checkState: { checkedKeys: string[], halfCheckedKeys: string[] }) {
let index = formData[fieldName].indexOf(uriValue) handlePermCheck(nodeData, checkState)
if(index !== -1) { roleForm.value?.validateField('permissions')
formData[fieldName].splice(index, 1)
} }
function findParentNode(nodes: PermissionTreeNode[], childCode: string): PermissionTreeNode | null {
for (const node of nodes) {
if (node.children.some(c => c.code === childCode)) {
return node
}
const found = findParentNode(node.children, childCode)
if (found) return found
}
return null
}
/**
* 从树中获取要保存的 permissions
* 如果父节点被勾选只传父节点code否则传各个被勾选的子节点code
*/
function getPermissionsToSave(): string[] {
const tree = permTree.value
if (!tree) return []
const result: string[] = []
for (const group of permissionTreeData.value) {
const parentNode = tree.getNode(group.code)
if (parentNode?.checked) {
result.push(group.code)
} else {
for (const child of group.children) {
const childNode = tree.getNode(child.code)
if (childNode?.checked) {
result.push(child.code)
}
}
}
}
return result
}
/**
* 编辑角色时根据已有 permissions 回显树的勾选状态
* 如果有父级code'user'需要勾选父节点及所有子节点
*/
function setTreeCheckedByPermissions(permissions: string[]) {
nextTick(() => {
const tree = permTree.value
if (!tree) return
tree.setCheckedKeys([])
for (const perm of permissions) {
//
const parentGroup = permissionTreeData.value.find(g => g.code === perm)
if (parentGroup) {
//
tree.setChecked(perm, true, false)
parentGroup.children.forEach(c => tree.setChecked(c.code, true, false))
} else {
tree.setChecked(perm, true, false)
}
}
//
for (const group of permissionTreeData.value) {
if (!permissions.includes(group.code)) {
const allChildChecked = group.children.length > 0 && group.children.every(c => tree.getNode(c.code)?.checked)
if (allChildChecked) {
tree.setChecked(group.code, true, false)
}
}
}
})
} }
function update(row: SystemRoleModel) { function update(row: SystemRoleModel) {
uri.include = null
uri.exclude = null
Object.assign(formData, { Object.assign(formData, {
_id: row._id, _id: row._id,
name: row.name, name: row.name,
description: row.description, description: row.description,
methods: row.methods, permissions: [...row.permissions]
includeUri: row.includeUri,
excludeUri: row.excludeUri
}) })
modalTitle.value = '修改角色' modalTitle.value = '修改角色'
addModal.value = true addModal.value = true
clearValidate() clearValidate()
setTreeCheckedByPermissions(row.permissions)
} }
function remove(row: SystemRoleModel) { function remove(row: SystemRoleModel) {
@ -203,7 +287,8 @@ async function save() {
roleForm.value?.validate(async (valid: boolean) => { roleForm.value?.validate(async (valid: boolean) => {
if (!valid) return if (!valid) return
modalLoading.value = true modalLoading.value = true
await http.post<SystemRoleModel, any>('/system/role/save', formData) const saveData = { ...formData, permissions: getPermissionsToSave() }
await http.post<SystemRoleModel, any>('/system/role/save', saveData)
modalLoading.value = false modalLoading.value = false
addModal.value = false addModal.value = false
ElMessage.success("保存成功") ElMessage.success("保存成功")
@ -218,4 +303,7 @@ function clearValidate() {
} }
loadData() loadData()
http.get<never, any>('/system/role/permissionTree').then(data => {
permissionTreeData.value = data
})
</script> </script>

View File

@ -6,7 +6,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="btn-container"> <div class="btn-container">
<el-button type="primary" @click="add">添加</el-button> <el-button type="primary" @click="add" v-permission="'user:save'">添加</el-button>
<div class="search-btn"> <div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button> <el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button> <el-button @click="reset" icon="RefreshLeft">重置</el-button>
@ -27,8 +27,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" > <el-table-column label="操作" >
<template #default="scope"> <template #default="scope">
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button> <el-button link icon="Edit" @click="update(scope.row)" title="修改" v-permission="'user:save'"></el-button>
<el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除"></el-button> <el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除" v-permission="'user:delete'"></el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>