权限系统重构:

- 角色模型改为 permissions 字段,移除 methods/includeUri/excludeUri
- 角色管理页面支持权限树选择,保存时只传最上级权限
- /common/verifyToken 返回 permissions,存入 vuex
- 新增 v-permission 指令,所有操作按钮和菜单均按权限控制显示
- 菜单按 list 权限过滤
- 各业务页面按钮加权限指令
- 角色管理列表只显示名称/描述/时间,不显示权限列,权限必选
This commit is contained in:
2026-04-02 20:38:14 +08:00
parent 86c7d75963
commit a4f7db415e
16 changed files with 268 additions and 105 deletions
+14 -2
View File
@@ -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')
+2 -2
View File
@@ -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>
+4 -4
View File
@@ -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>
+2 -2
View File
@@ -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>
+3 -3
View File
@@ -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>
+4 -3
View File
@@ -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">
+3 -3
View File
@@ -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
View File
@@ -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>
+3 -3
View File
@@ -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>