feat: 新的菜单选择组件(beta)

This commit is contained in:
dap
2025-01-04 16:25:14 +08:00
parent e53ac28331
commit ada7fdc78d
9 changed files with 1180 additions and 223 deletions

View File

@@ -1 +1,2 @@
export { default as MenuSelectTable } from './src/menu-select-table.vue';
export { default as TreeSelectPanel } from './src/tree-select-panel.vue';

View File

@@ -0,0 +1,440 @@
<script setup lang="tsx">
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { ID } from '#/api/common';
import type { MenuOption } from '#/api/system/menu/model';
import type { PropType } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';
import { cloneDeep, eachTree, findGroupParentIds } from '@vben/utils';
import { Alert, Checkbox, RadioGroup, Space } from 'ant-design-vue';
import { difference, uniq } from 'lodash-es';
import { h, markRaw, nextTick, onMounted, watch } from 'vue';
defineOptions({
name: 'MenuSelectTable',
inheritAttrs: false,
});
const props = withDefaults(
defineProps<{ defaultExpandAll?: boolean; menus: MenuOption[] }>(),
{
defaultExpandAll: true,
},
);
interface Permission {
checked: boolean;
id: ID;
label: string;
}
interface MenuPermissionOption extends MenuOption {
permissions: Permission[];
}
const checkedKeys = defineModel('checkedKeys', {
type: Array as PropType<(number | string)[]>,
default: () => [],
});
/**
* 是否节点关联
*/
const association = defineModel('association', {
type: Boolean,
default: true,
});
const menuTypes = {
C: { icon: markRaw(MenuIcon), value: '菜单' },
F: { icon: markRaw(OkButtonIcon), value: '按钮' },
M: { icon: markRaw(FolderIcon), value: '目录' },
};
const gridOptions: VxeGridProps = {
checkboxConfig: {
// checkbox显示的字段
labelField: 'label',
// 是否严格模式 即节点不关联
checkStrictly: !association.value,
},
size: 'small',
columns: [
{
type: 'checkbox',
title: '菜单名称',
field: 'label',
treeNode: true,
width: 230,
},
{
title: '图标',
field: 'icon',
width: 80,
slots: {
default: ({ row }) => {
if (row?.icon === '#') {
return '';
}
return (
<span class={'flex justify-center'}>
<VbenIcon icon={row.icon} />
</span>
);
},
},
},
{
title: '类型',
field: 'menuType',
width: 80,
slots: {
default: ({ row }) => {
const current = menuTypes[row.menuType as 'C' | 'F' | 'M'];
if (!current) {
return '未知';
}
return (
<span class="flex items-center justify-center gap-1">
{h(current.icon, { class: 'size-[18px]' })}
<span>{current.value}</span>
</span>
);
},
},
},
{
title: '权限标识',
field: 'permissions',
headerAlign: 'left',
align: 'left',
slots: {
default: 'permissions',
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
enabled: false,
},
toolbarConfig: {
// 自定义列
custom: false,
// 最大化
zoom: false,
// 刷新
refresh: false,
},
rowConfig: {
isHover: false,
isCurrent: false,
keyField: 'id',
},
/**
* 开启虚拟滚动
* 数据量小可以选择关闭
* 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
*/
scrollY: {
enabled: true,
gt: 0,
},
treeConfig: {
parentField: 'parentId',
rowField: 'id',
transform: false,
},
showOverflow: false,
};
/**
* 设置是否全选
* @param record 行记录
* @param checked 是否选中
*/
function setPermissionsChecked(record: MenuPermissionOption, checked: boolean) {
if (record?.permissions?.length > 0) {
// 全部设置为选中
record.permissions.forEach((permission: Permission) => {
permission.checked = checked;
});
}
}
// 所有子节点
function allChecked(record: MenuPermissionOption, checked: boolean) {
setPermissionsChecked(record, checked);
record.children?.forEach((permission) => {
allChecked(permission as MenuPermissionOption, checked);
});
}
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
gridEvents: {
checkboxChange: (params) => {
// 节点独立 不做处理
if (!association.value) {
return;
}
console.log('params', params);
// 选中还是取消选中
const checked = params.checked;
// 行
const record = params.row;
// 设置所有子节点选中状态
allChecked(record, checked);
},
checkboxAll: (params) => {
const records = params.$grid.getData();
records.forEach((item) => {
allChecked(item, params.checked);
});
},
},
});
function menusWithPermissions(menus: MenuOption[]) {
eachTree(menus, (item: MenuPermissionOption) => {
if (item.children && item.children.length > 0) {
// 所有为按钮的节点提取出来
const permissions = item.children.filter(
(child: MenuOption) => child.menuType === 'F',
);
// 取差集
const diffCollection = difference(item.children, permissions);
// 更新后的children 即去除按钮
item.children = diffCollection;
// permissions作为字段添加到item
const permissionsArr = permissions.map((permission) => {
return {
id: permission.id,
label: permission.label,
checked: false,
};
});
item.permissions = permissionsArr;
}
});
}
/**
* 设置表格选中
* @param menus menu
* @param keys 选中的key
* @param handleChildren 节点独立情况 不需要处理children
*/
function setCheckedByKeys(
menus: MenuPermissionOption[],
keys: (number | string)[],
handleChildren: 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来选中 节点独立情况不需要处理
handleChildren && handlePermissionChange(item);
}
});
}
// 设置children选中
if (item.children && item.children.length > 0) {
setCheckedByKeys(item.children as any, keys, handleChildren);
}
});
}
onMounted(() => {
/**
* 加载表格数据
*/
watch(
() => props.menus,
async (menus) => {
const clonedMenus = cloneDeep(menus);
menusWithPermissions(clonedMenus);
console.log(clonedMenus);
await tableApi.grid.loadData(clonedMenus);
await nextTick();
if (props.defaultExpandAll) {
setExpandOrCollapse(true);
}
},
);
/**
* 节点关联设置表格勾选效果
*/
watch(association, (value) => {
tableApi.setGridOptions({
checkboxConfig: {
checkStrictly: !value,
},
});
});
watch(checkedKeys, (value) => {
console.log(props.menus);
const allCheckedKeys = uniq([...value]);
console.log(allCheckedKeys);
// 赋值
const records = tableApi.grid.getData();
setCheckedByKeys(records, allCheckedKeys, association.value);
});
});
const options = [
{ label: '节点关联', value: true },
{ label: '节点独立', value: false },
];
async function handleAssociationChange() {
// 需要清空全部勾选
await tableApi.grid.clearCheckboxRow();
// 清空全部permissions选中
const records = tableApi.grid.getData();
records.forEach((item) => {
allChecked(item, false);
});
}
/**
* 全部展开/折叠
* @param expand 是否展开
*/
function setExpandOrCollapse(expand: boolean) {
tableApi.grid?.setAllTreeExpand(expand);
}
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);
}
}
// 节点独立 不处理
}
/**
* 获取勾选的key
* @param records 行记录
*/
function getKeys(records: MenuPermissionOption[], handleChildren: boolean) {
const allKeys: (number | string)[] = [];
records.forEach((item) => {
if (item.children && item.children.length > 0) {
const keys = getKeys(
item.children as MenuPermissionOption[],
handleChildren,
);
allKeys.push(...keys);
} else {
// 当前id
handleChildren && 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();
console.log(records);
// 子节点
const nodeKeys = getKeys(records, true);
// 父节点
const parentIds = findGroupParentIds(props.menus, nodeKeys as number[]);
const realKeys = uniq([...parentIds, ...nodeKeys]);
console.log(realKeys);
return realKeys;
}
// 节点独立
// 勾选的行
const records = tableApi.grid.getCheckboxRecords();
// 全部数据 用于获取permissions
const allRecords = tableApi.grid.getData();
const ids = records.map((item) => item.id);
const permissions = getKeys(allRecords, false);
const allIds = uniq([...ids, ...permissions]);
console.log(allIds);
return allIds;
}
defineExpose({
getCheckedKeys,
});
</script>
<template>
<div class="flex h-full flex-col">
<Alert message="beta功能" type="warning" show-icon />
<BasicTable>
<template #toolbar-actions>
<RadioGroup
v-model:value="association"
:options="options"
button-style="solid"
option-type="button"
@change="handleAssociationChange"
/>
</template>
<template #toolbar-tools>
<Space>
<a-button @click="getCheckedKeys">测试expose</a-button>
<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-3">
<Checkbox
v-for="permission in row.permissions"
:key="permission.id"
v-model:checked="permission.checked"
@change="() => handlePermissionChange(row)"
>
{{ permission.label }}
</Checkbox>
</div>
</template>
</BasicTable>
</div>
</template>