Files
crawler-plugin/admin-frontend-vue/src/pages/account/UserMenuAuthTree.vue
T

132 lines
4.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.datasetCheckedKeys 必须等树
// 渲染完成后再应用已勾选 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>