diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 9b556995c4..8474ee231f 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -170,6 +170,7 @@ import { buildDataGridColumnLookupItems, dataGridColumnCommentFor, filterDataGri import { uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; import { dataGridColumnLayoutScopeKey, TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, tableDataGridColumnOrderScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage"; import { summarizeSelection } from "@/lib/dataGrid/gridSelection"; +import { dataGridFrameCoversRow, dataGridSelectionEdgeMask, dataGridSelectionFrameKindAtCell, dataGridSelectionUsesOuterFrame, resolveDataGridSelectionFrames } from "@/lib/dataGrid/dataGridSelectionFrames"; import { createDataGridCellContextMenuItems, createDataGridColumnContextMenuItems, @@ -2383,6 +2384,7 @@ function onScrollerScroll(e: Event) { clampGridScrollerBounds(target); updateDomGridVisibleItemsDuringScroll(target); syncHeaderScroll(e); + if (domGridScrollTop.value !== target.scrollTop) domGridScrollTop.value = target.scrollTop; recordScrollPosition({ top: target.scrollTop, left: target.scrollLeft }); maybeCheckInfiniteScroll(target); } else { @@ -3820,8 +3822,10 @@ const selection = useDataGridSelection({ const { isSelectingAll, + isSelectingCells, selectedRange, selectedCells, + selectedCellKeys, selectedCellMatrix, selectedCellCount, hasCellSelection, @@ -3856,8 +3860,21 @@ const multiRowCount = computed(() => { return 1; }); -const selectionSummary = computed(() => (hasCellSelection.value ? summarizeSelection(selectedCells.value) : null)); +// 框选拖拽中不物化 selectedCells / 不做数值汇总(大选区下这是 DOM 卡顿主因) +const selectionSummary = computed(() => { + if (!hasCellSelection.value) return null; + if (isSelectingCells.value) { + return { + cellCount: selectedCellCount.value, + rowCount: multiRowCount.value, + numericCount: 0, + sum: 0, + }; + } + return summarizeSelection(selectedCells.value); +}); const selectionSummarySumText = computed(() => { + if (isSelectingCells.value) return "…"; const summary = selectionSummary.value; if (!summary) return "0"; const sum = Object.is(summary.sum, -0) ? 0 : summary.sum; @@ -3866,9 +3883,90 @@ const selectionSummarySumText = computed(() => { const isMultiRow = computed(() => multiRowCount.value > 1); +// Navicat 风格选区形态:单个单元格细边框,多格范围浅色填充 + 一圈细外框; +// 离散点选(Ctrl)退回逐格描边 +const selectionFramesData = computed(() => + resolveDataGridSelectionFrames({ + sparseCellCount: selectedCellKeys.value.size, + hasColumnSelection: hasColumnSelection.value, + selectedColumnIndexes: selectedColumnIndexes.value, + selectedRange: selectedRange.value, + rowCount: displayRowCount.value, + }), +); +const selectionUsesOuterFrame = computed(() => dataGridSelectionUsesOuterFrame(selectionFramesData.value.frames)); +// DOM 框选拖拽:overlay 只画外框(与 canvas strokeRect 一致),填充仍走格子 cell-selected +const domSelectionDragOverlayActive = computed(() => { + if (!isSelectingCells.value || dataGridRenderMode.value === "canvas") return false; + const range = selectedRange.value; + if (!range) return false; + return range.startRow !== range.endRow || range.startCol !== range.endCol; +}); +const domGridScrollTop = ref(0); + +function selectionFrameKindForCell(rowIndex: number, visibleColIdx: number): "single" | "range" | null { + const frames = selectionFramesData.value.frames; + if (frames.length === 0) return null; + // 框选热路径:已确定是多格外框时,选中格必为 range,避免再扫 frames + if (selectionUsesOuterFrame.value && frames.length === 1) { + const frame = frames[0]!; + if (rowIndex < frame.startRow || rowIndex > frame.endRow || visibleColIdx < frame.startCol || visibleColIdx > frame.endCol) return null; + return "range"; + } + return dataGridSelectionFrameKindAtCell(frames, rowIndex, visibleColIdx); +} + +// 行号的选区覆盖指示(Navicat 风格),但行选中(--selected)和行状态色(新增/编辑/删除)优先, +// 与 canvas 渲染器的 rowNumberFill 优先级顺序保持一致 +function rowNumberShowsSelectionTint(item: RowItem): boolean { + if (isRowSelected(item.id)) return false; + if (item.status !== "clean") return false; + const frames = selectionFramesData.value.frames; + return frames.length > 0 && dataGridFrameCoversRow(frames, item.displayIndex); +} + +// 多格范围的外框:松手后用单元格 bitmask class;拖拽中改由 overlay 画一圈外框(与 canvas 一致,文字仍在格子里) +function selectionFrameEdgeClass(rowIndex: number, visibleColIdx: number): string { + if (domSelectionDragOverlayActive.value || !selectionUsesOuterFrame.value) return ""; + const mask = dataGridSelectionEdgeMask(selectionFramesData.value.frames, rowIndex, visibleColIdx); + return mask === 0 ? "" : `cell-sel-frame-${mask}`; +} + +const domSelectionDragOverlayStyle = computed((): CSSProperties | undefined => { + if (!domSelectionDragOverlayActive.value) return undefined; + const range = selectedRange.value; + if (!range) return undefined; + const startCol = range.startCol; + const endCol = range.endCol; + const startFrozen = startCol < frozenColumnCount.value; + const endFrozen = endCol < frozenColumnCount.value; + const left = rowNumberWidth.value + (renderedColumnOffsets.value[startCol] ?? 0) - (startFrozen ? 0 : gridHorizontalScrollLeft.value); + const right = rowNumberWidth.value + (renderedColumnOffsets.value[endCol] ?? 0) + (renderedColumnWidths.value[endCol] ?? 0) - (endFrozen ? 0 : gridHorizontalScrollLeft.value); + const top = range.startRow * CANVAS_DATA_GRID_ROW_HEIGHT - domGridScrollTop.value; + const bottom = (range.endRow + 1) * CANVAS_DATA_GRID_ROW_HEIGHT - domGridScrollTop.value; + const viewportWidth = gridViewportWidth.value; + const scroller = gridScrollerElement(); + const viewportHeight = scroller?.clientHeight ?? 0; + const clippedLeft = Math.max(rowNumberWidth.value, left); + const clippedTop = Math.max(0, top); + const clippedRight = viewportWidth > 0 ? Math.min(viewportWidth, right) : right; + const clippedBottom = viewportHeight > 0 ? Math.min(viewportHeight, bottom) : bottom; + const width = clippedRight - clippedLeft; + const height = clippedBottom - clippedTop; + if (width < 1 || height < 1) return undefined; + return { + left: `${clippedLeft}px`, + top: `${clippedTop}px`, + width: `${width}px`, + height: `${height}px`, + }; +}); + function onCellMouseenter(rowIndex: number, visibleColIdx: number, actualColIdx: number) { - quickDownloadMenuCell.value = retainBinaryCellDownloadMenuForHover(quickDownloadMenuCell.value, { rowIndex, col: actualColIdx }); - if (!isScrolling.value) hoveredDetailCell.value = { rowIndex, col: actualColIdx }; + if (!isSelectingCells.value) { + quickDownloadMenuCell.value = retainBinaryCellDownloadMenuForHover(quickDownloadMenuCell.value, { rowIndex, col: actualColIdx }); + if (!isScrolling.value) hoveredDetailCell.value = { rowIndex, col: actualColIdx }; + } extendCellSelection(rowIndex, visibleColIdx); } @@ -5461,6 +5559,7 @@ function drawCanvasGrid() { isRowActive, rowCellsUseSelectionVisual, cellIsSelected, + selectionFrames: selectionFramesData.value.frames, cellCanHover: canEditCellItem, infiniteScrollEnabled: infiniteScrollEnabled.value, pageOffset: rowNumberPageOffset(), @@ -6004,6 +6103,7 @@ function onTransposeCellContext(rowIndex: number, actualColIdx: number, event: M } watch([selectedRange, showCellDetail, isEditingDetail], () => { + if (isSelectingCells.value) return; const selectedCell = currentSelectedCellPosition(); const target = linkedCellDetailTarget({ isOpen: showCellDetail.value, @@ -9033,6 +9133,8 @@ const gridContextMenuItems = computed(() => { 'text-muted-foreground italic': cell.isNull, 'cell-selected': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], 'cell-selected-dirty': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], + 'cell-selected--sparse': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], + 'cell-selected-dirty--sparse': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], 'row-cell-selected': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], 'row-cell-selected-dirty': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex], 'bg-primary/15': transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex] && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex), @@ -9158,7 +9260,7 @@ const gridContextMenuItems = computed(() => {
# @@ -9674,141 +9776,143 @@ const gridContextMenuItems = computed(() => {
- -