Compare commits
2 Commits
86c7d75963
...
ca4c17042d
| Author | SHA1 | Date | |
|---|---|---|---|
| ca4c17042d | |||
| a4f7db415e |
@ -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
|
||||||
@ -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')
|
||||||
|
|||||||
@ -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[]
|
||||||
|
}
|
||||||
@ -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 = []
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 添加导航页签
|
* 添加导航页签
|
||||||
|
|||||||
@ -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 // 当前激活的页签
|
||||||
|
|||||||
@ -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
37
src/utils/permission.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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')
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user