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 @@ + + + + + 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) {