@@ -1,12 +1,14 @@
< script lang = "ts" setup >
import type { RolePermAssignContext , RolePermAssignResult , RolePermTreeNode } from '#/api/iam/perm/role-perm.api' ;
import { computed , ref } from 'vue' ;
import { computed , ref , watch } from 'vue' ;
import { $t } from '@vben/locales' ;
import { RoleApi } from '#/api/iam/perm/role.api' ;
import { RolePermApi } from '#/api/iam/perm/role-perm.api' ;
import { clientCodeColorMap , clientCodeI18nMap } from '#/enums/clientCode' ;
import { menuTypeDotClassMap , menuTypeI18nMap } from '#/enums/menuType' ;
import { useMessage } from '#/hooks/useMessage' ;
import { formatPermCodeTitle } from '#/utils/perm-i18n' ;
@@ -37,7 +39,7 @@
const menuLoading = ref ( false ) ;
// 当前角色信息
const role = ref < RolePermAssignContext > ( { } ) ;
// 树形数据
// 树形数据(源数据,索引与勾选逻辑基于此)
const treeData = ref < TreeNode [ ] > ( [ ] ) ;
// 手动勾选的菜单ID列表
const manualCheckedMenuIds = ref < KeyValue [ ] > ( [ ] ) ;
@@ -49,6 +51,16 @@
const checkedKeys = ref < KeyValue [ ] > ( [ ] ) ;
// 半选状态的菜单key列表
const halfCheckedMenuKeys = ref < KeyValue [ ] > ( [ ] ) ;
// 展开的节点key
const expandedKeys = ref < KeyValue [ ] > ( [ ] ) ;
// 搜索关键字
const searchKeyword = ref ( '' ) ;
// 仅看已选
const onlySelected = ref ( false ) ;
// 只显示菜单(隐藏权限码节点)
const onlyMenus = ref ( false ) ;
// 勾选菜单时是否级联下属权限码(默认开启)
const cascadeCodes = ref ( true ) ;
// 节点映射表
const nodeMap = ref ( new Map < KeyValue , TreeNode > ( ) ) ;
// 菜单父级映射表
@@ -63,15 +75,63 @@
const menuIdValueMap = ref ( new Map < KeyValue , IdValue > ( ) ) ;
// 权限码ID值映射表
const codeIdValueMap = ref ( new Map < KeyValue , IdValue > ( ) ) ;
// 初始快照(脏检查)
const initialSnapshot = ref ( { menuIds : [ ] as KeyValue [ ] , codeIds : [ ] as KeyValue [ ] } ) ;
// 本地化后的树形数据
// 角色显示名
const roleDisplayName = computed ( ( ) => {
const r = role . value ;
if ( ! r . i18nKey ) {
return r . code || '' ;
}
const text = $t ( r . i18nKey ) ;
if ( ! text || text === r . i18nKey ) {
return r . code || r . i18nKey ;
}
return text ;
} ) ;
// 本地化后的完整树
const localizedTreeData = computed ( ( ) => mapTree ( treeData . value ) ) ;
// 过滤后用于展示的树(只显示菜单 / 仅看已选 / 搜索)
const displayTreeData = computed ( ( ) => {
let tree = localizedTreeData . value ;
if ( onlyMenus . value ) {
tree = filterTreeMenusOnly ( tree ) ;
}
if ( onlySelected . value ) {
tree = filterTreeBySelected ( tree ) ;
}
const keyword = searchKeyword . value . trim ( ) ;
if ( keyword ) {
tree = filterTreeByKeyword ( tree , keyword ) ;
}
return tree ;
} ) ;
// 树形组件勾选状态
const treeCheckedKeys = computed ( ( ) => ( {
checked : checkedKeys . value ,
halfChecked : halfCheckedMenuKeys . value ,
} ) ) ;
// 已选统计:菜单 = manual ∪ auto ∪ 祖先补齐前的真实选择;与提交口径一致取 manual∪ auto
const selectedMenuCount = computed ( ( ) => {
return uniqueKeys ( [ ... manualCheckedMenuIds . value , ... autoCheckedMenuIds . value ] ) . length ;
} ) ;
const selectedCodeCount = computed ( ( ) => uniqueKeys ( checkedCodeIds . value ) . length ) ;
// 是否有未保存变更
const isDirty = computed ( ( ) => {
const currentMenus = uniqueKeys ( [ ... manualCheckedMenuIds . value , ... autoCheckedMenuIds . value ] ) . sort ( ) ;
const currentCodes = uniqueKeys ( checkedCodeIds . value ) . sort ( ) ;
return (
JSON . stringify ( currentMenus ) !== JSON . stringify ( initialSnapshot . value . menuIds ) ||
JSON . stringify ( currentCodes ) !== JSON . stringify ( initialSnapshot . value . codeIds )
) ;
} ) ;
/** 规范化ID值, 转换为字符串键值 */
function normalizeId ( value ? : IdValue | null ) : KeyValue {
return value === undefined || value === null ? '' : String ( value ) ;
@@ -122,9 +182,106 @@
} ) ) ;
}
/** 节点是否处于已选( full) 状态 */
function isNodeSelected ( node : TreeNode ) : boolean {
const key = String ( node . key || '' ) ;
if ( ! key ) {
return false ;
}
if ( node . type === 'code' ) {
return checkedKeys . value . includes ( key ) ;
}
return checkedKeys . value . includes ( key ) || halfCheckedMenuKeys . value . includes ( key ) ;
}
/** 只显示菜单:剔除权限码节点,保留菜单树结构 */
function filterTreeMenusOnly ( nodes : TreeNode [ ] ) : TreeNode [ ] {
return nodes
. filter ( ( node ) => node . type !== 'code' )
. map ( ( node ) => ( {
... node ,
children : node . children ? . length ? filterTreeMenusOnly ( node . children ) : node . children ,
} ) ) ;
}
/** 仅看已选:保留已选节点及其祖先路径 */
function filterTreeBySelected ( nodes : TreeNode [ ] ) : TreeNode [ ] {
const result : TreeNode [ ] = [ ] ;
nodes . forEach ( ( node ) => {
const children = node . children ? . length ? filterTreeBySelected ( node . children ) : [ ] ;
if ( isNodeSelected ( node ) || children . length > 0 ) {
result . push ( { ... node , children } ) ;
}
} ) ;
return result ;
}
/** 关键字是否命中节点(标题 / 权限码 code) */
function nodeMatchesKeyword ( node : TreeNode , keyword : string ) : boolean {
const lower = keyword . toLowerCase ( ) ;
const title = ( node . displayTitle || '' ) . toLowerCase ( ) ;
if ( title . includes ( lower ) ) {
return true ;
}
if ( node . type === 'code' && node . code ? . toLowerCase ( ) . includes ( lower ) ) {
return true ;
}
return false ;
}
/** 按关键字过滤树:命中节点保留,并保留通往命中节点的祖先 */
function filterTreeByKeyword ( nodes : TreeNode [ ] , keyword : string ) : TreeNode [ ] {
const result : TreeNode [ ] = [ ] ;
nodes . forEach ( ( node ) => {
const children = node . children ? . length ? filterTreeByKeyword ( node . children , keyword ) : [ ] ;
if ( nodeMatchesKeyword ( node , keyword ) || children . length > 0 ) {
result . push ( { ... node , children } ) ;
}
} ) ;
return result ;
}
/** 收集过滤树中所有节点 key( 用于搜索后展开) */
function collectTreeKeys ( nodes : TreeNode [ ] ) : KeyValue [ ] {
const keys : KeyValue [ ] = [ ] ;
function walk ( list : TreeNode [ ] ) {
list . forEach ( ( node ) => {
if ( node . key ) {
keys . push ( String ( node . key ) ) ;
}
if ( node . children ? . length ) {
walk ( node . children ) ;
}
} ) ;
}
walk ( nodes ) ;
return keys ;
}
/** 默认展开:根菜单及其第一层子菜单(约 2 级),避免全展开卡顿 */
function getDefaultExpandedKeys ( nodes : TreeNode [ ] ) : KeyValue [ ] {
const keys : KeyValue [ ] = [ ] ;
nodes . forEach ( ( root ) => {
if ( root . key ) {
keys . push ( String ( root . key ) ) ;
}
root . children ? . forEach ( ( child ) => {
if ( child . type === 'menu' && child . key ) {
keys . push ( String ( child . key ) ) ;
}
} ) ;
} ) ;
return uniqueKeys ( keys ) ;
}
/** 初始化权限分配弹窗 */
async function init ( roleId : number ) {
visible . value = true ;
searchKeyword . value = '' ;
onlySelected . value = false ;
onlyMenus . value = false ;
cascadeCodes . value = true ;
expandedKeys . value = [ ] ;
const res = await RoleApi . findById ( String ( roleId ) ) ;
role . value = { ... res . data } ;
resetAssignState ( ) ;
@@ -146,6 +303,7 @@
codeMenuMap . value = new Map ( ) ;
menuIdValueMap . value = new Map ( ) ;
codeIdValueMap . value = new Map ( ) ;
initialSnapshot . value = { menuIds : [ ] , codeIds : [ ] } ;
}
/** 加载权限分配数据 */
@@ -169,6 +327,17 @@
manualCheckedMenuIds . value = uniqueKeys ( ( data . checkedMenuIds || [ ] ) . map ( ( item ) => normalizeId ( item ) ) ) ;
checkedCodeIds . value = uniqueKeys ( ( data . checkedCodeIds || [ ] ) . map ( ( item ) => normalizeId ( item ) ) ) ;
recomputeState ( ) ;
// 打开时默认展开 2 级
expandedKeys . value = getDefaultExpandedKeys ( localizedTreeData . value ) ;
takeSnapshot ( ) ;
}
/** 记录当前勾选快照,供脏检查 */
function takeSnapshot ( ) {
initialSnapshot . value = {
menuIds : uniqueKeys ( [ ... manualCheckedMenuIds . value , ... autoCheckedMenuIds . value ] ) . sort ( ) ,
codeIds : uniqueKeys ( checkedCodeIds . value ) . sort ( ) ,
} ;
}
/** 构建索引映射表 */
@@ -255,7 +424,7 @@
return [ ... new Set ( keys . filter ( Boolean ) ) ] ;
}
/** 收集菜单的所有子孙菜单ID */
/** 收集菜单的所有子孙菜单ID(含自身) */
function collectDescendantMenuIds ( menuId : KeyValue ) : KeyValue [ ] {
const result : KeyValue [ ] = [ ] ;
const visited = new Set < KeyValue > ( ) ;
@@ -274,6 +443,28 @@
return result ;
}
/** 收集菜单子树内全部权限码 ID */
function collectDescendantCodeIds ( menuId : KeyValue ) : KeyValue [ ] {
const codes : KeyValue [ ] = [ ] ;
collectDescendantMenuIds ( menuId ) . forEach ( ( mid ) => {
codes . push ( ... ( menuCodeMap . value . get ( mid ) || [ ] ) ) ;
} ) ;
return uniqueKeys ( codes ) ;
}
/** 收集菜单的全部祖先 ID( 不含自身) */
function collectAncestorMenuIds ( menuId : KeyValue ) : KeyValue [ ] {
const result : KeyValue [ ] = [ ] ;
let current = menuParentMap . value . get ( menuId ) ;
const visited = new Set < KeyValue > ( ) ;
while ( current && ! visited . has ( current ) ) {
visited . add ( current ) ;
result . push ( current ) ;
current = menuParentMap . value . get ( current ) ;
}
return result ;
}
/** 重新计算勾选状态 */
function recomputeState ( ) {
const manualMenuSet = new Set ( uniqueKeys ( manualCheckedMenuIds . value ) ) ;
@@ -289,13 +480,19 @@
autoCheckedMenuIds . value = [ ... autoMenuSet ] ;
const sourceSelectedMenuSet = new Set < KeyValue > ( [ ... manualMenuSet , ... autoMenuSet ] ) ;
// 显示与提交对齐:已选菜单的祖先也视为 full( 提交时会补齐)
const displaySelectedMenuSet = new Set < KeyValue > ( sourceSelectedMenuSet ) ;
sourceSelectedMenuSet . forEach ( ( menuId ) => {
collectAncestorMenuIds ( menuId ) . forEach ( ( aid ) => displaySelectedMenuSet . add ( aid ) ) ;
} ) ;
const nextCheckedMenuKeys : KeyValue [ ] = [ ] ;
const nextHalfCheckedMenuKeys : KeyValue [ ] = [ ] ;
/** 递归访问菜单,计算勾选状态 */
function visitMenu ( menuId : KeyValue ) : MenuCheckState {
const childMenuIds = menuChildrenMap . value . get ( menuId ) || [ ] ;
const selfSelected = source SelectedMenuSet. has ( menuId ) ;
const selfSelected = display SelectedMenuSet. has ( menuId ) ;
const childStates : MenuCheckState [ ] = childMenuIds . map ( ( childMenuId ) => visitMenu ( childMenuId ) ) ;
const allChildrenFull = childStates . length > 0 && childStates . every ( ( item : MenuCheckState ) => item . full ) ;
const hasSelectedChild = childStates . some ( ( item : MenuCheckState ) => item . full || item . half ) ;
@@ -335,16 +532,29 @@
return info ? . node ? . dataRef || info ? . node ;
}
/** 处理菜单勾选事件,联动勾选所有子孙菜单 */
/**
* 处理菜单勾选:
* - 勾选:联动子孙菜单;开启 cascadeCodes 时同时勾选子树内全部权限码
* - 取消:联动清除子孙菜单,并始终清除子树内权限码(避免 auto 勾回)
*/
function handleMenuCheck ( menuId : KeyValue , checked : boolean ) {
const manualMenuSet = new Set ( uniqueKeys ( manualCheckedMenuIds . value ) ) ;
const checkedCodeSet = new Set ( uniqueKeys ( checkedCodeIds . value ) ) ;
const descendantMenuIds = collectDescendantMenuIds ( menuId ) ;
const descendantCodeIds = collectDescendantCodeIds ( menuId ) ;
if ( checked ) {
descendantMenuIds . forEach ( ( id ) => manualMenuSet . add ( id ) ) ;
if ( cascadeCodes . value ) {
descendantCodeIds . forEach ( ( codeId ) => checkedCodeSet . add ( codeId ) ) ;
}
} else {
descendantMenuIds . forEach ( ( id ) => manualMenuSet . delete ( id ) ) ;
// 取消菜单时始终清码,防止 autoChecked 把菜单勾回
descendantCodeIds . forEach ( ( codeId ) => checkedCodeSet . delete ( codeId ) ) ;
}
manualCheckedMenuIds . value = [ ... manualMenuSet ] ;
checkedCodeIds . value = [ ... checkedCodeSet ] ;
recomputeState ( ) ;
}
@@ -380,16 +590,60 @@
}
}
/** 展开/折叠回调 */
function handleExpand ( keys : KeyValue [ ] ) {
expandedKeys . value = keys ;
}
/** 全选:所有菜单 + 所有权限码 */
function handleCheckAll ( ) {
manualCheckedMenuIds . value = [ ... menuIdValueMap . value . keys ( ) ] ;
checkedCodeIds . value = [ ... codeIdValueMap . value . keys ( ) ] ;
recomputeState ( ) ;
}
/** 清空全部勾选 */
function handleUncheckAll ( ) {
manualCheckedMenuIds . value = [ ] ;
checkedCodeIds . value = [ ] ;
recomputeState ( ) ;
}
/** 展开全部 */
function handleExpandAll ( ) {
expandedKeys . value = collectTreeKeys ( localizedTreeData . value ) ;
}
/** 折叠全部 */
function handleCollapseAll ( ) {
expandedKeys . value = [ ] ;
}
/** 搜索时自动展开过滤结果中的节点路径 */
watch ( searchKeyword , ( value ) => {
if ( ! value . trim ( ) ) {
return ;
}
// 展开当前过滤结果中的全部节点,便于看到命中项
expandedKeys . value = uniqueKeys ( [ ... expandedKeys . value , ... collectTreeKeys ( displayTreeData . value ) ] ) ;
} ) ;
/** 取消按钮点击事件 */
function handleCancel ( ) {
visible . value = false ;
confirmLoading . value = false ;
}
/** 获取提交的菜单ID列表 */
/**
* 获取提交的菜单ID列表
* 在 manual ∪ auto 基础上向上补齐祖先,保证目录路径完整、与树显示 full 一致
*/
function getSubmitMenuIds ( ) {
const menuIds = uniqueKeys ( [ ... manualCheckedMenuIds . value , ... autoCheckedMenuIds . value ] ) ;
return menuIds
const menuIdSet = new Set ( uniqueKeys ( [ ... manualCheckedMenuIds . value , ... autoCheckedMenuIds . value ] ) ) ;
[ ... menuIdSet ] . forEach ( ( menuId ) => {
collectAncestorMenuIds ( menuId ) . forEach ( ( aid ) => menuIdSet . add ( aid ) ) ;
} ) ;
return [ ... menuIdSet ]
. map ( ( menuId ) => menuIdValueMap . value . get ( menuId ) )
. filter ( ( item ) : item is IdValue => item !== undefined && item !== null ) ;
}
@@ -403,11 +657,18 @@
/** 确认按钮点击事件,弹出确认对话框 */
async function handleOk ( ) {
if ( ! isDirty . value ) {
message . info ( $t ( 'iam.role.noChange' ) ) ;
return ;
}
confirm ( {
// 分配权限
title : $t ( 'iam.role.assignPermission' ) ,
// 确认要分配权限吗?
content : $t ( 'iam.role.assignPermissionConfirm' ) ,
// 确认要保存权限分配吗?(含统计)
content : $t ( 'iam.role.assignPermissionConfirmStats' , {
menu : selectedMenuCount . value ,
code : selectedCodeCount . value ,
} ) ,
okText : $t ( 'common.okText' ) ,
cancelText : $t ( 'common.cancelText' ) ,
onOk : async ( ) => {
@@ -427,8 +688,8 @@
await RolePermApi . save ( {
roleId ,
clientCode ,
menuIds : getSubmitMenuIds ( ) ,
codeIds : getSubmitCodeIds ( ) ,
menuIds : getSubmitMenuIds ( ) . map ( String ) ,
codeIds : getSubmitCodeIds ( ) . map ( String ) ,
updateChildren : false ,
} ) . finally ( ( ) => {
confirmLoading . value = false ;
@@ -439,6 +700,53 @@
emits ( 'ok' ) ;
}
/** 标题文本:优先 displayTitle, 其次 i18n */
function resolveNodeTitle ( node : TreeNode ) : string {
return node . displayTitle || getDisplayTitle ( node ) || '' ;
}
/** 权限码是否挂载到多个菜单(一码多挂) */
function isCodeMultiMounted ( codeId ? : IdValue | null ) : boolean {
const id = normalizeId ( codeId ) ;
return id ? ( codeMenuMap . value . get ( id ) ? . length ? ? 0 ) > 1 : false ;
}
/** 节点类型小圆点 class: 菜单按 menuType, 权限码中性色 */
function getNodeDotClass ( node : TreeNode ) : string {
if ( node . type === 'code' ) {
return 'bg-muted-foreground/50' ;
}
if ( node . menuType && menuTypeDotClassMap [ node . menuType ] ) {
return menuTypeDotClassMap [ node . menuType ] ! ;
}
return 'bg-muted-foreground/40' ;
}
/** 菜单类型悬停文案(有映射才返回) */
function getMenuTypeTip ( menuType ? : string ) : string {
if ( ! menuType || ! menuTypeI18nMap [ menuType ] ) {
return '' ;
}
return $t ( menuTypeI18nMap [ menuType ] ! ) ;
}
/** 拆分搜索高亮片段(供 titleRender 模板使用) */
function splitHighlight ( text : string ) : { before : string ; hit : string ; after : string } | null {
const keyword = searchKeyword . value . trim ( ) ;
if ( ! keyword || ! text ) {
return null ;
}
const idx = text . toLowerCase ( ) . indexOf ( keyword . toLowerCase ( ) ) ;
if ( idx < 0 ) {
return null ;
}
return {
before : text . slice ( 0 , idx ) ,
hit : text . slice ( idx , idx + keyword . length ) ,
after : text . slice ( idx + keyword . length ) ,
} ;
}
defineExpose ( { init } ) ;
< / script >
@@ -446,33 +754,141 @@
<!-- 国际化 : 分配权限 -- >
< a-drawer
:open = "visible"
:title = "$t('iam.role.assignPermission')"
:size = "820"
:mask-closable = "false"
@close ="handleCancel"
>
< template # title >
< div class = "flex min-w-0 items-center gap-2" >
< span class = "shrink-0" > { { $t ( 'iam.role.assignPermission' ) } } < / span >
< span v-if = "roleDisplayName" class="truncate text-sm font-normal text-muted-foreground" >
· {{ roleDisplayName }}
< / span >
< span v-if = "role.code" class="shrink-0 text-sm font-normal text-muted-foreground/80" >
( {{ role.code }} )
< / span >
< a -tag
v-if = "role.clientCode"
: color = "clientCodeColorMap[role.clientCode] || 'default'"
class = "!m-0 shrink-0"
>
{ { $t ( clientCodeI18nMap [ role . clientCode ] || role . clientCode ) } }
< / a-tag >
< / div >
< / template >
< template # extra >
< a-space >
< a-button @click ="handleCancel" > {{ $ t ( ' common.cancel ' ) }} < / a -button >
< a-button type = "primary" :loading = "confirmLoading" @click ="handleOk" >
< a-button type = "primary" :loading = "confirmLoading" : disabled = "!isDirty && !menuLoading" @click ="handleOk" >
{{ $ t ( ' common.save ' ) }}
< / a -button >
< / a-space >
< / template >
< a-spin :spinning = "menuLoading" >
< a-tree
v-if = "localizedTreeData.length > 0"
checkable
check -strictly
default -expand -all
:tree-data = "localizedTreeData"
:checked-keys = "treeCheckedKeys "
: field -names = " { children : ' children ' , key : ' key ' , title : ' displayTitle ' } "
@check ="handleCheck"
< div v-if = "!menuLoading && localizedTreeData.length > 0" class="flex flex-col gap-3" >
< ! - - 工具条 : 搜索 + 批量操作 + 过滤 - - >
< div class = "flex flex-col gap-2" >
< a-input
v -model :value = "searchKeyword"
allow -clear
:placeholder = "$t('iam.role.searchPerm') "
/ >
< div class = "flex flex-wrap items-center gap-2" >
< a-space wrap :size = "8" >
< a-button size = "small" @click ="handleCheckAll" > {{ $ t ( ' iam.role.checkAll ' ) }} < / a -button >
< a-button size = "small" @click ="handleUncheckAll" > {{ $ t ( ' iam.role.uncheckAll ' ) }} < / a -button >
< a-button size = "small" @click ="handleExpandAll" > {{ $ t ( ' iam.role.expandAll ' ) }} < / a -button >
< a-button size = "small" @click ="handleCollapseAll" > {{ $ t ( ' iam.role.collapseAll ' ) }} < / a -button >
< / a-space >
< a-divider type = "vertical" class = "!h-6" / >
< a-space wrap :size = "12" >
< a-tooltip :title = "$t('iam.role.cascadeCodesTip')" >
< a-checkbox v -model :checked = "cascadeCodes" >
{ { $t ( 'iam.role.cascadeCodes' ) } }
< / a-checkbox >
< / a-tooltip >
< a-checkbox v -model :checked = "onlyMenus" >
{ { $t ( 'iam.role.onlyMenus' ) } }
< / a-checkbox >
< a-checkbox v -model :checked = "onlySelected" >
{ { $t ( 'iam.role.onlySelected' ) } }
< / a-checkbox >
< / a-space >
< / div >
< div class = "text-sm text-muted-foreground" >
{ {
$t ( 'iam.role.selectedStats' , {
menu : selectedMenuCount ,
code : selectedCodeCount ,
} )
} }
< / div >
< / div >
< a-tree
v-if = "displayTreeData.length > 0"
checkable
check -strictly
:tree-data = "displayTreeData"
:checked-keys = "treeCheckedKeys"
:expanded-keys = "expandedKeys"
: field -names = " { children : ' children ' , key : ' key ' , title : ' displayTitle ' } "
@check ="handleCheck"
@expand ="handleExpand"
>
<!-- antdv - next Tree 自定义标题 : 类型小圆点区分 ; 权限码多挂时才提示同步 -- >
< template # titleRender = "node" >
< span class = "inline-flex max-w-full items-center gap-1.5" >
<!-- 菜单 : 圆点悬停显示类型名 -- >
< a-tooltip v-if = "node.type !== 'code' && getMenuTypeTip(node.menuType)" :title="getMenuTypeTip(node.menuType)" >
< span class = "inline-block h-1.5 w-1.5 shrink-0 rounded-full" :class = "getNodeDotClass(node)" / >
< / a-tooltip >
< span
v-else
class = "inline-block h-1.5 w-1.5 shrink-0 rounded-full"
:class = "getNodeDotClass(node)"
/ >
<!-- 权限码 : 多挂才包 tip ; 标题弱化 -- >
< template v-if = "node.type === 'code'" >
< a -tooltip v-if = "isCodeMultiMounted(node.codeId)" :title="$t('iam.role.codeMultiMountTip')" >
< span class = "text-muted-foreground" >
< template v-for = "hl in [splitHighlight(resolveNodeTitle(node))]" :key="'c'" >
< template v-if = "hl" >
{{ hl.before }} < span class = "text-red-500" > { { hl . hit } } < / span > { { hl . after } }
< / template >
< template v-else > {{ resolveNodeTitle ( node ) }} < / template >
< / template >
< / span >
< / a -tooltip >
< span v-else class = "text-muted-foreground" >
< template v-for = "hl in [splitHighlight(resolveNodeTitle(node))]" :key="'c2'" >
< template v-if = "hl" >
{{ hl.before }} < span class = "text-red-500" > { { hl . hit } } < / span > { { hl . after } }
< / template >
< template v-else > {{ resolveNodeTitle ( node ) }} < / template >
< / template >
< / span >
< / template >
< template v-else >
< template v-for = "hl in [splitHighlight(resolveNodeTitle(node))]" :key="'m'" >
< span v-if = "hl" >
{{ hl.before }} < span class = "text-red-500" > { { hl . hit } } < / span > { { hl . after } }
< / span >
< span v-else > {{ resolveNodeTitle ( node ) }} < / span >
< / template >
< / template >
< / span >
< / template >
< / a -tree >
< a-empty v-else :description = "$t('iam.role.assignPermissionEmpty')" / >
< / div >
<!-- 加载完成后树为空 -- >
< a-empty
v-else-if = "!menuLoading"
:description = "$t('iam.role.assignPermissionEmpty')"
/ >
<!-- 国际化 : 当前终端下暂无可分配权限 -- >
< a-empty v-else :description = "$t('iam.role.assignPermissionEmpty')" / >
< / a-spin >
< / a-drawer >
< / template >