132 lines
4.3 KiB
Vue
132 lines
4.3 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||
import type { MenuOptionNode } from './user-menu-auth'
|
||
import { compactDirectGrantIds } from './user-menu-auth'
|
||
import { fetchGrantableMenus } from './user-menu-auth-api'
|
||
|
||
const props = defineProps<{ checked?: number[] }>()
|
||
const emit = defineEmits<{
|
||
(e: 'update:checked', ids: number[]): void
|
||
(e: 'load', ok: boolean): void
|
||
}>()
|
||
|
||
/** 仅暴露 el-tree 我们需要的两个方法,避免整组件类型耦合。 */
|
||
type TreeRef = { setCheckedKeys(keys: unknown[]): void; getCheckedKeys(): unknown }
|
||
const adminTree = ref<{ setCheckedKeys(keys: unknown[]): void; getCheckedKeys(): unknown } | null>(null)
|
||
const appTree = ref<{ setCheckedKeys(keys: unknown[]): void; getCheckedKeys(): unknown } | null>(null)
|
||
const data = ref<MenuOptionNode[]>([])
|
||
const loaded = ref(false)
|
||
|
||
/** 两种一级分类…(与 API menuType 分区一致):后台(admin) / 桌面端(app)。 */
|
||
const adminNodes = computed(() => data.value.filter((node) => node.type === 'admin'))
|
||
const appNodes = computed(() => data.value.filter((node) => node.type === 'app'))
|
||
|
||
function applyChecked(): void {
|
||
const ids = Array.isArray(props.checked) ? props.checked.filter((id) => typeof id === 'number' && id > 0) : []
|
||
adminTree.value?.setCheckedKeys(ids)
|
||
appTree.value?.setCheckedKeys(ids)
|
||
}
|
||
|
||
function onCheck(): void {
|
||
const collect = (el: { getCheckedKeys(): unknown } | null): number[] => {
|
||
const keys = el ? el.getCheckedKeys() : []
|
||
return (Array.isArray(keys) ? keys : []).map((key) => Number(key)).filter((id) => id > 0)
|
||
}
|
||
const ids = [...collect(adminTree.value), ...collect(appTree.value)]
|
||
// 父子联动模式(未启用严格勾选)下勾选父菜单会自动全选全部子菜单;
|
||
// 提交前压缩为最小直接授权集:父级已勾选时后代不再重复提交(后端按“父级授权
|
||
// 展开全部后代”判断权限,与旧 admin.html 勾父清后代语义一致)。
|
||
emit('update:checked', compactDirectGrantIds(ids, data.value))
|
||
}
|
||
|
||
async function load(): Promise<void> {
|
||
try {
|
||
data.value = await fetchGrantableMenus()
|
||
loaded.value = true
|
||
emit('load', true)
|
||
// el-tree 的 store 通过异步 watch 同步 props.data,setCheckedKeys 必须等树
|
||
// 渲染完成后再应用已勾选 id,否则作用在空 store 上导致已授权不回显。
|
||
await nextTick()
|
||
applyChecked()
|
||
} catch {
|
||
loaded.value = false
|
||
emit('load', false)
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
watch(
|
||
() => props.checked,
|
||
() => {
|
||
if (loaded.value) applyChecked()
|
||
},
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<el-scrollbar max-height="300px" class="user-menu-tree-scroll">
|
||
<template v-if="adminNodes.length || appNodes.length">
|
||
<div class="menu-group-columns">
|
||
<div v-if="adminNodes.length" class="menu-group-block">
|
||
<div class="menu-group-title">后台</div>
|
||
<!-- 父子联动模式(未启用严格勾选):勾选父菜单自动全选全部子菜单,
|
||
勾父即代表全部后代,与后端“父级授权展开后代”的权限语义一致。 -->
|
||
<el-tree
|
||
ref="adminTree"
|
||
:data="adminNodes"
|
||
node-key="id"
|
||
show-checkbox
|
||
default-expand-all
|
||
:props="{ label: 'name', children: 'children' }"
|
||
@check="onCheck"
|
||
/>
|
||
</div>
|
||
<div v-if="appNodes.length" class="menu-group-block">
|
||
<div class="menu-group-title">桌面端</div>
|
||
<el-tree
|
||
ref="appTree"
|
||
:data="appNodes"
|
||
node-key="id"
|
||
show-checkbox
|
||
default-expand-all
|
||
:props="{ label: 'name', children: 'children' }"
|
||
@check="onCheck"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<p v-else class="menu-group-empty">暂无可用菜单</p>
|
||
</el-scrollbar>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.menu-group-columns {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
}
|
||
.menu-group-block {
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
padding: 0 10px 8px 0;
|
||
}
|
||
.menu-group-block:last-child {
|
||
padding-right: 0;
|
||
border-left: 1px solid #e6edf4;
|
||
padding-left: 16px;
|
||
}
|
||
.menu-group-title {
|
||
padding: 4px 0 6px;
|
||
font-size: 12.5px;
|
||
font-weight: 650;
|
||
color: #40586e;
|
||
border-bottom: 1px solid #e6edf4;
|
||
margin-bottom: 4px;
|
||
}
|
||
.menu-group-empty {
|
||
margin: 12px 0;
|
||
color: #8293a5;
|
||
font-size: 13px;
|
||
}
|
||
</style>
|