20260920220520
This commit is contained in:
1 parent
6f03d92425
commit
b3364fe6dc
44 files changed
+2313
-2737
No files matched your search
@@ -2,18 +2,26 @@ import { computed, ref, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { loadDataApi } from '@/services/api'
|
||||
import { useAuthStore } from './auth'
|
||||
import { usePermissionStore } from './permissions'
|
||||
import { buildTree, sortTree } from '@/utils/tree'
|
||||
|
||||
/** 递归裁剪菜单树:目录仅当存在可见子菜单时渲染(空目录不显示) */
|
||||
function pruneByAccess(nodes) {
|
||||
/**
|
||||
* 递归裁剪菜单树:
|
||||
* - 页面 / 外链菜单:没有该菜单的 menu.* 授权时不渲染(白名单:无记录即无入口);
|
||||
* - 目录:仅当存在可见子菜单时渲染(空目录不显示)。
|
||||
* @param {Array} nodes 菜单树
|
||||
* @param {(node: Object) => boolean} canEnter 是否能进入该菜单页面
|
||||
*/
|
||||
function pruneByAccess(nodes, canEnter) {
|
||||
const result = []
|
||||
for (const node of nodes) {
|
||||
const children = node.children?.length ? pruneByAccess(node.children) : []
|
||||
const children = node.children?.length ? pruneByAccess(node.children, canEnter) : []
|
||||
// 目录:没有可访问子菜单时不渲染
|
||||
if (node.b_menu_type === 'directory') {
|
||||
if (children.length) result.push({ ...node, children })
|
||||
continue
|
||||
}
|
||||
if (!canEnter(node)) continue
|
||||
result.push(children.length ? { ...node, children } : { ...node })
|
||||
}
|
||||
return result
|
||||
@@ -24,13 +32,15 @@ function pruneByAccess(nodes) {
|
||||
*
|
||||
* 菜单树由「菜单管理」维护在 s_menu 表,含目录 / 页面 / 外链三类节点。
|
||||
* 可见性规则由前端执行(不改后端):
|
||||
* - 全部启用菜单按登录主体加载,目录仅当存在可见子菜单时渲染(空目录不显示);
|
||||
* - 全部启用菜单按登录主体加载,再按 menu.* 授权裁剪(无授权不渲染),
|
||||
* 目录仅当存在可见子菜单时渲染(空目录不显示);
|
||||
* - 模块中心等系统管理员专用页面不进 s_menu:入口固定写在 AppSidebar(仅 g3soft 渲染),
|
||||
* 路由层由 meta.adminOnly 守卫拦截非 g3soft,菜单管理也配不出这些路由。
|
||||
* 与权限 store 一致:同一登录主体加载去重、并发请求复用、登录主体切换清空缓存。
|
||||
*/
|
||||
export const useMenuStore = defineStore('menus', () => {
|
||||
const authStore = useAuthStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
/** s_menu 原始记录(b_canuse = 1,接口按 b_xh / b_id 排序返回) */
|
||||
const rawMenus = ref([])
|
||||
const loadedPrincipalKey = ref('')
|
||||
@@ -67,14 +77,37 @@ export const useMenuStore = defineStore('menus', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前主体可见的菜单树(s_menu 原始行结构,含 children 子树)。
|
||||
* 当前主体可见的菜单树(s_menu 原始行结构,含 children 子树):
|
||||
* 已按 menu.* 授权裁剪,未授权的页面菜单与因此变空的目录都不出现。
|
||||
*/
|
||||
const visibleMenus = computed(() => {
|
||||
if (!rawMenus.value.length) return []
|
||||
const tree = sortTree(buildTree(JSON.parse(JSON.stringify(rawMenus.value))))
|
||||
return pruneByAccess(tree)
|
||||
return pruneByAccess(tree, (node) => permissionStore.canEnterMenu(node.b_id))
|
||||
})
|
||||
|
||||
/**
|
||||
* 按路由地址找菜单:b_route 精确匹配,或命中它的子路径(如详情页 /base/othercompany/123)。
|
||||
* 返回 null 表示该路由不在 s_menu(工作台、模块中心等),路由守卫据此放行。
|
||||
* @param {string} path 路由 path(不含 query)
|
||||
* @returns {Object|null} s_menu 行
|
||||
*/
|
||||
function findMenuByRoute(path) {
|
||||
const target = String(path || '')
|
||||
.split('?')[0]
|
||||
.replace(/\/+$/, '')
|
||||
if (!target) return null
|
||||
return (
|
||||
rawMenus.value.find((row) => {
|
||||
const route = String(row.b_route || '')
|
||||
.trim()
|
||||
.replace(/\/+$/, '')
|
||||
if (!route) return false
|
||||
return target === route || target.startsWith(`${route}/`)
|
||||
}) || null
|
||||
)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
// 仅当“正在进行中的请求属于别的主体”时才作废它(如切换账号),
|
||||
// 否则保留进行中请求、只清空已加载数据,交由后续 load 结果填充。
|
||||
@@ -105,5 +138,6 @@ export const useMenuStore = defineStore('menus', () => {
|
||||
clear,
|
||||
rawMenus,
|
||||
visibleMenus,
|
||||
findMenuByRoute,
|
||||
}
|
||||
})
|
||||
@@ -1,33 +1,43 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { loadDataApi } from '@/services/api'
|
||||
import {
|
||||
buildActionPowerCode,
|
||||
buildMenuPowerCode,
|
||||
buildModulePowerCode,
|
||||
isStandardModuleAction,
|
||||
} from '@/utils/powerCodes'
|
||||
import { useAuthStore } from './auth'
|
||||
|
||||
/**
|
||||
* 权限 store:负责拉取模块 / 用户授权数据并暴露访问与功能点判断。
|
||||
* 逻辑与 fms-vue-old2 完全一致:
|
||||
* - 管理员账号(g3soft)提权,直接可见全部启用模块
|
||||
* - 普通用户按 b_user_module / b_user_power 授权
|
||||
* - 同一主体(orgId:userId)加载结果去重,并发请求复用
|
||||
* 权限 store:按《FMS新系统核心表结构设计》§14 读取新表并暴露访问与功能点判断。
|
||||
*
|
||||
* s_power 系统有哪些权限点(menu.* / module.* / action.*)
|
||||
* s_user_power 当前用户拥有哪些权限点(白名单:无记录即无权限)
|
||||
*
|
||||
* 判定口径(与后端 SqlPermissionService 一致):
|
||||
* - s_power 中不存在该权限点、或任一侧 b_canuse = 0,授权一律不生效;
|
||||
* - 标准操作(read / create / update / delete / export)由「模块使用权限」统一表达,
|
||||
* 有 module.* 即允许(见 @/utils/powerCodes);
|
||||
* - 业务动作(审核 / 反审 / 结算 …)按 action.{模块}.{动作} 单独授权;
|
||||
* - 菜单入口(menu.*)只控制「能否进入页面」,与模块权限相互独立。
|
||||
*
|
||||
* - 管理员账号(g3soft)提权:全部启用模块可见、一切判断放行,不查授权表;
|
||||
* - 同一主体(orgId:userId)加载结果去重,并发请求复用。
|
||||
*/
|
||||
export const usePermissionStore = defineStore('permissions', () => {
|
||||
const authStore = useAuthStore()
|
||||
const loadedPrincipalKey = ref('')
|
||||
const modules = ref([])
|
||||
const grantedPageIds = ref([])
|
||||
const moduleCodes = ref([])
|
||||
const powerCodes = ref([])
|
||||
/** 当前用户拥有的权限点编码(s_user_power ∩ s_power 启用行) */
|
||||
const grantedPowerCodes = ref([])
|
||||
let loadingPromise = null
|
||||
let loadingPrincipalKey = ''
|
||||
let loadVersion = 0
|
||||
|
||||
// 字符串业务键:模块编码即主键 b_id
|
||||
const moduleByCode = computed(
|
||||
() => new Map(modules.value.map((m) => [String(m.b_id || ''), m])),
|
||||
)
|
||||
const moduleById = computed(() => new Map(modules.value.map((m) => [String(m.b_id || ''), m])))
|
||||
const grantedPageSet = computed(() => new Set(grantedPageIds.value))
|
||||
const powerSet = computed(() => new Set(powerCodes.value))
|
||||
const moduleByCode = computed(() => new Map(modules.value.map((m) => [String(m.b_id || ''), m])))
|
||||
const grantedPowerSet = computed(() => new Set(grantedPowerCodes.value))
|
||||
const isG3soft = computed(() => {
|
||||
return (
|
||||
String(authStore.loginInfo.user?.account || '')
|
||||
@@ -55,9 +65,7 @@ export const usePermissionStore = defineStore('permissions', () => {
|
||||
.then((res) => {
|
||||
if (requestVersion !== loadVersion) return
|
||||
modules.value = res.data || []
|
||||
grantedPageIds.value = []
|
||||
moduleCodes.value = []
|
||||
powerCodes.value = []
|
||||
grantedPowerCodes.value = []
|
||||
loadedPrincipalKey.value = elevatedKey
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -74,34 +82,28 @@ export const usePermissionStore = defineStore('permissions', () => {
|
||||
|
||||
const requestVersion = ++loadVersion
|
||||
loadingPrincipalKey = principalKey
|
||||
// 授权表(b_user_module/b_user_power/s_module_power)在部分机构库不存在,
|
||||
// 失败时按无授权处理,不阻塞登录主流程。
|
||||
// 授权表在部分机构库可能尚未建立:失败时按无授权处理,不阻塞登录主流程。
|
||||
const failSafe = () => ({ data: [] })
|
||||
const quotedUserId = `N'${String(userId).replaceAll("'", "''")}'`
|
||||
const request = Promise.all([
|
||||
loadDataApi('s_module', 'b_canuse = 1', 'b_id ASC'),
|
||||
loadDataApi('b_user_module', `b_user_id = ${quotedUserId}`, 'b_id ASC').catch(failSafe),
|
||||
loadDataApi('b_user_power', `b_user_id = ${quotedUserId}`, 'b_id ASC').catch(failSafe),
|
||||
loadDataApi('s_module_power', 'b_canuse = 1', 'b_id ASC').catch(failSafe),
|
||||
loadDataApi('s_power', 'b_canuse = 1', 'b_id ASC').catch(failSafe),
|
||||
loadDataApi(
|
||||
's_user_power',
|
||||
`b_user_id = ${quotedUserId} AND b_canuse = 1`,
|
||||
'b_power_id ASC',
|
||||
).catch(failSafe),
|
||||
])
|
||||
.then(([moduleResponse, userModules, userPowers, powers]) => {
|
||||
.then(([moduleResponse, powerResponse, userPowerResponse]) => {
|
||||
if (requestVersion !== loadVersion) return
|
||||
const activeModules = moduleResponse.data || []
|
||||
const mById = new Map(activeModules.map((m) => [String(m.b_id), m]))
|
||||
const pById = new Map(powers.data.map((p) => [String(p.b_id), p]))
|
||||
const pageIds = userModules.data
|
||||
.map((item) => String(item.b_module_id || ''))
|
||||
.filter(Boolean)
|
||||
modules.value = activeModules
|
||||
grantedPageIds.value = pageIds
|
||||
// 模块编码即主键 b_id
|
||||
moduleCodes.value = userModules.data
|
||||
.map((item) => mById.get(String(item.b_module_id))?.b_id)
|
||||
.filter(Boolean)
|
||||
powerCodes.value = userPowers.data
|
||||
.map((item) => pById.get(String(item.b_power_id)))
|
||||
.filter((p) => Boolean(p))
|
||||
.map((power) => `${String(power.b_module_id)}.${power.b_id}`)
|
||||
// 只有 s_power 中启用的权限点、且用户被授权才算数(设计文档 14.2 判定规则 2)
|
||||
const activePowers = new Set(
|
||||
(powerResponse.data || []).map((row) => String(row.b_id || '')),
|
||||
)
|
||||
modules.value = moduleResponse.data || []
|
||||
grantedPowerCodes.value = (userPowerResponse.data || [])
|
||||
.map((row) => String(row.b_power_id || ''))
|
||||
.filter((code) => code && activePowers.has(code))
|
||||
loadedPrincipalKey.value = principalKey
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -114,47 +116,37 @@ export const usePermissionStore = defineStore('permissions', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 由模块编码向上查找其所属 page 类型模块的 ID。
|
||||
*/
|
||||
function resolvePageId(code) {
|
||||
let module = moduleByCode.value.get(String(code || ''))
|
||||
const visited = new Set()
|
||||
while (module) {
|
||||
const moduleId = String(module.b_id || '')
|
||||
if (!moduleId || visited.has(moduleId)) return ''
|
||||
visited.add(moduleId)
|
||||
if (module.b_module_type === 'page') return moduleId
|
||||
const parentId = String(module.b_parent_id || '')
|
||||
module = parentId ? moduleById.value.get(parentId) : undefined
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前用户是否有某模块的访问权限(管理员恒为 true)。
|
||||
* 判断当前用户是否有某模块的使用权限(管理员恒为 true)。
|
||||
* 白名单语义:没有 module.{模块编码} 授权即不可访问。
|
||||
*/
|
||||
function canAccess(moduleCode) {
|
||||
if (isG3soft.value || !moduleCode) return true
|
||||
const module = moduleByCode.value.get(String(moduleCode))
|
||||
if (!module) return false
|
||||
const moduleId = String(module.b_id || '')
|
||||
const pageId = resolvePageId(moduleCode)
|
||||
return grantedPageSet.value.has(moduleId) || (!!pageId && grantedPageSet.value.has(pageId))
|
||||
if (!moduleByCode.value.has(String(moduleCode))) return false
|
||||
return grantedPowerSet.value.has(buildModulePowerCode(moduleCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前用户是否有某模块的功能点权限。
|
||||
* 标准操作(read / create / update / delete / export)由模块使用权限统一表达,
|
||||
* 有 module.* 即允许;业务动作(audit 等)要求单独的 action.{模块}.{动作} 授权。
|
||||
*/
|
||||
function canPower(scopeCode, powerCode) {
|
||||
function canPower(scopeCode, action) {
|
||||
if (isG3soft.value) return true
|
||||
const module = moduleByCode.value.get(String(scopeCode || ''))
|
||||
if (!module || !canAccess(scopeCode)) return false
|
||||
const moduleId = String(module.b_id || '')
|
||||
const pageId = resolvePageId(scopeCode || '')
|
||||
return (
|
||||
powerSet.value.has(`${moduleId}.${powerCode}`) ||
|
||||
(!!pageId && powerSet.value.has(`${pageId}.${powerCode}`))
|
||||
)
|
||||
const code = String(scopeCode || '')
|
||||
const actionCode = String(action || '')
|
||||
if (!code || !actionCode) return false
|
||||
if (!canAccess(code)) return false
|
||||
if (isStandardModuleAction(actionCode)) return true
|
||||
return grantedPowerSet.value.has(buildActionPowerCode(code, actionCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前用户能否进入某个菜单页面(侧栏渲染与路由入口共用)。
|
||||
* 白名单语义:没有 menu.{菜单编码} 授权即不可见、不可进入。
|
||||
*/
|
||||
function canEnterMenu(menuCode) {
|
||||
if (isG3soft.value || !menuCode) return true
|
||||
return grantedPowerSet.value.has(buildMenuPowerCode(menuCode))
|
||||
}
|
||||
|
||||
function clear() {
|
||||
@@ -170,9 +162,7 @@ export const usePermissionStore = defineStore('permissions', () => {
|
||||
}
|
||||
loadedPrincipalKey.value = ''
|
||||
modules.value = []
|
||||
grantedPageIds.value = []
|
||||
moduleCodes.value = []
|
||||
powerCodes.value = []
|
||||
grantedPowerCodes.value = []
|
||||
}
|
||||
|
||||
// 登录主体变化(退出/切换账号)时清空权限缓存
|
||||
@@ -190,11 +180,10 @@ export const usePermissionStore = defineStore('permissions', () => {
|
||||
load,
|
||||
canAccess,
|
||||
canPower,
|
||||
canEnterMenu,
|
||||
clear,
|
||||
modules,
|
||||
grantedPageIds,
|
||||
moduleCodes,
|
||||
powerCodes,
|
||||
grantedPowerCodes,
|
||||
isG3soft,
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user