【QQYUN-5571】自封装选择列,解决BasicTable数据多,行选择卡顿问题

pull/620/head
zhangdaiscott 2023-07-03 22:28:09 +08:00
parent 72a0e7dbe3
commit e9c2854b43
13 changed files with 538 additions and 22 deletions

View File

@ -113,18 +113,18 @@
{
title: '职务编码',
dataIndex: 'code',
width: 40,
width: 180,
align: 'left',
},
{
title: '职务名称',
dataIndex: 'name',
width: 40,
// width: 180,
},
{
title: '职务等级',
dataIndex: 'postRank_dictText',
width: 40,
width: 180,
},
];
//table

View File

@ -80,13 +80,13 @@
{
title: '角色名称',
dataIndex: 'roleName',
width: 40,
width: 240,
align: 'left',
},
{
title: '角色编码',
dataIndex: 'roleCode',
width: 40,
// width: 40,
},
];

View File

@ -76,22 +76,22 @@
{
title: '用户账号',
dataIndex: 'username',
width: 50,
width: 180,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 50,
width: 180,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 50,
width: 80,
},
{
title: '手机号码',
dataIndex: 'phone',
width: 50,
// width: 50,
},
],
useSearchForm: true,

View File

@ -145,33 +145,33 @@
{
title: '用户账号',
dataIndex: 'username',
width: 40,
width: 120,
align: 'left',
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 40,
width: 120,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 20,
width: 50,
},
{
title: '手机号码',
dataIndex: 'phone',
width: 30,
width: 120,
},
{
title: '邮箱',
dataIndex: 'email',
width: 40,
// width: 40,
},
{
title: '状态',
dataIndex: 'status_dictText',
width: 20,
width: 80,
},
];
//table

View File

@ -79,7 +79,7 @@ export function useSelectBiz(getList, props) {
*/
const indexColumnProps = {
dataIndex: 'index',
width: 20,
width: 50,
};
/**

View File

@ -22,7 +22,10 @@
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #headerCell="{ column }">
<HeaderCell :column="column" />
<!-- update-begin--author:sunjianlei---date:220230630---forQQYUN-5571自封装选择列解决数据行选择卡顿问题 -->
<CustomSelectHeader v-if="isCustomSelection(column)" v-bind="selectHeaderProps"/>
<HeaderCell v-else :column="column" />
<!-- update-end--author:sunjianlei---date:220230630---forQQYUN-5571自封装选择列解决数据行选择卡顿问题 -->
</template>
<!-- 增加对antdv3.x兼容 -->
<template #bodyCell="data">
@ -39,6 +42,7 @@
import { Table } from 'ant-design-vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { PageWrapperFixedHeightKey } from '/@/components/Page/injectionKey';
import CustomSelectHeader from './components/CustomSelectHeader.vue'
import expandIcon from './components/ExpandIcon';
import HeaderCell from './components/HeaderCell.vue';
import { InnerHandlers } from './types/table';
@ -56,6 +60,7 @@
import { useTableFooter } from './hooks/useTableFooter';
import { useTableForm } from './hooks/useTableForm';
import { useDesign } from '/@/hooks/web/useDesign';
import { useCustomSelection } from "./hooks/useCustomSelection";
import { omit } from 'lodash-es';
import { basicProps } from './props';
@ -67,6 +72,7 @@
Table,
BasicForm,
HeaderCell,
CustomSelectHeader,
},
props: basicProps,
emits: [
@ -112,8 +118,34 @@
const { getLoading, setLoading } = useLoading(getProps);
const { getPaginationInfo, getPagination, setPagination, setShowPagination, getShowPagination } = usePagination(getProps);
const { getRowSelection, getRowSelectionRef, getSelectRows, clearSelectedRowKeys, getSelectRowKeys, deleteSelectRowByKey, setSelectedRowKeys } =
useRowSelection(getProps, tableData, emit);
// update-begin--author:sunjianlei---date:220230630---forQQYUN-5571
// const { getRowSelection, getRowSelectionRef, getSelectRows, clearSelectedRowKeys, getSelectRowKeys, deleteSelectRowByKey, setSelectedRowKeys } =
// useRowSelection(getProps, tableData, emit);
//
const childrenColumnName = computed(() => getProps.value.childrenColumnName || 'children');
//
const {
getRowSelection,
getSelectRows,
getSelectRowKeys,
setSelectedRowKeys,
getRowSelectionRef,
selectHeaderProps,
isCustomSelection,
handleCustomSelectColumn,
clearSelectedRowKeys,
deleteSelectRowByKey,
} = useCustomSelection(
getProps,
wrapRef,
getPaginationInfo,
tableData,
childrenColumnName
)
// update-end--author:sunjianlei---date:220230630---forQQYUN-5571
const {
handleTableChange: onTableChange,
@ -153,7 +185,10 @@
const { getViewColumns, getColumns, setCacheColumnsByField, setColumns, getColumnsRef, getCacheColumns } = useColumns(
getProps,
getPaginationInfo
getPaginationInfo,
// update-begin--author:sunjianlei---date:220230630---forQQYUN-5571
handleCustomSelectColumn,
// update-end--author:sunjianlei---date:220230630---forQQYUN-5571
);
const { getScrollRef, redoHeight } = useTableScroll(getProps, tableElRef, getColumnsRef, getRowSelectionRef, getDataSourceRef);
@ -213,10 +248,21 @@
}*/
//update-end---author:wangshuai ---date:20230214 for[QQYUN-4237] ------------
// update-begin--author:sunjianlei---date:220230630---forQQYUN-5571
//
delete propsData.rowSelection
// update-end--author:sunjianlei---date:220230630---forQQYUN-5571
propsData = omit(propsData, ['class', 'onChange']);
return propsData;
});
//
const getMaxColumnWidth = computed(() => {
const values = unref(getBindValues);
return values.maxColumnWidth > 0 ? values.maxColumnWidth + 'px' : null;
});
const getWrapperClass = computed(() => {
const values = unref(getBindValues);
return [
@ -225,6 +271,7 @@
{
[`${prefixCls}-form-container`]: values.useSearchForm,
[`${prefixCls}--inset`]: values.inset,
[`${prefixCls}-col-max-width`]: getMaxColumnWidth.value != null,
},
];
});
@ -300,7 +347,14 @@
replaceFormSlotKey,
getFormSlotKeys,
getWrapperClass,
getMaxColumnWidth,
columns: getViewColumns,
// update-begin--author:sunjianlei---date:220230630---forQQYUN-5571
selectHeaderProps,
isCustomSelection,
// update-end--author:sunjianlei---date:220230630---forQQYUN-5571
};
},
});
@ -429,5 +483,15 @@
padding: 0;
}
}
// ------ ------
&-col-max-width {
.ant-table-thead tr th,
.ant-table-tbody tr td {
max-width: v-bind(getMaxColumnWidth);
}
}
// ------ ------
}
</style>

View File

@ -0,0 +1,36 @@
<!-- 自定义选择列表头实现部分 -->
<template>
<a-checkbox :checked="checked" :indeterminate="isHalf" @update:checked="onChange" />
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
selectedLength: {
type: Number,
required: true,
},
//
pageSize: {
type: Number,
required: true,
},
});
const emit = defineEmits(['select-all']);
//
const checked = computed(() => {
return props.selectedLength > 0 && props.selectedLength >= props.pageSize;
});
//
const isHalf = computed(() => {
return props.selectedLength > 0 && props.selectedLength < props.pageSize;
});
function onChange(checked: boolean) {
emit('select-all', checked);
}
</script>
<style scoped lang="scss"></style>

View File

@ -9,6 +9,7 @@ import { isArray, isBoolean, isFunction, isMap, isString } from '/@/utils/is';
import { cloneDeep, isEqual } from 'lodash-es';
import { formatToDate } from '/@/utils/dateUtil';
import { ACTION_COLUMN_FLAG, DEFAULT_ALIGN, INDEX_COLUMN_FLAG, PAGE_SIZE } from '../const';
import { CUS_SEL_COLUMN_KEY } from "./useCustomSelection";
function handleItem(item: BasicColumn, ellipsis: boolean) {
const { key, dataIndex, children } = item;
@ -95,7 +96,11 @@ function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: Bas
}
}
export function useColumns(propsRef: ComputedRef<BasicTableProps>, getPaginationRef: ComputedRef<boolean | PaginationProps>) {
export function useColumns(
propsRef: ComputedRef<BasicTableProps>,
getPaginationRef: ComputedRef<boolean | PaginationProps>,
handleCustomSelectColumn: Fn,
) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
@ -104,6 +109,10 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>, getPagination
handleIndexColumn(propsRef, getPaginationRef, columns);
handleActionColumn(propsRef, columns);
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
handleCustomSelectColumn(columns);
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
if (!columns) {
return [];
}
@ -238,6 +247,10 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>, getPagination
if (ignoreAction) {
columns = columns.filter((item) => item.flag !== ACTION_COLUMN_FLAG);
}
// update-begin--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
// 过滤自定义选择列
columns = columns.filter((item) => item.key !== CUS_SEL_COLUMN_KEY);
// update-enb--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
if (sort) {
columns = sortFixedColumn(columns);

View File

@ -0,0 +1,396 @@
import type { BasicColumn } from '/@/components/Table';
import type { Ref, ComputedRef } from 'vue';
import type { BasicTableProps, PaginationProps, TableRowSelection } from '/@/components/Table';
import { computed, onUnmounted, ref, toRaw, unref, watchEffect } from 'vue';
import { omit } from 'lodash-es';
import { throttle } from 'lodash-es';
import { Checkbox } from 'ant-design-vue';
import { isFunction } from '/@/utils/is';
import { findNodeAll } from '/@/utils/helper/treeHelper';
import { ROW_KEY } from '/@/components/Table/src/const';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useMessage } from '/@/hooks/web/useMessage';
import { ModalFunc } from 'ant-design-vue/lib/modal/Modal';
// 自定义选择列的key
export const CUS_SEL_COLUMN_KEY = 'j-custom-selected-column';
/**
*
*/
export function useCustomSelection(
propsRef: ComputedRef<BasicTableProps>,
wrapRef: Ref<null | HTMLDivElement>,
getPaginationRef: ComputedRef<boolean | PaginationProps>,
tableData: Ref<Recordable[]>,
childrenColumnName: ComputedRef<string>
) {
const { createConfirm } = useMessage();
// 表格body元素
const bodyEl = ref<HTMLDivElement>();
// body元素高度
const bodyHeight = ref<number>(0);
// 表格tr高度
const rowHeight = ref<number>(0);
// body 滚动高度
const scrollTop = ref(0);
// 选择的key
const selectedKeys = ref<string[]>([]);
// 选择的行
const selectedRows = ref<Recordable[]>([]);
// 扁平化数据children数据也会放到一起
const flattedData = computed(() => {
return flattenData(tableData.value, childrenColumnName.value);
});
const getRowSelectionRef = computed((): TableRowSelection | null => {
const { rowSelection } = unref(propsRef);
if (!rowSelection) {
return null;
}
return {
preserveSelectedRowKeys: true,
// selectedRowKeys: unref(selectedKeys),
// onChange: (selectedRowKeys: string[]) => {
// setSelectedRowKeys(selectedRowKeys);
// },
...omit(rowSelection, ['onChange', 'selectedRowKeys']),
};
});
const getAutoCreateKey = computed(() => {
return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
});
// 列key字段
const getRowKey = computed(() => {
const { rowKey } = unref(propsRef);
return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
});
// 获取行的key字段数据
const getRecordKey = (record) => {
if (!getRowKey.value) {
return record[ROW_KEY];
} else if (isFunction(getRowKey.value)) {
return getRowKey.value(record);
} else {
return record[getRowKey.value];
}
};
// 分页配置
const getPagination = computed<PaginationProps>(() => {
return typeof getPaginationRef.value === 'boolean' ? {} : getPaginationRef.value;
});
// 当前页条目数量
const currentPageSize = computed(() => {
const { pageSize = 10, total = flattedData.value.length } = getPagination.value;
return pageSize > total ? total : pageSize;
});
// 选择列表头props
const selectHeaderProps = computed(() => {
return {
onSelectAll,
selectedLength: flattedData.value.filter((data) => selectedKeys.value.includes(getRecordKey(data))).length,
pageSize: currentPageSize.value,
};
});
// 监听滚动条事件
const onScrollTopChange = throttle((e) => (scrollTop.value = e?.target?.scrollTop), 150);
// 获取首行行高
watchEffect(() => {
if (bodyEl.value) {
bodyHeight.value = bodyEl.value.offsetHeight;
const el = bodyEl.value?.querySelector('tbody.ant-table-tbody tr.ant-table-row') as HTMLDivElement;
if (el) {
rowHeight.value = el.offsetHeight;
return;
}
}
rowHeight.value = 50;
// 这种写法是为了监听到 size 的变化
propsRef.value.size && void 0;
});
onMountedOrActivated(async () => {
bodyEl.value = await getTableBody(wrapRef.value!);
bodyEl.value.addEventListener('scroll', onScrollTopChange);
});
onUnmounted(() => {
if (bodyEl.value) {
bodyEl.value?.removeEventListener('scroll', onScrollTopChange);
}
});
// 选择全部
function onSelectAll(checked: boolean) {
// 取消全选
if (!checked) {
selectedKeys.value = [];
selectedRows.value = [];
emitChange();
return;
}
let modal: Nullable<ReturnType<ModalFunc>> = null;
// 全选
const checkAll = () => {
if (modal != null) {
modal.update({
content: '正在分批全选,请稍后……',
cancelButtonProps: { disabled: true },
});
}
let showCount = 0;
// 最小选中数量
let minSelect = 100;
const hidden: Recordable[] = [];
flattedData.value.forEach((item, index, array) => {
if (array.length > 120) {
if (showCount <= minSelect && recordIsShow(index, Math.max((minSelect - 10) / 2, 3))) {
showCount++;
updateSelected(item, checked);
} else {
hidden.push(item);
}
} else {
updateSelected(item, checked);
}
});
if (hidden.length > 0) {
return batchesSelectAll(hidden, checked, minSelect);
} else {
emitChange();
}
};
// 当数据量大于120条时全选会导致页面卡顿需进行慢速全选
if (flattedData.value.length > 120) {
modal = createConfirm({
title: '全选',
content: '当前数据量较大,全选可能会导致页面卡顿,确定要执行此操作吗?',
iconType: 'warning',
onOk: () => checkAll(),
});
} else {
checkAll();
}
}
// 分批全选
function batchesSelectAll(hidden: Recordable[], checked: boolean, minSelect: number) {
return new Promise<void>((resolve) => {
(function call() {
// 每隔半秒钟选择100条数据
setTimeout(() => {
const list = hidden.splice(0, minSelect);
if (list.length > 0) {
list.forEach((item) => {
updateSelected(item, checked);
});
call();
} else {
setTimeout(() => {
emitChange();
resolve();
}, 500);
}
}, 300);
})();
});
}
// 选中单个
function onSelect(record, checked) {
updateSelected(record, checked);
emitChange();
}
function updateSelected(record, checked) {
const recordKey = getRecordKey(record);
const index = selectedKeys.value.findIndex((key) => key === recordKey);
if (checked) {
if (index === -1) {
selectedKeys.value.push(recordKey);
selectedRows.value.push(record);
}
} else {
if (index !== -1) {
selectedKeys.value.splice(index, 1);
selectedRows.value.splice(index, 1);
}
}
}
// 调用用户自定义的onChange事件
function emitChange() {
const { rowSelection } = unref(propsRef);
if (rowSelection) {
const { onChange } = rowSelection;
if (onChange && isFunction(onChange)) {
setTimeout(() => {
onChange(selectedKeys.value, selectedRows.value);
}, 0);
}
}
}
// 用于判断是否是自定义选择列
function isCustomSelection(column: BasicColumn) {
return column.key === CUS_SEL_COLUMN_KEY;
}
/**
*
* @param index
* @param threshold 3
*/
function recordIsShow(index: number, threshold = 3) {
// 只有数据量大于50条时才会进行虚拟滚动
const isVirtual = flattedData.value.length > 50;
if (isVirtual) {
// 根据 scrollTop、bodyHeight、rowHeight 计算出当前行是否可视阈值前后3条
// flag1 = 判断当前行是否在可视区域上方3条
const flag1 = scrollTop.value - rowHeight.value * threshold < index * rowHeight.value;
// flag2 = 判断当前行是否在可视区域下方3条
const flag2 = index * rowHeight.value < scrollTop.value + bodyHeight.value + rowHeight.value * threshold;
// 全部条件满足时,才显示当前行
return flag1 && flag2;
}
return true;
}
// 自定义渲染Body
function bodyCustomRender({ record, index }) {
if (!recordIsShow(index)) {
return '';
}
const recordKey = getRecordKey(record);
// 获取用户自定义checkboxProps
const checkboxProps = ((getCheckboxProps) => {
if (typeof getCheckboxProps === 'function') {
try {
return getCheckboxProps(record) ?? {};
} catch (error) {
console.error(error);
}
}
return {};
})(propsRef.value.rowSelection?.getCheckboxProps);
return (
<Checkbox
{...checkboxProps}
data-index={index}
key={'j-select__' + recordKey}
checked={selectedKeys.value.includes(recordKey)}
onUpdate:checked={(checked) => onSelect(record, checked)}
/>
);
}
// 创建选择列
function handleCustomSelectColumn(columns: BasicColumn[]) {
if (!propsRef.value.rowSelection) {
return;
}
const isFixedLeft = columns.some((item) => item.fixed === 'left');
columns.unshift({
title: '选择列',
flag: 'CHECKBOX',
key: CUS_SEL_COLUMN_KEY,
width: 50,
minWidth: 50,
maxWidth: 50,
align: 'center',
...(isFixedLeft ? { fixed: 'left' } : {}),
customRender: bodyCustomRender,
});
}
// 清空所有选择
function clearSelectedRowKeys() {
onSelectAll(false);
}
// 设置选择的key
function setSelectedRowKeys(rowKeys: string[]) {
selectedKeys.value = rowKeys;
const allSelectedRows = findNodeAll(
toRaw(unref(flattedData)).concat(toRaw(unref(selectedRows))),
(item) => rowKeys.includes(getRecordKey(item)),
{
children: propsRef.value.childrenColumnName ?? 'children',
}
);
const trueSelectedRows: any[] = [];
rowKeys.forEach((key: string) => {
const found = allSelectedRows.find((item) => getRecordKey(item) === key);
found && trueSelectedRows.push(found);
});
selectedRows.value = trueSelectedRows;
}
function getSelectRows<T = Recordable>() {
return unref(selectedRows) as T[];
}
function getSelectRowKeys() {
return unref(selectedKeys);
}
function getRowSelection() {
return unref(getRowSelectionRef)!;
}
function deleteSelectRowByKey(key: string) {
const index = selectedKeys.value.findIndex((item) => item === key);
if (index !== -1) {
selectedKeys.value.splice(index, 1);
selectedRows.value.splice(index, 1);
}
}
return {
getRowSelection,
getRowSelectionRef,
getSelectRows,
getSelectRowKeys,
setSelectedRowKeys,
deleteSelectRowByKey,
selectHeaderProps,
isCustomSelection,
handleCustomSelectColumn,
clearSelectedRowKeys,
};
}
function getTableBody(wrap: HTMLDivElement) {
return new Promise<HTMLDivElement>((resolve) => {
(function fn() {
const bodyEl = wrap.querySelector('.ant-table-wrapper .ant-table-body') as HTMLDivElement;
if (bodyEl) {
resolve(bodyEl);
} else {
setTimeout(fn, 100);
}
})();
});
}
function flattenData<RecordType>(data: RecordType[] | undefined, childrenColumnName: string): RecordType[] {
let list: RecordType[] = [];
(data || []).forEach((record) => {
list.push(record);
if (record && typeof record === 'object' && childrenColumnName in record) {
list = [...list, ...flattenData<RecordType>((record as any)[childrenColumnName], childrenColumnName)];
}
});
return list;
}

View File

@ -19,6 +19,7 @@
const [register, { expandAll, collapseAll }] = useTable({
title: '树形表格',
isTreeTable: true,
expandIconColumnIndex: 1,
rowSelection: {
type: 'checkbox',
getCheckboxProps(record: Recordable) {

View File

@ -52,6 +52,7 @@
size: 'small',
pagination: false,
isTreeTable: true,
expandIconColumnIndex: 1,
striped: true,
useSearchForm: true,
showTableSetting: true,

View File

@ -49,6 +49,10 @@
formConfig: {
schemas: searchFormSchema,
},
actionColumn: {
width: 180,
},
showIndexColumn: true,
},
exportConfig: {
name: '职务列表',

View File

@ -11,7 +11,8 @@ export const columns: BasicColumn[] = [
{
title: '职务名称',
dataIndex: 'name',
width: 200,
align: 'left'
// width: 200,
},
// {
// title: '职务等级',