From 2701af90544f7f1c94322c0e80b7bdf4e43201dc Mon Sep 17 00:00:00 2001 From: Yuki Guo Date: Thu, 8 Oct 2026 17:11:28 +0800 Subject: [PATCH] refactor: select bind (#13993) * refactor: select bind * refactor: simplify table selection state handling --------- Co-authored-by: ssongliu --- .../components/table/complex/ComplexTable.vue | 44 +++++++++++++-- .../table/complex/useTableSelection.ts | 56 ++++++++++++++----- frontend/src/views/ai/model/ollama/index.vue | 2 + .../src/views/container/container/index.vue | 1 + .../src/views/container/network/index.vue | 2 + .../src/views/container/template/index.vue | 2 + frontend/src/views/container/volume/index.vue | 2 + frontend/src/views/cronjob/cronjob/index.vue | 2 + frontend/src/views/cronjob/library/index.vue | 2 + .../src/views/host/firewall/forward/index.vue | 12 ++++ .../src/views/host/firewall/rule/index.vue | 2 +- frontend/src/views/setting/snapshot/index.vue | 2 + frontend/src/views/toolbox/clam/index.vue | 2 + frontend/src/views/website/website/index.vue | 3 +- 14 files changed, 112 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/table/complex/ComplexTable.vue b/frontend/src/components/table/complex/ComplexTable.vue index 7455a6632..8b6865590 100644 --- a/frontend/src/components/table/complex/ComplexTable.vue +++ b/frontend/src/components/table/complex/ComplexTable.vue @@ -11,6 +11,7 @@ , 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 | 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 | 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); diff --git a/frontend/src/components/table/complex/useTableSelection.ts b/frontend/src/components/table/complex/useTableSelection.ts index 807c4c049..cb37fa1bd 100644 --- a/frontend/src/components/table/complex/useTableSelection.ts +++ b/frontend/src/components/table/complex/useTableSelection.ts @@ -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([]); 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, diff --git a/frontend/src/views/ai/model/ollama/index.vue b/frontend/src/views/ai/model/ollama/index.vue index acb87ab43..90b14c08a 100644 --- a/frontend/src/views/ai/model/ollama/index.vue +++ b/frontend/src/views/ai/model/ollama/index.vue @@ -65,6 +65,8 @@