jvxetable 使用编辑的时候卡顿问题 #8695

This commit is contained in:
JEECG
2026-01-30 14:27:37 +08:00
parent 22666ade0c
commit 80b8573232
6 changed files with 279 additions and 186 deletions

View File

@ -1,4 +1,4 @@
import { defineComponent, h, nextTick, ref, useSlots } from 'vue';
import { defineComponent, h, nextTick, useSlots, shallowRef, markRaw } from 'vue';
import { vxeEmits, vxeProps } from './vxe.data';
import { useData, useRefs, useResolveComponent as rc } from './hooks/useData';
import { useColumns } from './hooks/useColumns';
@ -17,7 +17,10 @@ export default defineComponent({
props: vxeProps(),
emits: [...vxeEmits],
setup(props: JVxeTableProps, context) {
const instanceRef = ref();
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
// 使用 shallowRef 优化大型对象响应式性能
const instanceRef = shallowRef();
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
const refs = useRefs();
const slots = useSlots();
const data = useData(props);
@ -33,6 +36,9 @@ export default defineComponent({
const finallyProps = useFinallyProps(props, data, methods);
// 渲染子组件
const renderComponents = useRenderComponents(props, data, methods, slots);
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
markRaw(renderComponents);
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
return {
instanceRef,
...refs,

View File

@ -1,7 +1,7 @@
import type { JVxeColumn, JVxeDataProps, JVxeTableProps } from '../types';
import { computed, nextTick, toRaw } from 'vue';
import { computed, nextTick, toRaw, shallowRef, markRaw } from 'vue';
import { isArray, isEmpty, isPromise } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, debounce } from 'lodash-es';
import { JVxeTypePrefix, JVxeTypes } from '../types/JVxeTypes';
import { initDictOptions } from '/@/utils/dict';
import { pushIfNotExist } from '/@/utils/common/compUtils';
@ -24,97 +24,129 @@ export interface HandleArgs {
}
export function useColumns(props: JVxeTableProps, data: JVxeDataProps, methods: JVxeTableMethods, slots) {
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
// 使用 shallowRef 优化列数据响应式性能
const columnsCache = shallowRef<JVxeColumn[]>([]);
let lastColumnsHash = '';
// 计算列哈希值,用于缓存判断
const getColumnsHash = (columns: JVxeColumn[]) => {
return JSON.stringify(columns.map(col => ({ key: col.key, type: col.type, title: col.title })));
};
// 防抖处理列计算,避免频繁重新计算
const debouncedComputeColumns = debounce(() => {
if (!isArray(props.columns)) {
columnsCache.value = [];
return;
}
const currentHash = getColumnsHash(props.columns);
if (currentHash === lastColumnsHash) {
return; // 列没有变化,直接返回缓存
}
lastColumnsHash = currentHash;
const columns: JVxeColumn[] = [];
// handle 方法参数
const args: HandleArgs = { props, slots, data, methods, columns };
let seqColumn, selectionColumn, expandColumn, dragSortColumn;
const handleColumn = (column: JVxeColumn, container: JVxeColumn[]) => {
// 排除未授权的列 1 = 显示/隐藏; 2 = 禁用
let auth = methods.getColAuth(column.key);
if (auth?.type == '1' && !auth.isAuth) {
return;
} else if (auth?.type == '2' && !auth.isAuth) {
column.disabled = true;
}
// type 不填,默认为 normal
if (column.type == null || isEmpty(column.type)) {
column.type = JVxeTypes.normal;
}
let col: JVxeColumn = cloneDeep(column);
// 处理隐藏列
if (col.type === JVxeTypes.hidden) {
return handleInnerColumn(args, col, handleHiddenColumn);
}
// 处理子级列
// 判断是否是分组列,如果当前是父级,则无需处理 render
if (Array.isArray(col.children) && col.children.length > 0) {
const children: JVxeColumn[] = [];
col.children.forEach((child: JVxeColumn) => handleColumn(child, children));
col.children = children;
container.push(col);
return;
}
// 组件未注册,自动设置为 normal
if (!isRegistered(col.type)) {
col.type = JVxeTypes.normal;
}
args.enhanced = getEnhanced(col.type);
args.col = col;
args.renderOptions = {
bordered: props.bordered,
disabled: props.disabled,
scrolling: data.scrolling,
isDisabledRow: methods.isDisabledRow,
listeners: {
trigger: (name, event) => methods.trigger(name, event),
valueChange: (event) => methods.trigger('valueChange', event),
/** 重新排序行 */
rowResort: (event) => {
methods.doSort(event.oldIndex, event.newIndex);
methods.trigger('dragged', event);
},
/** 在当前行下面插入一行 */
rowInsertDown: (rowIndex) => methods.insertRows({}, rowIndex + 1),
},
};
if (col.type === JVxeTypes.rowNumber) {
seqColumn = col;
container.push(col);
} else if (col.type === JVxeTypes.rowRadio || col.type === JVxeTypes.rowCheckbox) {
selectionColumn = col;
container.push(col);
} else if (col.type === JVxeTypes.rowExpand) {
expandColumn = col;
container.push(col);
} else if (col.type === JVxeTypes.rowDragSort) {
dragSortColumn = col;
container.push(col);
} else {
col.params = column;
args.columns = container;
handlerCol(args);
}
}
props.columns.forEach((column: JVxeColumn) => handleColumn(column, columns));
handleInnerColumn(args, seqColumn, handleSeqColumn);
handleInnerColumn(args, selectionColumn, handleSelectionColumn);
handleInnerColumn(args, expandColumn, handleExpandColumn);
handleInnerColumn(args, dragSortColumn, handleDragSortColumn, true);
// update-begin--author:liaozhiyang---date:2024-05-30---for【TV360X-371】不可编辑组件必填缺少*号
customComponentAddStar(columns);
// update-end--author:liaozhiyang---date:2024-05-30---for【TV360X-371】不可编辑组件必填缺少*号
// 标记为原始对象,避免深度响应式
columnsCache.value = markRaw(columns);
}, 16); // 16ms 防抖,约等于一帧的时间
data.vxeColumns = computed(() => {
// linkageConfig变化时也需要执行
// 【issues/7812】linkageConfig改变了vxetable没更新
// linkageConfig变化时也需要执行
const linkageConfig = toRaw(props.linkageConfig);
if (linkageConfig) {
// console.log(linkageConfig);
}
let columns: JVxeColumn[] = [];
if (isArray(props.columns)) {
// handle 方法参数
const args: HandleArgs = { props, slots, data, methods, columns };
let seqColumn, selectionColumn, expandColumn, dragSortColumn;
const handleColumn = (column: JVxeColumn, container: JVxeColumn[]) => {
// 排除未授权的列 1 = 显示/隐藏; 2 = 禁用
let auth = methods.getColAuth(column.key);
if (auth?.type == '1' && !auth.isAuth) {
return;
} else if (auth?.type == '2' && !auth.isAuth) {
column.disabled = true;
}
// type 不填,默认为 normal
if (column.type == null || isEmpty(column.type)) {
column.type = JVxeTypes.normal;
}
let col: JVxeColumn = cloneDeep(column);
// 处理隐藏列
if (col.type === JVxeTypes.hidden) {
return handleInnerColumn(args, col, handleHiddenColumn);
}
// 处理子级列
// 判断是否是分组列,如果当前是父级,则无需处理 render
if (Array.isArray(col.children) && col.children.length > 0) {
const children: JVxeColumn[] = [];
col.children.forEach((child: JVxeColumn) => handleColumn(child, children));
col.children = children;
container.push(col);
return;
}
// 组件未注册,自动设置为 normal
if (!isRegistered(col.type)) {
col.type = JVxeTypes.normal;
}
args.enhanced = getEnhanced(col.type);
args.col = col;
args.renderOptions = {
bordered: props.bordered,
disabled: props.disabled,
scrolling: data.scrolling,
isDisabledRow: methods.isDisabledRow,
listeners: {
trigger: (name, event) => methods.trigger(name, event),
valueChange: (event) => methods.trigger('valueChange', event),
/** 重新排序行 */
rowResort: (event) => {
methods.doSort(event.oldIndex, event.newIndex);
methods.trigger('dragged', event);
},
/** 在当前行下面插入一行 */
rowInsertDown: (rowIndex) => methods.insertRows({}, rowIndex + 1),
},
};
if (col.type === JVxeTypes.rowNumber) {
seqColumn = col;
container.push(col);
} else if (col.type === JVxeTypes.rowRadio || col.type === JVxeTypes.rowCheckbox) {
selectionColumn = col;
container.push(col);
} else if (col.type === JVxeTypes.rowExpand) {
expandColumn = col;
container.push(col);
} else if (col.type === JVxeTypes.rowDragSort) {
dragSortColumn = col;
container.push(col);
} else {
col.params = column;
args.columns = container;
handlerCol(args);
}
}
props.columns.forEach((column: JVxeColumn) => handleColumn(column, columns));
handleInnerColumn(args, seqColumn, handleSeqColumn);
handleInnerColumn(args, selectionColumn, handleSelectionColumn);
handleInnerColumn(args, expandColumn, handleExpandColumn);
handleInnerColumn(args, dragSortColumn, handleDragSortColumn, true);
// update-begin--author:liaozhiyang---date:2024-05-30---for【TV360X-371】不可编辑组件必填缺少*号
customComponentAddStar(columns);
}
return columns;
// 触发防抖计算
debouncedComputeColumns();
return columnsCache.value;
});
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
}
/**

View File

@ -1,4 +1,4 @@
import { ref, reactive, provide, resolveComponent } from 'vue';
import { ref, reactive, provide, resolveComponent, shallowRef, markRaw } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { JVxeDataProps, JVxeRefs, JVxeTableProps } from '../types';
import { VxeGridInstance } from 'vxe-table';
@ -7,90 +7,108 @@ import { randomString } from '/@/utils/common/compUtils';
export function useData(props: JVxeTableProps): JVxeDataProps {
const { prefixCls } = useDesign('j-vxe-table');
provide('prefixCls', prefixCls);
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
// 使用 shallowRef 优化大数据源性能
const vxeDataSource = shallowRef([]);
// 标记静态配置对象,避免深度响应式
const defaultVxeProps = markRaw({
// update-begin--author:liaozhiyang---date:20240607---for【TV360X-327】vxetable警告
// rowId: props.rowKey,
rowConfig: {
keyField: props.rowKey,
// 高亮hover的行
isHover: true,
},
// update-end--author:liaozhiyang---date:20240607---for【TV360X-327】vxetable警告
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
// 溢出隐藏并显示tooltip
showOverflow: "title",
// 表头溢出隐藏并显示tooltip
showHeaderOverflow: "title",
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
showFooterOverflow: true,
// 可编辑配置
editConfig: {
trigger: 'click',
mode: 'cell',
// update-begin--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
//activeMethod: () => !props.disabled,
beforeEditMethod: () => !props.disabled,
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
},
expandConfig: {
iconClose: 'vxe-icon-arrow-right',
iconOpen: 'vxe-icon-arrow-down',
...props.expandConfig,
},
// 虚拟滚动配置y轴大于xx条数据时启用虚拟滚动
scrollY: {
gt: 30,
},
scrollX: {
gt: 20,
// 暂时关闭左右虚拟滚动
enabled: false,
},
radioConfig: {
// 保留勾选状态
reserve: true,
highlight: true,
},
checkboxConfig: {
// 保留勾选状态
reserve: true,
highlight: true,
},
mouseConfig: { selected: false },
keyboardConfig: {
// 删除键功能
isDel: false,
// Esc键关闭编辑功能
isEsc: true,
// Tab 键功能
isTab: true,
// 任意键进入编辑(功能键除外)
isEdit: true,
// 方向键功能
isArrow: true,
// 回车键功能
isEnter: true,
// 如果功能被支持,用于 column.type=checkbox|radio开启空格键切换复选框或单选框状态功能
isChecked: true,
},
});
// 使用 shallowRef 优化选中行性能
const selectedRows = shallowRef<any[]>([]);
const selectedRowIds = shallowRef<string[]>([]);
const authsMap = shallowRef(null);
return {
prefixCls: prefixCls,
caseId: `j-vxe-${randomString(8)}`,
vxeDataSource: ref([]),
vxeDataSource,
scroll: reactive({ top: 0, left: 0 }),
scrolling: ref(false),
defaultVxeProps: reactive({
// rowId: props.rowKey,
rowConfig: {
keyField: props.rowKey,
// 高亮hover的行
isHover: true,
},
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
// 溢出隐藏并显示tooltip
showOverflow: "title",
// 表头溢出隐藏并显示tooltip
showHeaderOverflow: "title",
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
showFooterOverflow: true,
// 可编辑配置
editConfig: {
trigger: 'click',
mode: 'cell',
//activeMethod: () => !props.disabled,
beforeEditMethod: () => !props.disabled,
},
expandConfig: {
iconClose: 'vxe-icon-arrow-right',
iconOpen: 'vxe-icon-arrow-down',
...props.expandConfig,
},
// 虚拟滚动配置y轴大于xx条数据时启用虚拟滚动
scrollY: {
gt: 30,
},
scrollX: {
gt: 20,
// 暂时关闭左右虚拟滚动
enabled: false,
},
radioConfig: {
// 保留勾选状态
reserve: true,
highlight: true,
},
checkboxConfig: {
// 保留勾选状态
reserve: true,
highlight: true,
},
mouseConfig: { selected: false },
keyboardConfig: {
// 删除键功能
isDel: false,
// Esc键关闭编辑功能
isEsc: true,
// Tab 键功能
isTab: true,
// 任意键进入编辑(功能键除外)
isEdit: true,
// 方向键功能
isArrow: true,
// 回车键功能
isEnter: true,
// 如果功能被支持,用于 column.type=checkbox|radio开启空格键切换复选框或单选框状态功能
isChecked: true,
},
}),
selectedRows: ref<any[]>([]),
selectedRowIds: ref<string[]>([]),
defaultVxeProps,
selectedRows,
selectedRowIds,
disabledRowIds: [],
statistics: reactive({
has: false,
sum: [],
average: [],
}),
authsMap: ref(null),
authsMap,
innerEditRules: {},
innerLinkageConfig: new Map<string, any>(),
reloadEffectRowKeysMap: reactive({}),
};
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
}
export function useRefs(): JVxeRefs {

View File

@ -1,36 +1,55 @@
import { nextTick, watch } from 'vue';
import { JVxeDataProps, JVxeRefs, JVxeTableMethods } from '../types';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, debounce } from 'lodash-es';
export function useDataSource(props, data: JVxeDataProps, methods: JVxeTableMethods, refs: JVxeRefs) {
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
// 使用浅拷贝优化大数据量处理
const processDataSource = debounce(async (newDataSource) => {
if (!Array.isArray(newDataSource)) {
data.vxeDataSource.value = [];
return;
}
data.vxeDataSource.value = cloneDeep(newDataSource);
// 批量处理禁用行,减少循环次数
const disabledRowIds: string[] = [];
data.vxeDataSource.value.forEach((row, rowIndex) => {
// 判断是否是禁用行
if (methods.isDisabledRow(row, rowIndex)) {
disabledRowIds.push(row.id);
}
// 处理联动回显数据
methods.handleLinkageBackData(row);
});
data.disabledRowIds = disabledRowIds;
const grid = await waitRef(refs.gridRef);
if (grid?.value) methods.recalcSortNumber();
}, 50); // 50ms 防抖,避免频繁更新
watch(
() => props.dataSource,
async () => {
data.disabledRowIds = [];
data.vxeDataSource.value = cloneDeep(props.dataSource);
data.vxeDataSource.value.forEach((row, rowIndex) => {
// 判断是否是禁用行
if (methods.isDisabledRow(row, rowIndex)) {
data.disabledRowIds.push(row.id);
}
// 处理联动回显数据
methods.handleLinkageBackData(row);
});
await waitRef(refs.gridRef);
methods.recalcSortNumber();
(newDataSource) => {
processDataSource(newDataSource);
},
{ immediate: true }
);
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
}
function waitRef($ref) {
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
function waitRef($ref, maxTries = 10) {
return new Promise<any>((resolve) => {
let tries = 0;
(function next() {
if ($ref.value) {
resolve($ref);
} else if (tries >= maxTries) {
resolve(null);
} else {
tries++;
nextTick(() => next());
}
})();
});
}
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿

View File

@ -1,5 +1,5 @@
import { unref, computed, ref, watch, nextTick } from 'vue';
import { merge, debounce } from 'lodash-es';
import { unref, computed, ref, watch, nextTick, shallowRef } from 'vue';
import { merge, debounce, throttle } from 'lodash-es';
import { isArray } from '/@/utils/is';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { useKeyboardEdit } from '../hooks/useKeyboardEdit';
@ -11,12 +11,19 @@ export function useFinallyProps(props: JVxeTableProps, data: JVxeDataProps, meth
const { keyboardEditConfig } = useKeyboardEdit(props);
// vxe 最终 editRules
const vxeEditRules = computed(() => merge({}, props.editRules, data.innerEditRules));
// ==================== 性能优化 - 开始 ====================
// 使用节流优化高频事件
const throttledScroll = throttle(methods.handleVxeScroll, 16); // 约60fps
const throttledCellClick = throttle(methods.handleCellClick, 100);
// vxe 最终 events
const vxeEvents = computed(() => {
let listeners = { ...unref(attrs) };
let events = {
onScroll: methods.handleVxeScroll,
onCellClick: methods.handleCellClick,
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
onScroll: throttledScroll,
onCellClick: throttledCellClick,
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
onEditClosed: methods.handleEditClosed,
onEditActived: methods.handleEditActived,
onRadioChange: methods.handleVxeRadioChange,
@ -111,14 +118,20 @@ export function useFinallyProps(props: JVxeTableProps, data: JVxeDataProps, meth
);
});
// 代码逻辑说明: 【issues/8593】修复列改变后内容不刷新
const vxeColumnsRef = ref(data.vxeColumns!.value || [])
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
// 使用 shallowRef 优化列更新性能
const vxeColumnsRef = shallowRef([])
const watchColumnsDebounce = debounce(async () => {
vxeColumnsRef.value = []
await nextTick()
vxeColumnsRef.value = data.vxeColumns!.value
}, 50)
watch(data.vxeColumns!, watchColumnsDebounce)
vxeColumnsRef.value = data.vxeColumns?.value || []
}, 16) // 减少防抖时间到16ms提高响应速度
// 安全地监听列变化
if (data.vxeColumns) {
watch(data.vxeColumns, watchColumnsDebounce)
}
// update-end--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
const vxeProps = computed(() => {
return {

View File

@ -4,7 +4,7 @@ import { simpleDebounce } from '/@/utils/common/compUtils';
import { JVxeDataProps, JVxeRefs, JVxeTableProps, JVxeTypes } from '../types';
import { getEnhanced } from '../utils/enhancedUtils';
import { VxeTableInstance, VxeTablePrivateMethods } from 'vxe-table';
import { cloneDeep } from 'lodash-es';
import { cloneDeep, throttle } from 'lodash-es';
import { isArray, isEmpty, isNull, isString } from '/@/utils/is';
import { useLinkage } from './useLinkage';
import { useWebSocket } from './useWebSocket';
@ -67,7 +67,7 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
};
/** 监听vxe滚动条位置 */
function handleVxeScroll(event) {
const throttledScroll = throttle((event) => {
let { scroll } = data;
// 记录滚动条的位置
@ -77,7 +77,12 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
refs.subPopoverRef.value?.close();
data.scrolling.value = true;
closeScrolling();
}, 16);
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
function handleVxeScroll(event) {
throttledScroll(event);
}
// update-begin--author:liaozhiyang---date:20260130---for:【QQYUN-14177】online配置界面字段配置卡顿
// 当手动勾选单选时触发的事件
function handleVxeRadioChange(event) {