refactor: select bind (#13993)

* refactor: select bind

* refactor: simplify table selection state handling

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
This commit is contained in:
Yuki Guo
2026-10-08 17:11:28 +08:00
committed by GitHub
co-authored by ssongliu
parent 4954067736
commit 2701af9054
14 changed files with 112 additions and 22 deletions
@@ -11,6 +11,7 @@
<fu-table
v-if="currentViewMode === 'table'"
v-bind="$attrs"
:data="props.data"
ref="tableRef"
@select="handleSelect"
@selection-change="handleSelectionChange"
@@ -175,7 +176,7 @@ import { useResponsivePagination } from './useResponsivePagination';
import { useTableSelection } from './useTableSelection';
const slots = useSlots();
const attrs = useAttrs();
const { isMobile, openMenuTabs } = useGlobalStore();
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
defineOptions({ name: 'ComplexTable' });
export type DropdownProps = FuTableOperationButton;
@@ -184,6 +185,8 @@ type ViewMode = 'table' | 'card';
const props = defineProps({
header: String,
data: { type: Array as PropType<any[]>, default: () => [] },
selectionContext: { type: [String, Number, Array, Object, Function], default: undefined },
paginationConfig: {
type: Object,
required: false,
@@ -239,7 +242,7 @@ const selectionColumn = computed(() =>
columnNodes.value.find((column) => (column.props as Record<string, any> | null)?.type === 'selection'),
);
const tableData = computed(() => {
const data = attrs.data;
const data = props.data;
return Array.isArray(data) ? data : [];
});
const cardContentStyle = computed(() => {
@@ -295,7 +298,7 @@ const {
isDisabled: isRightButtonDisabled,
click: rightButtonClick,
} = useContextMenu(() => props.rightButtons);
const getTableData = () => (Array.isArray(attrs.data) ? attrs.data : []);
const getTableData = () => props.data || [];
const isRowSelectable = (row: any) => {
const selectable = (selectionColumn.value?.props as Record<string, any> | null)?.selectable;
return typeof selectable === 'function' ? selectable(row) : true;
@@ -304,7 +307,6 @@ const {
selectedRows,
isRowSelected,
clearSelects,
pruneSelection,
toggleSelection,
selectRow,
syncTableSelection,
@@ -313,7 +315,18 @@ const {
handleRowClick,
handleKeyDown: handleSelectionKeyDown,
handleKeyUp,
} = useTableSelection(tableRef, getTableData, (rows) => emit('update:selects', rows), isRowSelectable);
} = useTableSelection(
tableRef,
getTableData,
(rows) => emit('update:selects', rows),
isRowSelectable,
(row) => {
const key = attrs.rowKey ?? attrs['row-key'];
if (typeof key === 'function') return row == null ? undefined : key(row);
if (typeof key === 'string') return key.split('.').reduce((value, part) => value?.[part], row);
return row?.id;
},
);
const toggleCardSelection = (row: any, selected: boolean) => {
selectRow(row, selected);
};
@@ -446,7 +459,26 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
flush: 'post',
});
watch(tableData, pruneSelection);
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
flush: 'sync',
});
watch(
[
currentNode,
() =>
JSON.stringify(
typeof props.selectionContext === 'function' ? props.selectionContext() : props.selectionContext,
),
],
() => {
clearSelects();
if (props.paginationConfig) {
props.paginationConfig.currentPage = 1;
}
},
{ flush: 'sync' },
);
onBeforeUnmount(() => {
window.removeEventListener('resize', calcHeight);
@@ -1,4 +1,4 @@
import { shallowRef, ref, toRaw, type Ref } from 'vue';
import { shallowRef, ref, toRaw, watch, type Ref } from 'vue';
type TableRef = { refElTable?: any } | undefined;
@@ -7,6 +7,7 @@ export const useTableSelection = (
getTableData: () => any[],
onSelectionChange: (rows: any[]) => void,
isRowSelectable: (row: any) => boolean = () => true,
getRowKey: (row: any) => unknown = (row) => row?.id,
) => {
const selectedRows = shallowRef<any[]>([]);
const shiftPressed = ref(false);
@@ -15,8 +16,13 @@ export const useTableSelection = (
let isSyncingTableSelection = false;
let skipNextSelectionChange = false;
// Element Plus can return reactive proxies for rows supplied as plain objects.
const sameRow = (left: any, right: any) => toRaw(left) === toRaw(right);
let isRefreshingData = false;
const sameRow = (left: any, right: any) => {
if (left == null || right == null) return left === right;
const leftKey = getRowKey(left);
const rightKey = getRowKey(right);
return leftKey != null && rightKey != null ? leftKey === rightKey : toRaw(left) === toRaw(right);
};
const hasRow = (rows: any[], row: any) => rows.some((item) => sameRow(item, row));
const isRowSelected = (row: any) => hasRow(selectedRows.value, row);
const getTable = () => tableRef.value?.refElTable;
@@ -39,8 +45,12 @@ export const useTableSelection = (
const tableData = getTableData();
const nextRows = selectedRows.value.filter((row) => hasRow(tableData, row) && isRowSelectable(row));
const currentRows = table.getSelectionRows();
currentRows.filter((row) => !hasRow(nextRows, row)).forEach((row) => table.toggleRowSelection(row, false));
nextRows.filter((row) => !hasRow(currentRows, row)).forEach((row) => table.toggleRowSelection(row, true));
currentRows
.filter((row) => !nextRows.some((item) => toRaw(item) === toRaw(row)))
.forEach((row) => table.toggleRowSelection(row, false));
nextRows
.filter((row) => !currentRows.some((item) => toRaw(item) === toRaw(row)))
.forEach((row) => table.toggleRowSelection(row, true));
} finally {
isSyncingTableSelection = false;
}
@@ -73,7 +83,7 @@ export const useTableSelection = (
return true;
};
const handleSelectionChange = (rows: any[]) => {
if (isSyncingTableSelection) {
if (isSyncingTableSelection || isRefreshingData) {
return;
}
if (skipNextSelectionChange) {
@@ -104,16 +114,35 @@ export const useTableSelection = (
rangeBaseRows.value = [];
};
const pruneSelection = () => {
const nextRows = selectedRows.value.filter((row) => hasRow(getTableData(), row));
if (nextRows.length !== selectedRows.value.length) {
const data = getTableData();
const nextRows = data.filter((row) => hasRow(selectedRows.value, row) && isRowSelectable(row));
if (nextRows.length !== selectedRows.value.length || nextRows.some((row, i) => row !== selectedRows.value[i])) {
setSelectedRows(nextRows);
}
if (lastSelectedRow.value && !hasRow(nextRows, lastSelectedRow.value)) {
lastSelectedRow.value = null;
rangeBaseRows.value = [];
}
syncTableSelection();
lastSelectedRow.value = nextRows.find((row) => sameRow(row, lastSelectedRow.value)) ?? null;
rangeBaseRows.value = lastSelectedRow.value ? nextRows.filter((row) => hasRow(rangeBaseRows.value, row)) : [];
};
// Guard before the child table reacts to replacement data and emits an empty selection.
watch(
getTableData,
() => {
isRefreshingData = true;
skipNextSelectionChange = false;
pruneSelection();
},
{ flush: 'sync' },
);
watch(
getTableData,
() => {
try {
syncTableSelection();
} finally {
isRefreshingData = false;
}
},
{ flush: 'post' },
);
const toggleSelection = () => {
const selectableRows = getTableData().filter(isRowSelectable);
const allSelected = selectableRows.length > 0 && selectableRows.every(isRowSelected);
@@ -155,7 +184,6 @@ export const useTableSelection = (
selectedRows,
isRowSelected,
clearSelects,
pruneSelection,
toggleSelection,
selectRow,
syncTableSelection,
@@ -65,6 +65,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName]"
row-key="id"
:pagination-config="paginationConfig"
v-model:selects="selects"
:class="{ mask: maskShow }"
@@ -56,6 +56,7 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName, activeTag, includeAppStore, props.filters]"
:pagination-config="paginationConfig"
v-model:view-mode="viewMode"
v-model:selects="selects"
@@ -26,6 +26,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName]"
row-key="id"
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@@ -38,6 +38,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName]"
row-key="id"
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@@ -26,6 +26,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName]"
row-key="name"
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@@ -60,6 +60,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName, searchGroupID]"
row-key="id"
:pagination-config="paginationConfig"
:default-sort="
paginationConfig.order !== 'null'
@@ -60,6 +60,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchInfo, group]"
row-key="id"
v-model:selects="selects"
:pagination-config="paginationConfig"
:data="data"
@@ -57,6 +57,18 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName, fireName]"
:row-key="
(row) =>
JSON.stringify([
row.family,
row.protocol,
row.port,
row.targetIP,
row.targetPort,
row.interface,
])
"
:pagination-config="paginationConfig"
v-model:selects="selects"
@search="search"
@@ -185,6 +185,7 @@
<template #main>
<div v-loading="loading">
<ComplexTable
:selection-context="() => [searchName, selectedRuleFilters, visibleIptablesChains]"
v-model:selects="selects"
:pagination-config="paginationConfig"
:data="allRows"
@@ -661,7 +662,6 @@ const loadRules = async (refreshUsage: boolean) => {
paginationConfig.total = total;
inventoryTotal.value = response.data.allTotal || 0;
managedTotal.value = response.data.managedTotal || 0;
selects.value = [];
} finally {
if (requestID === searchRequestID) loading.value = false;
}
@@ -19,6 +19,8 @@
</template>
<template #main>
<ComplexTable
:selection-context="() => [searchName]"
row-key="id"
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@@ -49,6 +49,8 @@
</el-card>
<template #main v-if="clamStatus.isExist">
<ComplexTable
:selection-context="() => [searchName]"
row-key="id"
:class="{ mask: !clamStatus.isRunning }"
v-if="!isSettingShow"
:pagination-config="paginationConfig"
+2 -1
View File
@@ -66,6 +66,8 @@
</template>
<template v-if="!openNginxConfig" #main>
<ComplexTable
:selection-context="() => [req.name, req.type, req.websiteGroupId]"
row-key="id"
:pagination-config="paginationConfig"
:default-sort="tableSort.order ? tableSort : undefined"
v-model:view-mode="viewMode"
@@ -498,7 +500,6 @@ const search = async () => {
req.pageSize = paginationConfig.pageSize;
loading.value = true;
data.value = [];
await searchWebsites(req)
.then((res) => {
data.value = res.data.items;