diff --git a/mock-server/data/menu.js b/mock-server/data/menu.js
index 3e6d747..7dba21c 100644
--- a/mock-server/data/menu.js
+++ b/mock-server/data/menu.js
@@ -61,3 +61,21 @@ export function flattenButtons(tree) {
walk(tree)
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)
+}
+
diff --git a/mock-server/routes/auth.js b/mock-server/routes/auth.js
index bb39dfb..a4bc648 100644
--- a/mock-server/routes/auth.js
+++ b/mock-server/routes/auth.js
@@ -1,7 +1,8 @@
import express from 'express'
import crypto from 'crypto'
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 {
accessTokens,
refreshTokens,
@@ -17,6 +18,16 @@ function findUser(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) => {
const { username, password } = req.body || {}
const user = findUser(username)
@@ -54,14 +65,15 @@ router.post('/login', (req, res) => {
maxAge: REFRESH_TOKEN_TTL_MS,
sameSite: 'lax'
})
+ const { menuTree: userMenuTree, buttonCodes: userButtonCodes } = resolveUserMenu(user)
res.json({
code: 200,
msg: 'ok',
data: {
accessToken,
userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds },
- menuTree,
- buttonCodes: flattenButtons(menuTree),
+ menuTree: userMenuTree,
+ buttonCodes: userButtonCodes,
mustChangePassword: !!user.mustChangePassword
}
})
@@ -91,7 +103,9 @@ router.get('/userinfo', 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
diff --git a/mock-server/routes/system.js b/mock-server/routes/system.js
index ab5d0ac..01dd6c3 100644
--- a/mock-server/routes/system.js
+++ b/mock-server/routes/system.js
@@ -1,13 +1,15 @@
import express from 'express'
-import crypto from 'crypto'
import { orgs, nextOrgId, buildOrgTree, collectDescendantIds } from '../data/orgs.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'
const router = express.Router()
router.use(requireAuth)
+const DATA_SCOPES = ['own', 'ownAndSub', 'all']
+
function orgName(orgId) {
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 })
})
-// ---------------- 角色管理(简版,供用户管理下拉使用;完整 CRUD 见后续路由) ----------------
+// ---------------- 角色管理 ----------------
router.get('/role/list', (req, res) => {
- const { status } = req.query || {}
+ const { name, orgId, status, page = 1, pageSize = 1000 } = req.query || {}
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)
- res.json({
- code: 200,
- msg: 'ok',
- data: list.map((r) => ({ id: r.id, name: r.name, orgId: r.orgId, status: r.status, builtin: r.builtin }))
- })
+
+ const total = list.length
+ const start = (Number(page) - 1) * Number(pageSize)
+ 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
diff --git a/src/api/system.js b/src/api/system.js
index b928a5e..2a5d7e9 100644
--- a/src/api/system.js
+++ b/src/api/system.js
@@ -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 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')
diff --git a/src/components/PermissionTree.vue b/src/components/PermissionTree.vue
new file mode 100644
index 0000000..ca3da50
--- /dev/null
+++ b/src/components/PermissionTree.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
数据权限范围
+
+ 本机构
+ 本机构及下级机构
+ 全部机构
+
+
+
+
+
+
+
+
diff --git a/src/views/system/role/RoleList.vue b/src/views/system/role/RoleList.vue
new file mode 100644
index 0000000..ea0c2f0
--- /dev/null
+++ b/src/views/system/role/RoleList.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增角色
+
+
+
+
+ {{ index + 1 }}
+
+ {{ record.name }}
+ 内置
+
+
+
+ 查看
+ 修改
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/UserList.vue b/src/views/system/user/UserList.vue
index 0caef33..aac2832 100644
--- a/src/views/system/user/UserList.vue
+++ b/src/views/system/user/UserList.vue
@@ -136,7 +136,7 @@ const roleOptions = ref([])
async function loadRoleOptions() {
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) {