权限系统重构:
- 角色模型改为 permissions 字段,移除 methods/includeUri/excludeUri - 角色管理页面支持权限树选择,保存时只传最上级权限 - /common/verifyToken 返回 permissions,存入 vuex - 新增 v-permission 指令,所有操作按钮和菜单均按权限控制显示 - 菜单按 list 权限过滤 - 各业务页面按钮加权限指令 - 角色管理列表只显示名称/描述/时间,不显示权限列,权限必选
This commit is contained in:
+14
-2
@@ -81,10 +81,12 @@
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
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 { ElMessage } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
@@ -97,6 +99,16 @@ const openMenuNames = ref<string[]>([])
|
||||
const mainViewReady = 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) {
|
||||
isDark.value = dark
|
||||
document.documentElement.classList.toggle('dark', dark)
|
||||
@@ -170,7 +182,7 @@ if (!store.state.loginInfo.token) {
|
||||
router.push('/login')
|
||||
} else {
|
||||
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
|
||||
}).catch(() => {
|
||||
router.push('/login')
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="btn-container">
|
||||
<el-button type="primary" @click="addModal = true">添加</el-button>
|
||||
<el-button type="danger" @click="deleteAll">删除</el-button>
|
||||
<el-button type="primary" @click="addModal = true" v-permission="'hitokoto:save'">添加</el-button>
|
||||
<el-button type="danger" @click="deleteAll" v-permission="'hitokoto:delete'">删除</el-button>
|
||||
<div class="search-btn">
|
||||
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
|
||||
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</el-form>
|
||||
<div class="btn-container">
|
||||
<el-button type="success" plain icon="VideoPlay" @click="playMusic">播放</el-button>
|
||||
<el-button type="primary" icon="Upload" @click="openUploadModal">上传音乐</el-button>
|
||||
<el-button type="warning" plain icon="Notebook" @click="libDrawerVisible = true">歌单管理</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" v-permission="'music:save'">歌单管理</el-button>
|
||||
<div class="search-btn">
|
||||
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
|
||||
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
|
||||
@@ -72,9 +72,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" >
|
||||
<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 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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
auto-upload
|
||||
:show-file-list="false"
|
||||
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-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">
|
||||
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
|
||||
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
auto-upload
|
||||
:show-file-list="false"
|
||||
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-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>
|
||||
<el-table class="table-container" :data="sourceImageData" v-loading="loading" stripe @selection-change="dataSelect">
|
||||
<el-table-column type="selection" width="55" />
|
||||
@@ -41,7 +41,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" >
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="btn-container">
|
||||
<el-button type="primary" @click="splitWord" style="vertical-align: bottom">分词处理</el-button>
|
||||
<el-button @click="pullArticles" 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" v-permission="'article:edit'">拉取文章</el-button>
|
||||
<el-upload
|
||||
:action="`${apiBase}/system/deployBlog`"
|
||||
accept="application/zip"
|
||||
@@ -42,7 +42,8 @@
|
||||
:on-error="uploadError"
|
||||
auto-upload
|
||||
: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-upload>
|
||||
<div class="search-btn">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<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">
|
||||
<el-button type="primary" @click="loadData" icon="Search">搜索</el-button>
|
||||
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
|
||||
@@ -37,8 +37,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" >
|
||||
<template #default="scope">
|
||||
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button>
|
||||
<el-button link type="danger" icon="Delete" @click="remove(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="删除" v-permission="'config:delete'"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
+150
-62
@@ -6,7 +6,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<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">
|
||||
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
|
||||
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
|
||||
@@ -14,11 +14,7 @@
|
||||
</div>
|
||||
<el-table class="table-container" :data="systemRoleData" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="角色名称" />
|
||||
<el-table-column prop="methods" 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="description" label="描述" />
|
||||
<el-table-column prop="createdAt" label="创建时间" >
|
||||
<template #default="scope">
|
||||
{{ datetimeFormat(scope.row.createdAt) }}
|
||||
@@ -31,8 +27,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" >
|
||||
<template #default="scope">
|
||||
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button>
|
||||
<el-button link type="danger" icon="Delete" @click="remove(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="删除" v-permission="'role:delete'"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -54,29 +50,17 @@
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="formData.description" />
|
||||
</el-form-item>
|
||||
<el-form-item label="允许的请求类型">
|
||||
<el-select v-model="formData.methods" multiple collapse-tags>
|
||||
<el-option value="GET">GET</el-option>
|
||||
<el-option value="POST">POST</el-option>
|
||||
<el-option value="PUT">PUT</el-option>
|
||||
<el-option value="DELETE">DELETE</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="允许的URI">
|
||||
<el-input v-model="uri.include">
|
||||
<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 label="权限" prop="permissions">
|
||||
<el-tree
|
||||
ref="permTree"
|
||||
:data="permissionTreeData"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
node-key="code"
|
||||
:default-checked-keys="formData.permissions"
|
||||
:check-strictly="true"
|
||||
@check="handlePermCheckWithValidate"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -95,9 +79,9 @@ import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
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 { VForm } from '@/types'
|
||||
import type { VForm } from '@/types'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
@@ -115,26 +99,33 @@ const ruleValidate = {
|
||||
name: [
|
||||
{ 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 addModal = ref(false)
|
||||
const modalTitle = ref<string | null>(null)
|
||||
const uri = reactive<{
|
||||
include: string | null,
|
||||
exclude: string | null
|
||||
}>({
|
||||
include: null,
|
||||
exclude: null
|
||||
})
|
||||
const permissionTreeData = ref<PermissionTreeNode[]>([])
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
}
|
||||
const formData = reactive<SystemRoleModel>({
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
includeUri: [],
|
||||
excludeUri: []
|
||||
permissions: []
|
||||
})
|
||||
const roleForm = ref<VForm>()
|
||||
const permTree = ref<any>()
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
@@ -146,49 +137,142 @@ async function loadData() {
|
||||
setLoadData(loadData)
|
||||
|
||||
function add() {
|
||||
uri.include = null
|
||||
uri.exclude = null
|
||||
Object.assign(formData, {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
includeUri: [],
|
||||
excludeUri: []
|
||||
permissions: []
|
||||
})
|
||||
modalTitle.value = '新增角色'
|
||||
addModal.value = true
|
||||
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) {
|
||||
let index = formData[fieldName].indexOf(uriValue)
|
||||
if(index !== -1) {
|
||||
formData[fieldName].splice(index, 1)
|
||||
function handlePermCheckWithValidate(nodeData: PermissionTreeNode, checkState: { checkedKeys: string[], halfCheckedKeys: string[] }) {
|
||||
handlePermCheck(nodeData, checkState)
|
||||
roleForm.value?.validateField('permissions')
|
||||
}
|
||||
|
||||
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) {
|
||||
uri.include = null
|
||||
uri.exclude = null
|
||||
Object.assign(formData, {
|
||||
_id: row._id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
methods: row.methods,
|
||||
includeUri: row.includeUri,
|
||||
excludeUri: row.excludeUri
|
||||
permissions: [...row.permissions]
|
||||
})
|
||||
modalTitle.value = '修改角色'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
setTreeCheckedByPermissions(row.permissions)
|
||||
}
|
||||
|
||||
function remove(row: SystemRoleModel) {
|
||||
@@ -203,7 +287,8 @@ async function save() {
|
||||
roleForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
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
|
||||
addModal.value = false
|
||||
ElMessage.success("保存成功")
|
||||
@@ -218,4 +303,7 @@ function clearValidate() {
|
||||
}
|
||||
|
||||
loadData()
|
||||
http.get<never, any>('/system/role/permissionTree').then(data => {
|
||||
permissionTreeData.value = data
|
||||
})
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<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">
|
||||
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
|
||||
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
|
||||
@@ -27,8 +27,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" >
|
||||
<template #default="scope">
|
||||
<el-button link icon="Edit" @click="update(scope.row)" title="修改"></el-button>
|
||||
<el-button link type="danger" icon="Delete" @click="remove(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="删除" v-permission="'user:delete'"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
Reference in New Issue
Block a user