admin-vben5/apps/web-antd/src/components/tree/src/menu-select-table.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>