feat(需求): ①用户创建/编辑菜单权限按「后台/桌面端」一级分组展示(双树分区勾选合并) ②全站表格操作列编辑/删除横向对齐(ops-cell 统一 flex+gap 8px)
This commit is contained in:
@@ -338,11 +338,12 @@ h3 {
|
|||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -517,11 +517,12 @@ tr.is-column-dragging td {
|
|||||||
color: #5b6f83;
|
color: #5b6f83;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import type { MenuOptionNode } from './user-menu-auth'
|
import type { MenuOptionNode } from './user-menu-auth'
|
||||||
import { fetchGrantableMenus } from './user-menu-auth-api'
|
import { fetchGrantableMenus } from './user-menu-auth-api'
|
||||||
|
|
||||||
@@ -11,22 +11,27 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
/** 仅暴露 el-tree 我们需要的两个方法,避免整组件类型耦合。 */
|
/** 仅暴露 el-tree 我们需要的两个方法,避免整组件类型耦合。 */
|
||||||
type TreeRef = { setCheckedKeys(keys: unknown[]): void; getCheckedKeys(): unknown }
|
type TreeRef = { setCheckedKeys(keys: unknown[]): void; getCheckedKeys(): unknown }
|
||||||
const tree = ref<{ setCheckedKeys(keys: unknown[]): void; getCheckedKeys(): unknown } | null>(null)
|
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 data = ref<MenuOptionNode[]>([])
|
||||||
const loaded = ref(false)
|
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 {
|
function applyChecked(): void {
|
||||||
const el: TreeRef | null = tree.value
|
|
||||||
if (!el) return
|
|
||||||
const ids = Array.isArray(props.checked) ? props.checked.filter((id) => typeof id === 'number' && id > 0) : []
|
const ids = Array.isArray(props.checked) ? props.checked.filter((id) => typeof id === 'number' && id > 0) : []
|
||||||
el.setCheckedKeys(ids)
|
adminTree.value?.setCheckedKeys(ids)
|
||||||
|
appTree.value?.setCheckedKeys(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCheck(): void {
|
function onCheck(): void {
|
||||||
const el: TreeRef | null = tree.value
|
const collect = (el: { getCheckedKeys(): unknown } | null): number[] => {
|
||||||
if (!el) return
|
const keys = el ? el.getCheckedKeys() : []
|
||||||
const keys = el.getCheckedKeys()
|
return (Array.isArray(keys) ? keys : []).map((key) => Number(key)).filter((id) => id > 0)
|
||||||
const ids = (Array.isArray(keys) ? keys : []).map((key) => Number(key)).filter((id) => id > 0)
|
}
|
||||||
|
const ids = [...collect(adminTree.value), ...collect(appTree.value)]
|
||||||
emit('update:checked', ids)
|
emit('update:checked', ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,15 +58,53 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-scrollbar max-height="300px" class="user-menu-tree-scroll">
|
<el-scrollbar max-height="300px" class="user-menu-tree-scroll">
|
||||||
<el-tree
|
<template v-if="adminNodes.length || appNodes.length">
|
||||||
ref="tree"
|
<div v-if="adminNodes.length" class="menu-group-block">
|
||||||
:data="data"
|
<div class="menu-group-title">后台</div>
|
||||||
node-key="id"
|
<el-tree
|
||||||
show-checkbox
|
ref="adminTree"
|
||||||
check-strictly
|
:data="adminNodes"
|
||||||
default-expand-all
|
node-key="id"
|
||||||
:props="{ label: 'name', children: 'children' }"
|
show-checkbox
|
||||||
@check="onCheck"
|
check-strictly
|
||||||
/>
|
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
|
||||||
|
check-strictly
|
||||||
|
default-expand-all
|
||||||
|
:props="{ label: 'name', children: 'children' }"
|
||||||
|
@check="onCheck"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="menu-group-empty">暂无可用菜单</p>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.menu-group-block + .menu-group-block {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.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>
|
||||||
|
|||||||
@@ -410,11 +410,12 @@ h3 {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -430,11 +430,12 @@ h3 {
|
|||||||
color: #2f5d8b;
|
color: #2f5d8b;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -661,9 +661,7 @@ h3 {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -677,11 +677,12 @@ h3 {
|
|||||||
color: #8293a5;
|
color: #8293a5;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.load-more-row td {
|
.load-more-row td {
|
||||||
background: #fbfdfe;
|
background: #fbfdfe;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,11 +449,12 @@ h3 {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -441,11 +441,12 @@ h3 {
|
|||||||
border-color: #d6e2ec;
|
border-color: #d6e2ec;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -505,11 +505,12 @@ h3 {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.ops-cell {
|
.ops-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.ops-cell .btn + .btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 44px 24px;
|
padding: 44px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user