feat: 新增角色管理(权限树配置/增删改查)并实现基于角色的菜单按钮权限裁剪
parent
0e4a38fd6f
commit
18d4bf4067
|
|
@ -61,3 +61,21 @@ export function flattenButtons(tree) {
|
||||||
walk(tree)
|
walk(tree)
|
||||||
return codes
|
return codes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 依据角色勾选的 menuIds 集合裁剪菜单树:保留命中节点及其祖先节点(用于渲染路径),
|
||||||
|
// 同时按 buttonCodeSet 过滤每个节点的按钮列表,实现"页面权限+功能权限"双重收敛。
|
||||||
|
export function pruneMenuTree(tree, menuIdSet, buttonCodeSet) {
|
||||||
|
function walk(nodes) {
|
||||||
|
const result = []
|
||||||
|
for (const n of nodes) {
|
||||||
|
const children = n.children ? walk(n.children) : []
|
||||||
|
const selfMatched = menuIdSet.has(n.id)
|
||||||
|
if (!selfMatched && children.length === 0) continue
|
||||||
|
const buttons = (n.buttons || []).filter((b) => buttonCodeSet.has(b.code))
|
||||||
|
result.push({ ...n, children, buttons })
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return walk(tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { users } from '../data/users.js'
|
import { users } from '../data/users.js'
|
||||||
import { menuTree, flattenButtons } from '../data/menu.js'
|
import { menuTree, flattenButtons, pruneMenuTree } from '../data/menu.js'
|
||||||
|
import { roles } from '../data/roles.js'
|
||||||
import {
|
import {
|
||||||
accessTokens,
|
accessTokens,
|
||||||
refreshTokens,
|
refreshTokens,
|
||||||
|
|
@ -17,6 +18,16 @@ function findUser(username) {
|
||||||
return users.find((u) => u.username === username)
|
return users.find((u) => u.username === username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 依据用户所有已启用角色的 menuIds/buttonCodes 求并集,裁剪出该用户实际可见的菜单权限树
|
||||||
|
function resolveUserMenu(user) {
|
||||||
|
const userRoles = roles.filter((r) => user.roleIds.includes(r.id) && r.status === 'enabled')
|
||||||
|
const menuIdSet = new Set(userRoles.flatMap((r) => r.menuIds))
|
||||||
|
const buttonCodeSet = new Set(userRoles.flatMap((r) => r.buttonCodes))
|
||||||
|
const prunedTree = pruneMenuTree(menuTree, menuIdSet, buttonCodeSet)
|
||||||
|
return { menuTree: prunedTree, buttonCodes: flattenButtons(prunedTree) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
router.post('/sms/send', (req, res) => {
|
router.post('/sms/send', (req, res) => {
|
||||||
const { username, password } = req.body || {}
|
const { username, password } = req.body || {}
|
||||||
const user = findUser(username)
|
const user = findUser(username)
|
||||||
|
|
@ -54,14 +65,15 @@ router.post('/login', (req, res) => {
|
||||||
maxAge: REFRESH_TOKEN_TTL_MS,
|
maxAge: REFRESH_TOKEN_TTL_MS,
|
||||||
sameSite: 'lax'
|
sameSite: 'lax'
|
||||||
})
|
})
|
||||||
|
const { menuTree: userMenuTree, buttonCodes: userButtonCodes } = resolveUserMenu(user)
|
||||||
res.json({
|
res.json({
|
||||||
code: 200,
|
code: 200,
|
||||||
msg: 'ok',
|
msg: 'ok',
|
||||||
data: {
|
data: {
|
||||||
accessToken,
|
accessToken,
|
||||||
userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds },
|
userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds },
|
||||||
menuTree,
|
menuTree: userMenuTree,
|
||||||
buttonCodes: flattenButtons(menuTree),
|
buttonCodes: userButtonCodes,
|
||||||
mustChangePassword: !!user.mustChangePassword
|
mustChangePassword: !!user.mustChangePassword
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -91,7 +103,9 @@ router.get('/userinfo', requireAuth, (req, res) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/menu', requireAuth, (req, res) => {
|
router.get('/menu', requireAuth, (req, res) => {
|
||||||
res.json({ code: 200, msg: 'ok', data: { menuTree, buttonCodes: flattenButtons(menuTree) } })
|
const user = users.find((u) => u.id === req.userId)
|
||||||
|
const { menuTree: userMenuTree, buttonCodes: userButtonCodes } = resolveUserMenu(user)
|
||||||
|
res.json({ code: 200, msg: 'ok', data: { menuTree: userMenuTree, buttonCodes: userButtonCodes } })
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import crypto from 'crypto'
|
|
||||||
import { orgs, nextOrgId, buildOrgTree, collectDescendantIds } from '../data/orgs.js'
|
import { orgs, nextOrgId, buildOrgTree, collectDescendantIds } from '../data/orgs.js'
|
||||||
import { users, nextUserId } from '../data/users.js'
|
import { users, nextUserId } from '../data/users.js'
|
||||||
import { roles } from '../data/roles.js'
|
import { roles, nextRoleId } from '../data/roles.js'
|
||||||
|
import { menuTree } from '../data/menu.js'
|
||||||
import { requireAuth } from '../middleware/requireAuth.js'
|
import { requireAuth } from '../middleware/requireAuth.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
router.use(requireAuth)
|
router.use(requireAuth)
|
||||||
|
|
||||||
|
const DATA_SCOPES = ['own', 'ownAndSub', 'all']
|
||||||
|
|
||||||
function orgName(orgId) {
|
function orgName(orgId) {
|
||||||
return orgs.find((o) => o.id === orgId)?.name || ''
|
return orgs.find((o) => o.id === orgId)?.name || ''
|
||||||
}
|
}
|
||||||
|
|
@ -238,17 +240,98 @@ router.put('/user/:id/password/reset', (req, res) => {
|
||||||
res.json({ code: 200, msg: 'ok', data: null })
|
res.json({ code: 200, msg: 'ok', data: null })
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------- 角色管理(简版,供用户管理下拉使用;完整 CRUD 见后续路由) ----------------
|
// ---------------- 角色管理 ----------------
|
||||||
|
|
||||||
router.get('/role/list', (req, res) => {
|
router.get('/role/list', (req, res) => {
|
||||||
const { status } = req.query || {}
|
const { name, orgId, status, page = 1, pageSize = 1000 } = req.query || {}
|
||||||
let list = roles
|
let list = roles
|
||||||
|
if (name) list = list.filter((r) => r.name.includes(name))
|
||||||
|
if (orgId) list = list.filter((r) => r.orgId === orgId)
|
||||||
if (status) list = list.filter((r) => r.status === status)
|
if (status) list = list.filter((r) => r.status === status)
|
||||||
res.json({
|
|
||||||
code: 200,
|
const total = list.length
|
||||||
msg: 'ok',
|
const start = (Number(page) - 1) * Number(pageSize)
|
||||||
data: list.map((r) => ({ id: r.id, name: r.name, orgId: r.orgId, status: r.status, builtin: r.builtin }))
|
const pageList = list.slice(start, start + Number(pageSize)).map((r) => ({
|
||||||
})
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
orgId: r.orgId,
|
||||||
|
orgName: orgName(r.orgId),
|
||||||
|
status: r.status,
|
||||||
|
builtin: r.builtin,
|
||||||
|
createdAt: r.createdAt
|
||||||
|
}))
|
||||||
|
res.json({ code: 200, msg: 'ok', data: { list: pageList, total } })
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/role/:id', (req, res) => {
|
||||||
|
const role = roles.find((r) => r.id === req.params.id)
|
||||||
|
if (!role) return res.json({ code: 40404, msg: '角色不存在', data: null })
|
||||||
|
res.json({ code: 200, msg: 'ok', data: role })
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/role', (req, res) => {
|
||||||
|
const { name, orgId, menuIds, buttonCodes, dataScope } = req.body || {}
|
||||||
|
if (!name) return res.json({ code: 40016, msg: '请输入角色名称', data: null })
|
||||||
|
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
||||||
|
if (!menuIds?.length) return res.json({ code: 40017, msg: '请至少选择一个页面权限', data: null })
|
||||||
|
if (!DATA_SCOPES.includes(dataScope)) return res.json({ code: 40018, msg: '请选择数据权限范围', data: null })
|
||||||
|
const duplicate = roles.some((r) => r.orgId === orgId && r.name === name)
|
||||||
|
if (duplicate) return res.json({ code: 40019, msg: '同一所属机构下角色名称不可重复', data: null })
|
||||||
|
|
||||||
|
const role = {
|
||||||
|
id: nextRoleId(),
|
||||||
|
name,
|
||||||
|
orgId,
|
||||||
|
builtin: false,
|
||||||
|
status: 'enabled',
|
||||||
|
menuIds,
|
||||||
|
buttonCodes: buttonCodes || [],
|
||||||
|
dataScope,
|
||||||
|
createdAt: nowStr()
|
||||||
|
}
|
||||||
|
roles.push(role)
|
||||||
|
res.json({ code: 200, msg: 'ok', data: role })
|
||||||
|
})
|
||||||
|
|
||||||
|
router.put('/role/:id', (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { name, orgId, menuIds, buttonCodes, dataScope } = req.body || {}
|
||||||
|
const role = roles.find((r) => r.id === id)
|
||||||
|
if (!role) return res.json({ code: 40404, msg: '角色不存在', data: null })
|
||||||
|
if (!name) return res.json({ code: 40016, msg: '请输入角色名称', data: null })
|
||||||
|
if (role.builtin && name !== role.name) return res.json({ code: 40020, msg: '内置角色不可改名', data: null })
|
||||||
|
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
||||||
|
if (!menuIds?.length) return res.json({ code: 40017, msg: '请至少选择一个页面权限', data: null })
|
||||||
|
if (!DATA_SCOPES.includes(dataScope)) return res.json({ code: 40018, msg: '请选择数据权限范围', data: null })
|
||||||
|
const duplicate = roles.some((r) => r.id !== id && r.orgId === orgId && r.name === name)
|
||||||
|
if (duplicate) return res.json({ code: 40019, msg: '同一所属机构下角色名称不可重复', data: null })
|
||||||
|
|
||||||
|
role.name = name
|
||||||
|
role.orgId = orgId
|
||||||
|
role.menuIds = menuIds
|
||||||
|
role.buttonCodes = buttonCodes || []
|
||||||
|
role.dataScope = dataScope
|
||||||
|
res.json({ code: 200, msg: 'ok', data: role })
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete('/role/:id', (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const index = roles.findIndex((r) => r.id === id)
|
||||||
|
if (index === -1) return res.json({ code: 40404, msg: '角色不存在', data: null })
|
||||||
|
const role = roles[index]
|
||||||
|
if (role.builtin) return res.json({ code: 40021, msg: '内置角色不可删除', data: null })
|
||||||
|
const relatedUsers = users.filter((u) => u.roleIds.includes(id))
|
||||||
|
if (relatedUsers.length) {
|
||||||
|
return res.json({ code: 40022, msg: `该角色已关联${relatedUsers.length}个用户,请先解除关联后再删除`, data: null })
|
||||||
|
}
|
||||||
|
roles.splice(index, 1)
|
||||||
|
res.json({ code: 200, msg: 'ok', data: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------- 权限字典树 ----------------
|
||||||
|
|
||||||
|
router.get('/permission/tree', (req, res) => {
|
||||||
|
res.json({ code: 200, msg: 'ok', data: menuTree })
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,8 @@ export const resetUserPasswordApi = (id) => request.put(`/system/user/${id}/pass
|
||||||
|
|
||||||
// ---------------- 角色管理 ----------------
|
// ---------------- 角色管理 ----------------
|
||||||
export const fetchRoleListApi = (params) => request.get('/system/role/list', { params })
|
export const fetchRoleListApi = (params) => request.get('/system/role/list', { params })
|
||||||
|
export const fetchRoleDetailApi = (id) => request.get(`/system/role/${id}`)
|
||||||
|
export const createRoleApi = (data) => request.post('/system/role', data)
|
||||||
|
export const updateRoleApi = (id, data) => request.put(`/system/role/${id}`, data)
|
||||||
|
export const deleteRoleApi = (id) => request.delete(`/system/role/${id}`)
|
||||||
|
export const fetchPermissionTreeApi = () => request.get('/system/permission/tree')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<div class="permission-tree">
|
||||||
|
<div class="permission-columns">
|
||||||
|
<div class="menu-column">
|
||||||
|
<div class="column-title">页面权限</div>
|
||||||
|
<a-tree
|
||||||
|
v-if="treeData.length"
|
||||||
|
checkable
|
||||||
|
:disabled="readonly"
|
||||||
|
:tree-data="treeData"
|
||||||
|
:checked-keys="checkedKeys"
|
||||||
|
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||||
|
default-expand-all
|
||||||
|
@check="handleCheck"
|
||||||
|
/>
|
||||||
|
<a-empty v-else description="加载中" />
|
||||||
|
</div>
|
||||||
|
<div class="button-column">
|
||||||
|
<div class="column-title">功能权限</div>
|
||||||
|
<div v-if="buttonGroups.length === 0" class="empty-hint">请先在左侧勾选页面权限</div>
|
||||||
|
<div v-for="group in buttonGroups" :key="group.id" class="button-group">
|
||||||
|
<div class="button-group-title">{{ group.name }}</div>
|
||||||
|
<a-checkbox-group v-model:value="buttonCodesModel" :disabled="readonly" :options="group.options" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-scope-row">
|
||||||
|
<div class="column-title">数据权限范围</div>
|
||||||
|
<a-radio-group v-model:value="dataScopeModel" :disabled="readonly">
|
||||||
|
<a-radio value="own">本机构</a-radio>
|
||||||
|
<a-radio value="ownAndSub">本机构及下级机构</a-radio>
|
||||||
|
<a-radio value="all">全部机构</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { fetchPermissionTreeApi } from '@/api/system'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ menuIds: [], buttonCodes: [], dataScope: undefined })
|
||||||
|
},
|
||||||
|
readonly: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const rawTree = ref([])
|
||||||
|
const flatNodeMap = ref(new Map())
|
||||||
|
|
||||||
|
function toTreeData(nodes) {
|
||||||
|
return nodes.map((node) => {
|
||||||
|
flatNodeMap.value.set(node.id, node)
|
||||||
|
return {
|
||||||
|
key: node.id,
|
||||||
|
title: node.name,
|
||||||
|
children: node.children?.length ? toTreeData(node.children) : []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = computed(() => (rawTree.value.length ? toTreeData(rawTree.value) : []))
|
||||||
|
|
||||||
|
async function loadTree() {
|
||||||
|
const res = await fetchPermissionTreeApi()
|
||||||
|
if (res.data.code === 200) rawTree.value = res.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadTree)
|
||||||
|
|
||||||
|
const checkedKeys = computed(() => props.modelValue.menuIds || [])
|
||||||
|
const buttonCodesModel = computed({
|
||||||
|
get: () => props.modelValue.buttonCodes || [],
|
||||||
|
set: (val) => emitChange({ buttonCodes: val })
|
||||||
|
})
|
||||||
|
const dataScopeModel = computed({
|
||||||
|
get: () => props.modelValue.dataScope,
|
||||||
|
set: (val) => emitChange({ dataScope: val })
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonGroups = computed(() => {
|
||||||
|
const groups = []
|
||||||
|
checkedKeys.value.forEach((id) => {
|
||||||
|
const node = flatNodeMap.value.get(id)
|
||||||
|
if (node?.buttons?.length) {
|
||||||
|
groups.push({
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
options: node.buttons.map((b) => ({ label: b.name, value: b.code }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
function emitChange(patch) {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
menuIds: props.modelValue.menuIds || [],
|
||||||
|
buttonCodes: props.modelValue.buttonCodes || [],
|
||||||
|
dataScope: props.modelValue.dataScope,
|
||||||
|
...patch
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheck(keys) {
|
||||||
|
// 移除已不再勾选菜单对应的按钮权限,避免遗留孤立的功能权限码
|
||||||
|
const validCodes = new Set()
|
||||||
|
keys.forEach((id) => {
|
||||||
|
const node = flatNodeMap.value.get(id)
|
||||||
|
node?.buttons?.forEach((b) => validCodes.add(b.code))
|
||||||
|
})
|
||||||
|
const nextButtonCodes = (props.modelValue.buttonCodes || []).filter((code) => validCodes.has(code))
|
||||||
|
emit('update:modelValue', {
|
||||||
|
menuIds: keys,
|
||||||
|
buttonCodes: nextButtonCodes,
|
||||||
|
dataScope: props.modelValue.dataScope
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.permission-columns {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.menu-column,
|
||||||
|
.button-column {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.column-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.button-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.button-group-title {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.empty-hint {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.data-scope-row {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
<template>
|
||||||
|
<div class="role-list-page">
|
||||||
|
<ProTable
|
||||||
|
ref="proTableRef"
|
||||||
|
:columns="columns"
|
||||||
|
:fetch-data="loadRoleList"
|
||||||
|
:initial-search="{ name: '', orgId: undefined }"
|
||||||
|
>
|
||||||
|
<template #search="{ form }">
|
||||||
|
<a-form-item label="角色名称">
|
||||||
|
<a-input v-model:value="form.name" placeholder="请输入角色名称" allow-clear style="width: 180px" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="所属机构">
|
||||||
|
<OrgTreeSelect v-model="form.orgId" style="width: 200px" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<a-button v-permission="'role:add'" type="primary" @click="openModal('create')">
|
||||||
|
<PlusOutlined /> 新增角色
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.dataIndex === 'index'">{{ index + 1 }}</template>
|
||||||
|
<template v-else-if="column.dataIndex === 'name'">
|
||||||
|
{{ record.name }}
|
||||||
|
<a-tag v-if="record.builtin" color="blue" style="margin-left: 4px">内置</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.dataIndex === 'action'">
|
||||||
|
<a-space>
|
||||||
|
<a @click="openModal('view', record)">查看</a>
|
||||||
|
<a v-permission="'role:edit'" @click="openModal('edit', record)">修改</a>
|
||||||
|
<a-popconfirm title="确定删除该角色吗?" @confirm="handleDelete(record)">
|
||||||
|
<a v-if="!record.builtin" v-permission="'role:delete'" class="danger-link">删除</a>
|
||||||
|
</a-popconfirm>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:open="modalOpen"
|
||||||
|
:title="modalTitleMap[modalMode]"
|
||||||
|
:width="760"
|
||||||
|
:footer="modalMode === 'view' ? null : undefined"
|
||||||
|
:confirm-loading="submitting"
|
||||||
|
@ok="handleSubmit"
|
||||||
|
>
|
||||||
|
<a-form ref="formRef" layout="vertical" :model="form" :rules="rules">
|
||||||
|
<a-form-item label="角色名称" name="name">
|
||||||
|
<a-input
|
||||||
|
v-model:value="form.name"
|
||||||
|
:disabled="modalMode === 'view' || (modalMode === 'edit' && form.builtin)"
|
||||||
|
placeholder="请输入角色名称"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="所属机构" name="orgId">
|
||||||
|
<OrgTreeSelect v-model="form.orgId" :disabled="modalMode === 'view'" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="权限配置" name="menuIds">
|
||||||
|
<PermissionTree v-model="permissionValue" :readonly="modalMode === 'view'" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||||
|
import ProTable from '@/components/ProTable.vue'
|
||||||
|
import OrgTreeSelect from '@/components/OrgTreeSelect.vue'
|
||||||
|
import PermissionTree from '@/components/PermissionTree.vue'
|
||||||
|
import { fetchRoleListApi, fetchRoleDetailApi, createRoleApi, updateRoleApi, deleteRoleApi } from '@/api/system'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '序号', dataIndex: 'index', width: 60 },
|
||||||
|
{ title: '角色名称', dataIndex: 'name' },
|
||||||
|
{ title: '所属机构', dataIndex: 'orgName' },
|
||||||
|
{ title: '创建时间', dataIndex: 'createdAt' },
|
||||||
|
{ title: '操作', dataIndex: 'action', width: 180 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const proTableRef = ref(null)
|
||||||
|
|
||||||
|
async function loadRoleList(params) {
|
||||||
|
const res = await fetchRoleListApi(params)
|
||||||
|
if (res.data.code === 200) return res.data.data
|
||||||
|
return { list: [], total: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalOpen = ref(false)
|
||||||
|
const modalMode = ref('create') // create | edit | view
|
||||||
|
const submitting = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const modalTitleMap = { create: '新增角色', edit: '修改角色', view: '查看角色' }
|
||||||
|
|
||||||
|
const form = reactive({ id: '', name: '', orgId: undefined, builtin: false })
|
||||||
|
const permissionValue = ref({ menuIds: [], buttonCodes: [], dataScope: undefined })
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入角色名称' }],
|
||||||
|
orgId: [{ required: true, message: '请选择所属机构' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openModal(mode, record) {
|
||||||
|
modalMode.value = mode
|
||||||
|
if (mode === 'create') {
|
||||||
|
form.id = ''
|
||||||
|
form.name = ''
|
||||||
|
form.orgId = undefined
|
||||||
|
form.builtin = false
|
||||||
|
permissionValue.value = { menuIds: [], buttonCodes: [], dataScope: undefined }
|
||||||
|
modalOpen.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await fetchRoleDetailApi(record.id)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const detail = res.data.data
|
||||||
|
form.id = detail.id
|
||||||
|
form.name = detail.name
|
||||||
|
form.orgId = detail.orgId
|
||||||
|
form.builtin = detail.builtin
|
||||||
|
permissionValue.value = {
|
||||||
|
menuIds: detail.menuIds || [],
|
||||||
|
buttonCodes: detail.buttonCodes || [],
|
||||||
|
dataScope: detail.dataScope
|
||||||
|
}
|
||||||
|
modalOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (modalMode.value === 'view') {
|
||||||
|
modalOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!permissionValue.value.menuIds.length) {
|
||||||
|
message.error('请至少选择一个页面权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!permissionValue.value.dataScope) {
|
||||||
|
message.error('请选择数据权限范围')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name: form.name,
|
||||||
|
orgId: form.orgId,
|
||||||
|
menuIds: permissionValue.value.menuIds,
|
||||||
|
buttonCodes: permissionValue.value.buttonCodes,
|
||||||
|
dataScope: permissionValue.value.dataScope
|
||||||
|
}
|
||||||
|
const res =
|
||||||
|
modalMode.value === 'create' ? await createRoleApi(payload) : await updateRoleApi(form.id, payload)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
message.success(modalMode.value === 'create' ? '新增成功' : '修改成功')
|
||||||
|
modalOpen.value = false
|
||||||
|
proTableRef.value.reload()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(record) {
|
||||||
|
const res = await deleteRoleApi(record.id)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
message.success('删除成功')
|
||||||
|
proTableRef.value.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.danger-link {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -136,7 +136,7 @@ const roleOptions = ref([])
|
||||||
|
|
||||||
async function loadRoleOptions() {
|
async function loadRoleOptions() {
|
||||||
const res = await fetchRoleListApi({ status: 'enabled' })
|
const res = await fetchRoleListApi({ status: 'enabled' })
|
||||||
if (res.data.code === 200) roleOptions.value = res.data.data
|
if (res.data.code === 200) roleOptions.value = res.data.data.list
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUserList(params) {
|
async function loadUserList(params) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue