408 lines
10 KiB
Vue
408 lines
10 KiB
Vue
<!--
|
|
不兼容也不会兼容一些错误用法
|
|
比如: 菜单下放目录 菜单下放菜单
|
|
比如: 按钮下放目录 按钮下放菜单 按钮下放按钮
|
|
-->
|
|
<script setup lang="tsx">
|
|
import type { RadioChangeEvent } from 'ant-design-vue';
|
|
|
|
import type { MenuPermissionOption } from './data';
|
|
|
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
import type { MenuOption } from '#/api/system/menu/model';
|
|
|
|
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
|
|
|
|
import { cloneDeep, findGroupParentIds } from '@vben/utils';
|
|
|
|
import { Alert, Checkbox, RadioGroup, Space } from 'ant-design-vue';
|
|
import { uniq } from 'lodash-es';
|
|
|
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
|
|
import { columns, nodeOptions } from './data';
|
|
import {
|
|
menusWithPermissions,
|
|
rowAndChildrenChecked,
|
|
setPermissionsChecked,
|
|
setTableChecked,
|
|
} from './helper';
|
|
import { useFullScreenGuide } from './hook';
|
|
|
|
defineOptions({
|
|
name: 'MenuSelectTable',
|
|
inheritAttrs: false,
|
|
});
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
checkedKeys: (number | string)[];
|
|
defaultExpandAll?: boolean;
|
|
menus: MenuOption[];
|
|
}>(),
|
|
{
|
|
/**
|
|
* 是否默认展开全部
|
|
*/
|
|
defaultExpandAll: true,
|
|
/**
|
|
* 注意这里不是双向绑定 需要调用getCheckedKeys实例方法来获取真正选中的节点
|
|
*/
|
|
checkedKeys: () => [],
|
|
},
|
|
);
|
|
|
|
/**
|
|
* 是否节点关联
|
|
*/
|
|
const association = defineModel<boolean>('association', {
|
|
default: true,
|
|
});
|
|
|
|
const gridOptions: VxeGridProps = {
|
|
checkboxConfig: {
|
|
// checkbox显示的字段
|
|
labelField: 'label',
|
|
// 是否严格模式 即节点不关联
|
|
checkStrictly: !association.value,
|
|
},
|
|
size: 'small',
|
|
columns,
|
|
height: 'auto',
|
|
keepSource: true,
|
|
pagerConfig: {
|
|
enabled: false,
|
|
},
|
|
proxyConfig: {
|
|
enabled: false,
|
|
},
|
|
toolbarConfig: {
|
|
refresh: false,
|
|
custom: false,
|
|
},
|
|
rowConfig: {
|
|
isHover: false,
|
|
isCurrent: false,
|
|
keyField: 'id',
|
|
},
|
|
/**
|
|
* 开启虚拟滚动
|
|
* 数据量小可以选择关闭
|
|
* 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
|
|
*/
|
|
scrollY: {
|
|
enabled: true,
|
|
gt: 0,
|
|
},
|
|
treeConfig: {
|
|
parentField: 'parentId',
|
|
rowField: 'id',
|
|
transform: false,
|
|
},
|
|
// 溢出换行显示
|
|
showOverflow: false,
|
|
};
|
|
|
|
/**
|
|
* 用于界面显示选中的数量
|
|
*/
|
|
const checkedNum = ref(0);
|
|
/**
|
|
* 更新选中的数量
|
|
*/
|
|
function updateCheckedNumber() {
|
|
checkedNum.value = getCheckedKeys().length;
|
|
}
|
|
|
|
const [BasicTable, tableApi] = useVbenVxeGrid({
|
|
gridOptions,
|
|
gridEvents: {
|
|
// 勾选事件
|
|
checkboxChange: (params) => {
|
|
// 选中还是取消选中
|
|
const checked = params.checked;
|
|
// 行
|
|
const record = params.row;
|
|
if (association.value) {
|
|
// 节点关联
|
|
// 设置所有子节点选中状态
|
|
rowAndChildrenChecked(record, checked);
|
|
} else {
|
|
// 节点独立
|
|
// 点行会勾选/取消全部权限 点权限不会勾选行
|
|
setPermissionsChecked(record, checked);
|
|
}
|
|
updateCheckedNumber();
|
|
},
|
|
// 全选事件
|
|
checkboxAll: (params) => {
|
|
const records = params.$grid.getData();
|
|
records.forEach((item) => {
|
|
rowAndChildrenChecked(item, params.checked);
|
|
});
|
|
updateCheckedNumber();
|
|
},
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 设置表格选中
|
|
* @param menus menu
|
|
* @param keys 选中的key
|
|
* @param triggerOnchange 节点独立情况 不需要触发onChange(false)
|
|
*/
|
|
function setCheckedByKeys(
|
|
menus: MenuPermissionOption[],
|
|
keys: (number | string)[],
|
|
triggerOnchange: boolean,
|
|
) {
|
|
menus.forEach((item) => {
|
|
// 设置行选中
|
|
if (keys.includes(item.id)) {
|
|
tableApi.grid.setCheckboxRow(item, true);
|
|
}
|
|
// 设置权限columns选中
|
|
if (item.permissions && item.permissions.length > 0) {
|
|
// 遍历 设置勾选
|
|
item.permissions.forEach((permission) => {
|
|
if (keys.includes(permission.id)) {
|
|
permission.checked = true;
|
|
// 手动触发onChange来选中 节点独立情况不需要处理
|
|
triggerOnchange && handlePermissionChange(item);
|
|
}
|
|
});
|
|
}
|
|
// 设置children选中
|
|
if (item.children && item.children.length > 0) {
|
|
setCheckedByKeys(item.children as any, keys, triggerOnchange);
|
|
}
|
|
});
|
|
}
|
|
|
|
const { FullScreenGuide, openGuide } = useFullScreenGuide();
|
|
onMounted(() => {
|
|
/**
|
|
* 加载表格数据 转为指定结构
|
|
*/
|
|
watch(
|
|
() => props.menus,
|
|
async (menus) => {
|
|
const clonedMenus = cloneDeep(menus);
|
|
menusWithPermissions(clonedMenus);
|
|
// console.log(clonedMenus);
|
|
await tableApi.grid.loadData(clonedMenus);
|
|
// 展开全部 默认true
|
|
if (props.defaultExpandAll) {
|
|
await nextTick();
|
|
setExpandOrCollapse(true);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* 节点关联变动 更新表格勾选效果
|
|
*/
|
|
watch(association, (value) => {
|
|
tableApi.setGridOptions({
|
|
checkboxConfig: {
|
|
checkStrictly: !value,
|
|
},
|
|
});
|
|
});
|
|
|
|
/**
|
|
* checkedKeys依赖menus
|
|
* 要注意加载顺序
|
|
* !!!要在外部确保menus先加载!!!
|
|
*/
|
|
watch(
|
|
() => props.checkedKeys,
|
|
(value) => {
|
|
const allCheckedKeys = uniq([...value]);
|
|
// 获取表格data 如果checkedKeys在menus的watch之前触发 这里会拿到空 导致勾选异常
|
|
const records = tableApi.grid.getData();
|
|
setCheckedByKeys(records, allCheckedKeys, association.value);
|
|
updateCheckedNumber();
|
|
|
|
// 全屏引导
|
|
setTimeout(openGuide, 1000);
|
|
},
|
|
);
|
|
});
|
|
|
|
// 缓存上次(切换节点关系前)选中的keys
|
|
const lastCheckedKeys = shallowRef<(number | string)[]>([]);
|
|
/**
|
|
* 节点关联变动 事件
|
|
*/
|
|
async function handleAssociationChange(e: RadioChangeEvent) {
|
|
lastCheckedKeys.value = getCheckedKeys();
|
|
// 清空全部permissions选中
|
|
const records = tableApi.grid.getData();
|
|
records.forEach((item) => {
|
|
rowAndChildrenChecked(item, false);
|
|
});
|
|
// 需要清空全部勾选
|
|
await tableApi.grid.clearCheckboxRow();
|
|
// 滚动到顶部
|
|
await tableApi.grid.scrollTo(0, 0);
|
|
|
|
// 节点切换 不同的选中
|
|
setTableChecked(lastCheckedKeys.value, records, tableApi, !e.target.value);
|
|
|
|
updateCheckedNumber();
|
|
}
|
|
|
|
/**
|
|
* 全部展开/折叠
|
|
* @param expand 是否展开
|
|
*/
|
|
function setExpandOrCollapse(expand: boolean) {
|
|
tableApi.grid?.setAllTreeExpand(expand);
|
|
}
|
|
|
|
/**
|
|
* 权限列表 checkbox勾选的事件
|
|
* @param row 行
|
|
*/
|
|
function handlePermissionChange(row: any) {
|
|
// 节点关联
|
|
if (association.value) {
|
|
const checkedPermissions = row.permissions.filter(
|
|
(item: any) => item.checked === true,
|
|
);
|
|
// 有一条选中 则整个行选中
|
|
if (checkedPermissions.length > 0) {
|
|
tableApi.grid.setCheckboxRow(row, true);
|
|
}
|
|
// 无任何选中 则整个行不选中
|
|
if (checkedPermissions.length === 0) {
|
|
tableApi.grid.setCheckboxRow(row, false);
|
|
}
|
|
}
|
|
// 节点独立 不处理
|
|
updateCheckedNumber();
|
|
}
|
|
|
|
/**
|
|
* 获取勾选的key
|
|
* @param records 行记录列表
|
|
* @param addCurrent 是否添加当前行的id
|
|
*/
|
|
function getKeys(records: MenuPermissionOption[], addCurrent: boolean) {
|
|
const allKeys: (number | string)[] = [];
|
|
records.forEach((item) => {
|
|
// 处理children
|
|
if (item.children && item.children.length > 0) {
|
|
const keys = getKeys(item.children as MenuPermissionOption[], addCurrent);
|
|
allKeys.push(...keys);
|
|
} else {
|
|
// 当前行的id
|
|
addCurrent && allKeys.push(item.id);
|
|
// 当前行权限id 获取已经选中的
|
|
if (item.permissions && item.permissions.length > 0) {
|
|
const ids = item.permissions
|
|
.filter((m) => m.checked === true)
|
|
.map((m) => m.id);
|
|
allKeys.push(...ids);
|
|
}
|
|
}
|
|
});
|
|
return uniq(allKeys);
|
|
}
|
|
|
|
/**
|
|
* 获取选中的key
|
|
*/
|
|
function getCheckedKeys() {
|
|
// 节点关联
|
|
if (association.value) {
|
|
const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];
|
|
// 子节点
|
|
const nodeKeys = getKeys(records, true);
|
|
// 所有父节点
|
|
const parentIds = findGroupParentIds(props.menus, nodeKeys as number[]);
|
|
// 拼接 去重
|
|
const realKeys = uniq([...parentIds, ...nodeKeys]);
|
|
return realKeys;
|
|
}
|
|
// 节点独立
|
|
|
|
// 勾选的行
|
|
const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];
|
|
// 全部数据 用于获取permissions
|
|
const allRecords = tableApi?.grid?.getData?.() ?? [];
|
|
// 表格已经选中的行ids
|
|
const checkedIds = records.map((item) => item.id);
|
|
// 所有已经勾选权限的ids
|
|
const permissionIds = getKeys(allRecords, false);
|
|
// 合并 去重
|
|
const allIds = uniq([...checkedIds, ...permissionIds]);
|
|
return allIds;
|
|
}
|
|
|
|
/**
|
|
* 暴露给外部使用 获取已选中的key
|
|
*/
|
|
defineExpose({
|
|
getCheckedKeys,
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex h-full flex-col" id="menu-select-table">
|
|
<BasicTable>
|
|
<template #toolbar-actions>
|
|
<RadioGroup
|
|
v-model:value="association"
|
|
:options="nodeOptions"
|
|
button-style="solid"
|
|
option-type="button"
|
|
@change="handleAssociationChange"
|
|
/>
|
|
<Alert class="mx-2" type="info">
|
|
<template #message>
|
|
<div>
|
|
已选中
|
|
<span class="text-primary mx-1 font-semibold">
|
|
{{ checkedNum }}
|
|
</span>
|
|
个节点
|
|
</div>
|
|
</template>
|
|
</Alert>
|
|
</template>
|
|
<template #toolbar-tools>
|
|
<Space>
|
|
<a-button @click="setExpandOrCollapse(false)">
|
|
{{ $t('pages.common.collapse') }}
|
|
</a-button>
|
|
<a-button @click="setExpandOrCollapse(true)">
|
|
{{ $t('pages.common.expand') }}
|
|
</a-button>
|
|
</Space>
|
|
</template>
|
|
<template #permissions="{ row }">
|
|
<div class="flex flex-wrap gap-x-3 gap-y-1">
|
|
<Checkbox
|
|
v-for="permission in row.permissions"
|
|
:key="permission.id"
|
|
v-model:checked="permission.checked"
|
|
@change="() => handlePermissionChange(row)"
|
|
>
|
|
{{ permission.label }}
|
|
</Checkbox>
|
|
</div>
|
|
</template>
|
|
</BasicTable>
|
|
<!-- 全屏引导 -->
|
|
<FullScreenGuide />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.ant-alert) {
|
|
padding: 4px 8px;
|
|
}
|
|
</style>
|