From af9f490172021d52af37216db3f12b8aeb2d5d0d Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 9 Jul 2026 15:37:20 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(datagrid):=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E5=B7=A6=E5=9B=BA=E5=AE=9A=E5=88=97=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E5=B9=B6=E7=BB=9F=E4=B8=80=E9=80=89=E4=B8=AD=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 虚拟表横滚:表头 sticky + 表体 translateX 补偿,勾选/行号/钉住列同步固定 - 支持列头钉住左侧、钉住列显示图标;序号列可拖宽且不参与排序 - 行选中与单元格选区统一 V2 绿色,固定列底色对齐 --gn-bg-panel - 查询结果页启用列头筛选,并在客户端按条件过滤当前结果集 --- frontend/src/components/DataGrid.ddl.test.tsx | 4 + .../src/components/DataGrid.layout.test.tsx | 5 +- frontend/src/components/DataGrid.tsx | 156 +- .../src/components/DataGridColumnTitle.tsx | 30 +- frontend/src/components/DataGridCore.tsx | 49 +- frontend/src/components/DataGridShell.tsx | 3 + .../src/components/V2TableContextMenu.tsx | 15 + frontend/src/components/dataGridLayout.ts | 7 +- frontend/src/components/dataGridStyles.ts | 2212 ++++++++++++----- .../src/components/useDataGridColumnResize.ts | 30 +- .../src/components/useDataGridV2Actions.ts | 25 + frontend/src/store.ts | 48 + .../src/utils/dataGridClientFilter.test.ts | 36 + frontend/src/utils/dataGridClientFilter.ts | 170 ++ shared/i18n/de-DE.json | 3 + shared/i18n/en-US.json | 3 + shared/i18n/ja-JP.json | 3 + shared/i18n/ru-RU.json | 3 + shared/i18n/zh-CN.json | 3 + shared/i18n/zh-TW.json | 3 + 20 files changed, 2064 insertions(+), 744 deletions(-) create mode 100644 frontend/src/utils/dataGridClientFilter.test.ts create mode 100644 frontend/src/utils/dataGridClientFilter.ts diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index a7595d51..c7acf8fb 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -54,6 +54,7 @@ const storeState = vi.hoisted(() => ({ addTab: vi.fn(), setActiveContext: vi.fn(), tableColumnOrders: {}, + tablePinnedLeftColumns: {}, enableColumnOrderMemory: false, setTableColumnOrder: vi.fn(), setEnableColumnOrderMemory: vi.fn(), @@ -213,6 +214,7 @@ vi.mock('@ant-design/icons', () => { EditOutlined: Icon, VerticalAlignBottomOutlined: Icon, ColumnWidthOutlined: Icon, + PushpinOutlined: Icon, EyeInvisibleOutlined: Icon, LeftOutlined: Icon, RightOutlined: Icon, @@ -1324,6 +1326,8 @@ describe('DataGrid DDL interactions', () => { "t('data_grid.context_menu.current_marker')", "t('data_grid.context_menu.column_display_section')", "t('data_grid.context_menu.auto_fit_column')", + "t('data_grid.context_menu.pin_column_left')", + "t('data_grid.context_menu.unpin_column_left')", "t('data_grid.context_menu.hide_column')", "t('data_grid.context_menu.show_column_type')", "t('data_grid.context_menu.hide_column_type')", diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx index 5d4c97d0..ef2b9927 100644 --- a/frontend/src/components/DataGrid.layout.test.tsx +++ b/frontend/src/components/DataGrid.layout.test.tsx @@ -73,6 +73,7 @@ vi.mock('../store', () => ({ addTab: vi.fn(), setActiveContext: vi.fn(), tableColumnOrders: {}, + tablePinnedLeftColumns: {}, enableColumnOrderMemory: false, setTableColumnOrder: vi.fn(), setEnableColumnOrderMemory: vi.fn(), @@ -2130,6 +2131,8 @@ describe('DataGrid layout', () => { expect(markup).toContain('vertical-align:middle'); expect(markup).toContain('data-grid-row-number="true"'); expect(markup).toContain('width:36'); + // ant Table fixed 列会渲染 fix 相关 class + expect(markup.includes('ant-table-cell-fix') || markup.includes('fixed')).toBe(true); expect(markup).toContain('51'); } finally { setCurrentLanguage(previousLanguage); @@ -2542,7 +2545,7 @@ describe('DataGrid layout', () => { expect(filterHookSource).toContain('export type GridColumnFilterDraft'); expect(filterHookSource).toContain('const applyColumnFilter = React.useCallback'); expect(filterHookSource).toContain('onApplyFilter(nextConditions)'); - expect(source).toContain("const columnHeaderFilterEnabled = exportScope === 'table' && !!onApplyFilter;"); + expect(source).toContain("const columnHeaderFilterEnabled = !!onApplyFilter || exportScope === 'queryResult';"); expect(source).toContain("filterOpOptions.filter((option) => option.value !== 'CUSTOM')"); expect(source).toContain("eventTarget?.closest?.('[data-grid-column-filter-trigger=\"true\"]')"); expect(source).toContain("eventTarget?.closest?.('[data-grid-column-filter-popover=\"true\"]')"); diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index b4a1f853..0dae798b 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -42,6 +42,7 @@ import { resolveDataTableVerticalBorderColor, } from '../utils/dataGridDisplay'; import { resolvePaginationPageText, resolvePaginationSummaryText, resolvePaginationTotalForControl } from '../utils/dataGridPagination'; +import { filterRowsByGridConditions } from '../utils/dataGridClientFilter'; import { resolveGridSortInfoFromTableSorter } from '../utils/dataGridSort'; import { absorbExtraWidthIntoFlexibleColumns, @@ -308,6 +309,8 @@ const DataGrid: React.FC = ({ const setTableColumnOrder = useStore(state => state.setTableColumnOrder); const setEnableColumnOrderMemory = useStore(state => state.setEnableColumnOrderMemory); const clearTableColumnOrder = useStore(state => state.clearTableColumnOrder); + const tablePinnedLeftColumns = useStore(state => state.tablePinnedLeftColumns); + const setTablePinnedLeftColumns = useStore(state => state.setTablePinnedLeftColumns); const tableHiddenColumns = useStore(state => state.tableHiddenColumns); const enableHiddenColumnMemory = useStore(state => state.enableHiddenColumnMemory); @@ -478,11 +481,35 @@ const DataGrid: React.FC = ({ setAllOrderedColumnNames(nextOrder); }, [visibleColumnNames, tableColumnOrders, enableColumnOrderMemory, connectionId, dbName, tableName]); - // Compute final display columns + const tableMemoryKey = useMemo(() => { + if (!connectionId || !dbName || !tableName) return ''; + return `${connectionId}-${dbName}-${tableName}`; + }, [connectionId, dbName, tableName]); + + const pinnedLeftColumnNames = useMemo(() => { + if (!tableMemoryKey) return [] as string[]; + const stored = tablePinnedLeftColumns?.[tableMemoryKey]; + return Array.isArray(stored) ? stored.map((col) => String(col || '').trim()).filter(Boolean) : []; + }, [tableMemoryKey, tablePinnedLeftColumns]); + + const pinnedLeftColumnSet = useMemo( + () => new Set(pinnedLeftColumnNames), + [pinnedLeftColumnNames], + ); + + // Compute final display columns(固定列须连续靠左,否则 ant Table fixed 会错位) useEffect(() => { const hiddenSet = new Set(localHiddenColumns); - setDisplayColumnNames(allOrderedColumnNames.filter(col => !hiddenSet.has(col))); - }, [allOrderedColumnNames, localHiddenColumns]); + const visible = allOrderedColumnNames.filter((col) => !hiddenSet.has(col)); + if (pinnedLeftColumnNames.length === 0) { + setDisplayColumnNames(visible); + return; + } + const pinnedSet = new Set(pinnedLeftColumnNames); + const pinnedVisible = pinnedLeftColumnNames.filter((col) => visible.includes(col)); + const restVisible = visible.filter((col) => !pinnedSet.has(col)); + setDisplayColumnNames([...pinnedVisible, ...restVisible]); + }, [allOrderedColumnNames, localHiddenColumns, pinnedLeftColumnNames]); const displayOutputColumnNames = useMemo( () => resolveDataGridOutputColumnNames( @@ -1087,6 +1114,8 @@ const DataGrid: React.FC = ({ const gridCssText = useMemo( () => buildDataGridCssText({ darkMode, + // 固定列实心底必须跟表格内容底一致,禁止写死 #141414 / #1f1f1f + bgContent, dataGridBackdropFilter, dataTableVerticalBorderRule, densityParams, @@ -1275,7 +1304,8 @@ const DataGrid: React.FC = ({ resolveNextGridFilterOperatorForColumnChange, }); - const columnHeaderFilterEnabled = exportScope === 'table' && !!onApplyFilter; + // 表数据页:服务端筛选(onApplyFilter);查询结果页:列头筛选 + 客户端过滤当前结果 + const columnHeaderFilterEnabled = !!onApplyFilter || exportScope === 'queryResult'; const columnHeaderFilterOpOptions = useMemo( () => filterOpOptions.filter((option) => option.value !== 'CUSTOM'), [filterOpOptions], @@ -1323,6 +1353,7 @@ const DataGrid: React.FC = ({ columnMetaTooltipColor={columnMetaTooltipColor} darkMode={darkMode} highlighted={highlightedColumnName === normalizedName} + pinnedLeft={pinnedLeftColumnSet.has(normalizedName)} translate={translateDataGrid} onOpenForeignKey={foreignKeyTarget ? () => openForeignKeyTarget(foreignKeyTarget) : undefined} columnFilter={columnFilterState ? { @@ -1366,11 +1397,11 @@ const DataGrid: React.FC = ({ isListOp, isNoValueOp, openForeignKeyTarget, + pinnedLeftColumnSet, showColumnComment, showColumnType, translateDataGrid, ]); - const selectedRowKeysRef = useRef(selectedRowKeys); const displayDataRef = useRef([]); @@ -1618,8 +1649,13 @@ const DataGrid: React.FC = ({ ), [data, isMongoDBConnection]); const displayData = useMemo(() => { - return [...baseData, ...addedRows]; - }, [baseData, addedRows]); + const rows = [...baseData, ...addedRows]; + // 查询结果页没有服务端 WHERE 回调时,用列头筛选条件在客户端过滤当前结果集 + if (exportScope === 'queryResult' && filterConditions.length > 0) { + return filterRowsByGridConditions(rows, filterConditions); + } + return rows; + }, [addedRows, baseData, exportScope, filterConditions]); useEffect(() => { displayDataRef.current = displayData; }, [displayData]); @@ -2464,6 +2500,7 @@ const DataGrid: React.FC = ({ manualWidth: columnWidths[key], density: dataTableDensity, }), + ...(pinnedLeftColumnSet.has(key) ? { fixed: 'left' as const } : {}), sorter: onSort ? { multiple: displayColumnNames.indexOf(key) + 1 } : false, sortOrder: (sortInfo.find(s => s.columnKey === key && s.enabled !== false)?.order || null) as SortOrder | undefined, editable: canModifyData && isWritableResultColumn(key, effectiveEditLocator), @@ -2524,7 +2561,7 @@ const DataGrid: React.FC = ({ }, }), })); - }, [canModifyData, columnWidths, currentConnConfig, dataTableDensity, displayColumnNames, displayColumnTypeMap, enableVirtual, handleResizeAutoFit, handleResizeStart, isV2Ui, language, normalizedPageFindText, onSort, renderColumnTitle, showColumnComment, showColumnHeaderContextMenu, showColumnType, sortInfo]); + }, [canModifyData, columnWidths, currentConnConfig, dataTableDensity, displayColumnNames, displayColumnTypeMap, enableVirtual, handleResizeAutoFit, handleResizeStart, isV2Ui, language, normalizedPageFindText, onSort, pinnedLeftColumnSet, renderColumnTitle, showColumnComment, showColumnHeaderContextMenu, showColumnType, sortInfo]); const mergedColumns = useMemo(() => columns.map((col): ColumnType => { const dataIndex = String(col.dataIndex); @@ -2701,6 +2738,15 @@ const DataGrid: React.FC = ({ }; }), [closeVirtualInlineEditor, columns, currentConnConfig, darkMode, dbType, deletedRowKeys, displayColumnTypeMap, enableInlineEditableCell, enableVirtual, form, handleCellSave, handleSharedCellContextMenu, handleVirtualCellActivate, inputCellPadding, lockVirtualInlineTableScroll, modifiedColumns, openCellEditor, rowKeyStr, saveVirtualInlineEditor, updateFocusedCell, useInlineEditableBodyCell, virtualCellWrapperStyle, virtualEditingCell]); + const rowNumberColumnWidth = useMemo(() => { + const manual = columnWidths[GONAVI_ROW_NUMBER_COLUMN_KEY]; + if (typeof manual === 'number' && Number.isFinite(manual) && manual > 0) { + // 序号列允许拖宽,但保持较窄下限,避免挤占过多数据列 + return Math.max(28, Math.min(120, Math.round(manual))); + } + return ROW_NUMBER_COLUMN_WIDTH; + }, [columnWidths]); + const rowNumberColumn = useMemo>(() => ({ title: (
= ({ textAlign: 'center', }} > + {/* 序号列默认固定,不展示 📌,仅用户钉住的数据列显示图标 */} #
), key: GONAVI_ROW_NUMBER_COLUMN_KEY, dataIndex: GONAVI_ROW_NUMBER_COLUMN_KEY, - width: ROW_NUMBER_COLUMN_WIDTH, + width: rowNumberColumnWidth, className: 'data-grid-row-number-cell', align: 'center', - ellipsis: true, + // 横向滚动时固定在左侧;支持拖拽调宽,不参与排序 + fixed: 'left' as const, + sorter: false, + ellipsis: false, onHeaderCell: () => ({ + // 无 id → 不参与列拖拽排序;有 width + onResizeStart → 可拖列宽 + className: 'data-grid-row-number-cell ant-table-cell-fix-left', + width: rowNumberColumnWidth, + onResizeStart: handleResizeStart(GONAVI_ROW_NUMBER_COLUMN_KEY), + onResizeAutoFit: handleResizeAutoFit(GONAVI_ROW_NUMBER_COLUMN_KEY), style: { textAlign: 'center' as const, paddingInline: 2, verticalAlign: 'middle' as const, - width: ROW_NUMBER_COLUMN_WIDTH, - minWidth: ROW_NUMBER_COLUMN_WIDTH, - maxWidth: ROW_NUMBER_COLUMN_WIDTH, + width: rowNumberColumnWidth, + minWidth: 28, }, }), onCell: () => ({ style: { - width: ROW_NUMBER_COLUMN_WIDTH, - minWidth: ROW_NUMBER_COLUMN_WIDTH, - maxWidth: ROW_NUMBER_COLUMN_WIDTH, + width: rowNumberColumnWidth, + minWidth: 28, paddingInline: 2, textAlign: 'center' as const, }, @@ -2758,23 +2811,28 @@ const DataGrid: React.FC = ({ ); }, - }), [pagination?.current, pagination?.pageSize]); + }), [handleResizeAutoFit, handleResizeStart, pagination?.current, pagination?.pageSize, rowNumberColumnWidth]); const tableColumns = useMemo(() => { const baseColumns = resolvedShowRowNumberColumn ? [rowNumberColumn, ...mergedColumns] : mergedColumns; // 少列时把视口多余宽度只加到数据列,避免行号列被 rc-table 均摊撑宽 + const fixedKeys = [ + ...(resolvedShowRowNumberColumn ? [GONAVI_ROW_NUMBER_COLUMN_KEY] : []), + ...pinnedLeftColumnNames, + ]; return absorbExtraWidthIntoFlexibleColumns({ columns: baseColumns, selectionColumnWidth, tableViewportWidth, - fixedColumnKeys: resolvedShowRowNumberColumn ? [GONAVI_ROW_NUMBER_COLUMN_KEY] : [], + fixedColumnKeys: fixedKeys, defaultColumnWidth: densityParams.defaultColumnWidth, }); }, [ densityParams.defaultColumnWidth, mergedColumns, + pinnedLeftColumnNames, resolvedShowRowNumberColumn, rowNumberColumn, selectionColumnWidth, @@ -3221,6 +3279,8 @@ const DataGrid: React.FC = ({ supportsSqlQueryExport, tableName, toggleColumnVisibility, + pinnedLeftColumnNames, + setTablePinnedLeftColumns, translateDataGrid, uniqueKeyGroups, withSortBufferTuningSQL, @@ -3314,6 +3374,8 @@ const DataGrid: React.FC = ({ selectedRowKeys, onChange: setSelectedRowKeys, columnWidth: selectionColumnWidth, + // 与行号列一起左侧固定,横向滚动时仍可勾选/识别行 + fixed: true as const, ...(isV2Ui ? {} : { renderCell: (_checked: boolean, _record: any, _index: number, originNode: React.ReactNode) => (
@@ -3382,12 +3444,20 @@ const DataGrid: React.FC = ({ const readVirtualHorizontalOffset = useCallback((tableContainer: HTMLElement): number => { const { innerEl, headerEl } = resolveVirtualHorizontalElements(tableContainer); + // 虚拟表数据区横向靠 marginLeft 驱动(rc-virtual-list 约定),必须以此为准 if (innerEl instanceof HTMLElement) { return Math.max(0, Math.abs(parseFloat(innerEl.style.marginLeft) || 0)); } return headerEl ? Math.max(0, headerEl.scrollLeft) : 0; }, [resolveVirtualHorizontalElements]); + /** + * 虚拟表横滚视觉同步: + * - 表体:filler marginLeft = -offset;固定列用 --gn-datagrid-h-scroll 做 translateX 补偿 + * (filler 有 transform:translateY,sticky 在表体不可靠) + * - 表头:真实 scrollLeft + sticky 固定全选/行号(不要对 header table 做 transform, + * 否则会把全选 checkbox / # 裁没或钉飞) + */ const syncVirtualHorizontalVisualOffset = useCallback((tableContainer: HTMLElement, nextOffset: number) => { const { holderEl, innerEl, headerEl } = resolveVirtualHorizontalElements(tableContainer); if (!(holderEl instanceof HTMLElement) || !(innerEl instanceof HTMLElement)) { @@ -3398,12 +3468,35 @@ const DataGrid: React.FC = ({ const clampedOffset = Math.max(0, Math.min(maxScroll, nextOffset)); const currentOffset = Math.max(0, Math.abs(parseFloat(innerEl.style.marginLeft) || 0)); const nextMarginLeft = `${-clampedOffset}px`; + const scrollVar = `${clampedOffset}px`; if (innerEl.style.marginLeft !== nextMarginLeft) { innerEl.style.marginLeft = nextMarginLeft; } - if (headerEl instanceof HTMLElement && Math.abs(headerEl.scrollLeft - clampedOffset) > 1) { - headerEl.scrollLeft = clampedOffset; + + // 仅表体固定列补偿用;表头走 sticky,不要挂 transform + tableContainer.style.setProperty('--gn-datagrid-h-scroll', scrollVar); + holderEl.style.setProperty('--gn-datagrid-h-scroll', scrollVar); + innerEl.style.setProperty('--gn-datagrid-h-scroll', scrollVar); + + if (headerEl instanceof HTMLElement) { + headerEl.style.removeProperty('--gn-datagrid-h-scroll'); + const headerTable = headerEl.querySelector('table') as HTMLElement | null; + if (headerTable) { + if (headerTable.style.marginLeft) { + headerTable.style.marginLeft = ''; + } + // 去掉历史 transform 方案残留 + if (headerTable.style.transform) { + headerTable.style.transform = ''; + } + headerTable.style.setProperty('width', `${tableScrollX}px`, 'important'); + headerTable.style.setProperty('min-width', `${tableScrollX}px`, 'important'); + headerTable.style.setProperty('max-width', 'none', 'important'); + } + if (Math.abs(headerEl.scrollLeft - clampedOffset) > 1) { + headerEl.scrollLeft = clampedOffset; + } } return { holderEl, clampedOffset, currentOffset }; @@ -3419,20 +3512,33 @@ const DataGrid: React.FC = ({ const deltaX = clampedOffset - currentOffset; if (Math.abs(deltaX) < 0.5 && !options?.forceInternalScroll) return true; + const reassertVisual = () => { + syncVirtualHorizontalVisualOffset(tableContainer, clampedOffset); + }; + const tableInstance = tableRef.current; if (tableInstance && typeof tableInstance.scrollTo === 'function') { + // 更新 rc-virtual-list 内部 offsetLeft tableInstance.scrollTo({ left: clampedOffset, top: holderEl.scrollTop }); + // antd onInternalScroll 会 forceScroll(header.scrollLeft),需在其后清掉并重申 CSS 变量 + requestAnimationFrame(() => { + reassertVisual(); + window.setTimeout(reassertVisual, 0); + }); return true; } - // 回退:通过合成 WheelEvent 驱动 rc-virtual-list 内部 offsetLeft state, - // 让 rc-table onInternalScroll 自动同步 header scrollLeft。 + // 回退:合成 WheelEvent 驱动 rc-virtual-list 内部 offsetLeft state holderEl.dispatchEvent(new WheelEvent('wheel', { deltaX: deltaX, deltaY: 0, bubbles: true, cancelable: true, })); + requestAnimationFrame(() => { + reassertVisual(); + window.setTimeout(reassertVisual, 0); + }); return true; }, [syncVirtualHorizontalVisualOffset]); @@ -3464,6 +3570,7 @@ const DataGrid: React.FC = ({ }); }, [applyVirtualHorizontalOffset, enableVirtual, isTableSurfaceActive, readVirtualHorizontalOffset, syncVirtualHorizontalVisualOffset]); + const flushVirtualHorizontalWheel = useCallback((tableContainer: HTMLElement) => { tableHorizontalWheelRafRef.current = null; const delta = pendingTableHorizontalDeltaRef.current; @@ -4024,8 +4131,8 @@ const DataGrid: React.FC = ({ const headerTable = container.querySelector('.ant-table-header > table') as HTMLElement; if (headerTable) { headerTable.style.setProperty('width', `${tableScrollX}px`, 'important'); - headerTable.style.setProperty('min-width', '0px', 'important'); - headerTable.style.setProperty('max-width', `${tableScrollX}px`, 'important'); + headerTable.style.setProperty('min-width', `${tableScrollX}px`, 'important'); + headerTable.style.setProperty('max-width', 'none', 'important'); } }; syncHeaderWidth(); @@ -4507,6 +4614,7 @@ const DataGrid: React.FC = ({ panelPaddingY, panelRadius, pendingChangeCount, + pinnedLeftColumnSet, pkColumns, prefersManualTotalCount, previewModalOpen, diff --git a/frontend/src/components/DataGridColumnTitle.tsx b/frontend/src/components/DataGridColumnTitle.tsx index 04e051df..bfe01c51 100644 --- a/frontend/src/components/DataGridColumnTitle.tsx +++ b/frontend/src/components/DataGridColumnTitle.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button, Input, Popover, Select, Tooltip } from 'antd'; -import { FilterOutlined, LinkOutlined } from '@ant-design/icons'; +import { FilterOutlined, LinkOutlined, PushpinOutlined } from '@ant-design/icons'; import { t as defaultTranslate, type I18nParams } from '../i18n'; export type DataGridColumnTitleTranslate = (key: string, params?: I18nParams) => string; @@ -49,6 +49,8 @@ export interface DataGridColumnTitleProps { columnMetaTooltipColor: string; darkMode: boolean; highlighted?: boolean; + /** 左侧钉住列:表头显示固定图标 */ + pinnedLeft?: boolean; translate?: DataGridColumnTitleTranslate; onOpenForeignKey?: () => void; columnFilter?: DataGridColumnFilterConfig | null; @@ -69,6 +71,7 @@ const DataGridColumnTitle: React.FC = ({ columnMetaTooltipColor, darkMode, highlighted = false, + pinnedLeft = false, translate = defaultTranslate, onOpenForeignKey, columnFilter, @@ -81,6 +84,18 @@ const DataGridColumnTitle: React.FC = ({ const shouldShowColumnType = showColumnType && columnType.length > 0; const shouldShowColumnComment = showColumnComment && columnComment.length > 0; const isSingleLineColumnTitle = !shouldShowColumnType && !shouldShowColumnComment; + const pinIcon = pinnedLeft ? ( + + ) : null; const [filterPopoverOpen, setFilterPopoverOpen] = React.useState(false); const initialFilterOperator = columnFilter?.initialOperator || columnFilter?.defaultOperator || '='; const [draftFilterOperator, setDraftFilterOperator] = React.useState(initialFilterOperator); @@ -136,13 +151,19 @@ const DataGridColumnTitle: React.FC = ({ cursor: 'pointer', }} > + {pinIcon} {normalizedName} ) : ( - {normalizedName} + + {pinIcon} + + {normalizedName} + + ); const titleNode = ( @@ -375,7 +396,8 @@ const DataGridColumnTitle: React.FC = ({ className="gn-v2-column-title-shell" style={{ display: 'inline-flex', - alignItems: 'center', + // 顶对齐:有无注释时标题块高度不同,center 会让筛选图标上下错位 + alignItems: 'flex-start', gap: 4, maxWidth: '100%', minWidth: 0, @@ -409,6 +431,8 @@ const DataGridColumnTitle: React.FC = ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + // 与列名首行光学对齐(约 1.2 行高与 22px 按钮的中线差) + marginTop: 0, padding: 0, border: columnFilter.active ? `1px solid ${activeColor}` : '1px solid transparent', borderRadius: 6, diff --git a/frontend/src/components/DataGridCore.tsx b/frontend/src/components/DataGridCore.tsx index 532f2e68..9bb02413 100644 --- a/frontend/src/components/DataGridCore.tsx +++ b/frontend/src/components/DataGridCore.tsx @@ -693,8 +693,15 @@ const ResizableTitle = React.forwardRef((props, ref) return ; } + // 缩放手柄 absolute 定位需要 relative 上下文; + // 固定列表头的 sticky 由 CSS !important 覆盖,不会被这里的 relative 破坏。 + const thStyle: React.CSSProperties = { + ...nextStyle, + position: 'relative', + }; + return ( - + {restProps.children} ((props, ref) top: 0, width: 10, cursor: 'col-resize', - zIndex: 10, + // 必须低于固定列表头 z-index(30),否则横向滚动时会穿透到勾选/行号上方 + zIndex: 2, touchAction: 'none' }} /> @@ -779,20 +787,20 @@ const SortableHeaderCell: React.FC = React.memo((props) isDragging, } = useSortable({ id: id || '' }); + // 未拖拽时不要写 transform/willChange,否则会破坏表头 sticky 固定列(全选 / 行号) + const dndTransform = isDragging ? CSS.Transform.toString(transform) : undefined; const style: React.CSSProperties = { ...propStyle, - transform: CSS.Transform.toString(transform), + ...(dndTransform ? { transform: dndTransform, willChange: 'transform' as const } : {}), transition, - ...(isDragging ? { - position: 'relative', - zIndex: 9999, - opacity: 0.6, + ...(isDragging ? { + position: 'relative', + zIndex: 9999, + opacity: 0.6, backgroundColor: 'rgba(24, 144, 255, 0.15)', - boxShadow: '0 4px 12px rgba(0,0,0,0.15)' + boxShadow: '0 4px 12px rgba(0,0,0,0.15)', } : {}), touchAction: 'none', - willChange: 'transform', - // 核心修复:将指针直接绑定到 th 级别,并由 isPressed 控制 cursor: (isDragging || isPressed) ? 'grabbing' : 'pointer', }; @@ -802,18 +810,27 @@ const SortableHeaderCell: React.FC = React.memo((props) return () => window.removeEventListener('mouseup', handleGlobalMouseUp); }, []); + // 选择列 / 无 id:保留 propStyle(含 sticky left),不要被 dnd 样式污染 if (!id || id === 'GONAVI_SELECTION_COLUMN') { - return {children}; + return ( + + {children} + + ); } return ( - { setIsPressed(true); diff --git a/frontend/src/components/DataGridShell.tsx b/frontend/src/components/DataGridShell.tsx index eed82466..2b251e5d 100644 --- a/frontend/src/components/DataGridShell.tsx +++ b/frontend/src/components/DataGridShell.tsx @@ -243,6 +243,7 @@ const DataGridShell: React.FC = (props) => { panelPaddingY, panelRadius, pendingChangeCount, + pinnedLeftColumnSet, pkColumns, prefersManualTotalCount, previewModalOpen, @@ -873,6 +874,8 @@ const renderDataTableView = () => ( sortOrder={(activeSort?.order === 'ascend' || activeSort?.order === 'descend') ? activeSort.order : null} showColumnType={showColumnType} showColumnComment={showColumnComment} + pinnedLeft={Boolean(pinnedLeftColumnSet?.has?.(fieldName))} + canPinLeft={Boolean(connectionId && dbName && tableName)} onAction={handleV2ColumnHeaderContextMenuAction} /> ); diff --git a/frontend/src/components/V2TableContextMenu.tsx b/frontend/src/components/V2TableContextMenu.tsx index 7af3005c..fddf9e6b 100644 --- a/frontend/src/components/V2TableContextMenu.tsx +++ b/frontend/src/components/V2TableContextMenu.tsx @@ -646,6 +646,8 @@ export type V2ColumnHeaderContextMenuActionKey = | 'sort-desc' | 'clear-sort' | 'auto-fit-column' + | 'pin-column-left' + | 'unpin-column-left' | 'hide-column' | 'show-column-type' | 'hide-column-type' @@ -660,6 +662,8 @@ export const V2ColumnHeaderContextMenuView: React.FC<{ sortOrder?: 'ascend' | 'descend' | null; showColumnType?: boolean; showColumnComment?: boolean; + pinnedLeft?: boolean; + canPinLeft?: boolean; onAction?: (action: V2ColumnHeaderContextMenuActionKey) => void; }> = ({ fieldName, @@ -669,6 +673,8 @@ export const V2ColumnHeaderContextMenuView: React.FC<{ sortOrder, showColumnType = true, showColumnComment = true, + pinnedLeft = false, + canPinLeft = true, onAction, }) => { const renderItems = (items: V2TableContextMenuItemConfig[]) => renderV2ContextMenuItems( @@ -708,6 +714,15 @@ export const V2ColumnHeaderContextMenuView: React.FC<{
{t('data_grid.context_menu.column_display_section')}
{renderItems([ { action: 'auto-fit-column', icon: , title: t('data_grid.context_menu.auto_fit_column') }, + { + action: pinnedLeft ? 'unpin-column-left' : 'pin-column-left', + icon: , + title: pinnedLeft + ? t('data_grid.context_menu.unpin_column_left') + : t('data_grid.context_menu.pin_column_left'), + selected: pinnedLeft, + disabled: !canPinLeft && !pinnedLeft, + }, { action: 'hide-column', icon: , title: t('data_grid.context_menu.hide_column') }, { action: showColumnType ? 'hide-column-type' : 'show-column-type', diff --git a/frontend/src/components/dataGridLayout.ts b/frontend/src/components/dataGridLayout.ts index fc4ac543..115eb221 100644 --- a/frontend/src/components/dataGridLayout.ts +++ b/frontend/src/components/dataGridLayout.ts @@ -68,7 +68,10 @@ export const calculateVirtualTableScrollX = ({ return safeTotalWidth; }; -export type AbsorbExtraColumnWidthOptions = { +/** 兼容 antd ColumnType.key(React.Key = string | number | bigint) */ +export type AbsorbExtraColumnKey = string | number | bigint; + +export type AbsorbExtraColumnWidthOptions = { columns: T[]; selectionColumnWidth: number; tableViewportWidth: number; @@ -81,7 +84,7 @@ export type AbsorbExtraColumnWidthOptions({ +export const absorbExtraWidthIntoFlexibleColumns = ({ columns, selectionColumnWidth, tableViewportWidth, diff --git a/frontend/src/components/dataGridStyles.ts b/frontend/src/components/dataGridStyles.ts index 59b95a8b..8e2d3ce7 100644 --- a/frontend/src/components/dataGridStyles.ts +++ b/frontend/src/components/dataGridStyles.ts @@ -1,691 +1,1521 @@ -type DataGridCssTextParams = Record; - -export const buildDataGridCssText = ({ - darkMode, - dataGridBackdropFilter, - dataTableVerticalBorderRule, - densityParams, - floatingScrollbarBottomOffset, - floatingScrollbarHeight, - floatingScrollbarInset, - floatingScrollbarThumbBg, - floatingScrollbarThumbBorderColor, - floatingScrollbarThumbHoverBg, - floatingScrollbarThumbShadow, - gridId, - horizontalScrollbarThumbBg, - horizontalScrollbarThumbBorderColor, - horizontalScrollbarThumbHoverBg, - horizontalScrollbarThumbShadow, - horizontalScrollbarTrackBg, - horizontalScrollbarTrackBorderColor, - horizontalScrollbarTrackShadow, - paginationAccentBg, - paginationAccentBorderColor, - paginationActiveItemBg, - paginationActiveItemBorderColor, - paginationActiveItemTextColor, - paginationChipBg, - paginationChipBorderColor, - paginationHoverBg, - paginationPrimaryTextColor, - paginationSecondaryTextColor, - paginationShellBg, - paginationShellBorderColor, - paginationShellShadow, - panelRadius, - rowAddedBg, - rowAddedHover, - rowModBg, - rowModHover, - selectionAccentHex, - selectionAccentRgb, - tableBodyBottomPadding, - useVirtualEditablePaintContain, - useVirtualEditableVisibilityHints, - useVirtualHolderPaintHints, - useVirtualRowCellContain, - verticalScrollbarTrackBg, -}: DataGridCssTextParams) => ` - .${gridId} .data-grid-toolbar-scroll > * { - flex-shrink: 0; - } - .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar { - height: 7px; - } - .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar-thumb { - background: ${darkMode ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.22)'}; - border: 0; - background-clip: border-box; - border-radius: 999px; - } - .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar-thumb:hover { - background: ${darkMode ? 'rgba(255,255,255,0.38)' : 'rgba(0,0,0,0.32)'}; - border: 0; - background-clip: border-box; - } - .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar-track { - background: transparent; - } - .${gridId} .ant-table, - .${gridId} .ant-table-wrapper, - .${gridId} .ant-table-container { - background: transparent !important; - border-radius: ${panelRadius}px !important; - } - .${gridId} .ant-table-wrapper, - .${gridId} .ant-table-container { - border: none !important; - overflow: hidden !important; - } - .${gridId} .ant-table-tbody > tr > td, - .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell, - .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: ${dataTableVerticalBorderRule} !important; font-size: ${densityParams.dataFontSize}px !important; vertical-align: middle !important; } - .${gridId} .ant-table-thead > tr > th { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: ${dataTableVerticalBorderRule} !important; font-size: ${densityParams.dataFontSize}px !important; } - .${gridId} .ant-table-tbody > tr > td:last-child, - .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell:last-child, - .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell:last-child, - .${gridId} .ant-table-thead > tr > th:last-child { - border-inline-end-color: transparent !important; - } - /* 选择列对齐:header TH 无 class(Ant Design 虚拟模式),需用 :first-child 匹配 */ - .${gridId} .ant-table-header th:first-child, - .${gridId} .ant-table-thead > tr > th:first-child { - text-align: center !important; - padding-inline-start: 0 !important; - padding-inline-end: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; - } - .${gridId} .ant-table-selection-column { - vertical-align: middle !important; - text-align: center !important; - padding-inline-start: 0 !important; - padding-inline-end: 0 !important; - } - .${gridId} .ant-table-selection-column .ant-checkbox-wrapper { - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - margin-right: 0 !important; - } - /* 窄表场景下 rc-table 会按视口等比放大选择列宽度,不能再额外锁死 header 宽度; - 这里只统一 header/body 的内边距与对齐方式,避免第一列把后续数据列整体顶偏。 */ - .${gridId} .ant-table-tbody > tr > td.ant-table-selection-column, - .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column { - text-align: center !important; - vertical-align: middle !important; - padding-inline-start: 0 !important; - padding-inline-end: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; - } - .${gridId} .ant-table-tbody > tr > td.ant-table-selection-column .ant-checkbox-wrapper, - .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column .ant-checkbox-wrapper { - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - margin-right: 0 !important; - } - .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-selection-column { - display: flex !important; - align-items: center !important; - justify-content: center !important; - padding-inline-start: 0 !important; - padding-inline-end: 0 !important; - padding-left: 0 !important; - padding-right: 0 !important; - } - .${gridId} .ant-table-thead > tr:first-child > th:first-child, - .${gridId} .ant-table-header table > thead > tr:first-child > th:first-child { - border-top-left-radius: ${panelRadius}px !important; - } - .${gridId} .ant-table-thead > tr:first-child > th:last-child, - .${gridId} .ant-table-header table > thead > tr:first-child > th:last-child { - border-top-right-radius: ${panelRadius}px !important; - } - .${gridId} .ant-table-body { - border-bottom-left-radius: ${panelRadius}px !important; - border-bottom-right-radius: ${panelRadius}px !important; - } - .${gridId} .ant-table-thead > tr > th::before { display: none !important; } - .${gridId} .ant-table-thead > tr > th .ant-table-column-sorters { cursor: default !important; } - .${gridId} .ant-table-thead > tr > th .ant-table-column-sorter, - .${gridId} .ant-table-thead > tr > th .ant-table-column-sorter * { cursor: pointer !important; } - .${gridId} .ant-table-tbody > tr:hover > td, - .${gridId} .ant-table-tbody .ant-table-row:hover > .ant-table-cell { background-color: ${darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)'} !important; } - .${gridId} .ant-table-tbody > tr.ant-table-row-selected > td, - .${gridId} .ant-table-tbody .ant-table-row.ant-table-row-selected > .ant-table-cell { background-color: ${darkMode ? `rgba(${selectionAccentRgb}, 0.18)` : `rgba(${selectionAccentRgb}, 0.08)`} !important; } - .${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td, - .${gridId} .ant-table-tbody .ant-table-row.ant-table-row-selected:hover > .ant-table-cell { background-color: ${darkMode ? `rgba(${selectionAccentRgb}, 0.28)` : `rgba(${selectionAccentRgb}, 0.12)`} !important; } - .${gridId} .row-added td, - .${gridId} .row-added > .ant-table-cell { background-color: ${rowAddedBg} !important; color: ${darkMode ? '#e6fffb' : 'inherit'}; } - .${gridId} .row-modified td, - .${gridId} .row-modified > .ant-table-cell { background-color: ${rowModBg} !important; color: ${darkMode ? '#e6f7ff' : 'inherit'}; } - .${gridId} .row-deleted td, - .${gridId} .row-deleted > .ant-table-cell { background-color: ${darkMode ? '#1f1f1f' : '#f0f0f0'} !important; color: ${darkMode ? '#595959' : '#bfbfbf'} !important; text-decoration: line-through; } - .${gridId} .ant-table-tbody > tr.row-added:hover > td, - .${gridId} .ant-table-tbody .ant-table-row.row-added:hover > .ant-table-cell { background-color: ${rowAddedHover} !important; } - .${gridId} .ant-table-tbody > tr.row-modified:hover > td, - .${gridId} .ant-table-tbody .ant-table-row.row-modified:hover > .ant-table-cell { background-color: ${rowModHover} !important; } - .${gridId} .ant-table-tbody > tr.row-deleted:hover > td, - .${gridId} .ant-table-tbody .ant-table-row.row-deleted:hover > .ant-table-cell { background-color: ${darkMode ? '#2a2a2a' : '#e8e8e8'} !important; } - .${gridId}.cell-edit-mode .ant-table-tbody > tr > td[data-col-name], - .${gridId}.cell-edit-mode .ant-table-tbody .ant-table-row > .ant-table-cell[data-col-name] { user-select: none; -webkit-user-select: none; cursor: crosshair; } - .${gridId} .ant-table-tbody > tr > td[data-cell-selected="true"], - .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell[data-cell-selected="true"], - .${gridId} [data-cell-selected="true"] { - box-shadow: inset 0 0 0 2px ${selectionAccentHex} !important; - background-image: linear-gradient(${darkMode ? `rgba(${selectionAccentRgb}, 0.20)` : `rgba(${selectionAccentRgb}, 0.08)`}, ${darkMode ? `rgba(${selectionAccentRgb}, 0.20)` : `rgba(${selectionAccentRgb}, 0.08)`}) !important; - } - .${gridId} .ant-table-content, - .${gridId} .ant-table-body { - scrollbar-gutter: stable; - } - .${gridId} .ant-table-body { - padding-bottom: ${tableBodyBottomPadding}px; - box-sizing: border-box; - scroll-padding-bottom: ${tableBodyBottomPadding}px; - contain: layout paint style; - } - .${gridId} .ant-table-tbody-virtual-holder, - .${gridId} .rc-virtual-list-holder { - padding-bottom: ${tableBodyBottomPadding}px; - box-sizing: border-box; - scroll-padding-bottom: ${tableBodyBottomPadding}px; - contain: ${useVirtualHolderPaintHints ? 'layout paint style' : 'layout style'}; - content-visibility: ${useVirtualHolderPaintHints ? 'auto' : 'visible'}; - } - .${gridId} .ant-table-tbody-virtual-holder-inner { - padding-bottom: ${tableBodyBottomPadding}px; - box-sizing: border-box; - contain: ${useVirtualHolderPaintHints ? 'layout paint style' : 'layout style'}; - } - .${gridId} .ant-table-tbody-virtual-holder .ant-table-row, - .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { - contain: ${useVirtualRowCellContain ? 'layout paint style' : 'none'}; - } - .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .${gridId}.gn-v2-data-grid .ant-table-tbody > tr > td, - .${gridId}.gn-v2-data-grid .ant-table-tbody .ant-table-row > .ant-table-cell, - .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { - vertical-align: middle !important; - } - .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-row-hover, - .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.data-grid-virtual-inline-editing { - overflow: visible; - text-overflow: clip; - white-space: normal; - } - .${gridId} .data-grid-table-wrap { - width: 100%; - max-width: 100%; - overflow: hidden; - } - .${gridId} .ant-table-sticky-scroll { - display: none !important; - } - .${gridId} .data-grid-find-highlight { - padding: 0 1px; - border-radius: 3px; - background: ${darkMode ? 'rgba(246, 196, 83, 0.42)' : 'rgba(255, 193, 7, 0.42)'}; - color: inherit; - } - .${gridId} .editable-cell-value-wrap { - display: block; - width: 100%; - min-width: 0; - min-height: 20px; - padding-right: 0; - position: relative; - contain: ${useVirtualEditablePaintContain ? 'layout paint style' : 'layout style'}; - } - .${gridId} .editable-cell-value-wrap > * { - min-width: 0; - } - .${gridId} .data-grid-inline-editor-form-item, - .${gridId} .data-grid-inline-editor-form-item .ant-form-item-row, - .${gridId} .data-grid-inline-editor-form-item .ant-form-item-control, - .${gridId} .data-grid-inline-editor-form-item .ant-form-item-control-input, - .${gridId} .data-grid-inline-editor-form-item .ant-form-item-control-input-content { - width: 100%; - min-width: 0; - } - .${gridId} .data-grid-inline-editor-input, - .${gridId} .data-grid-inline-editor-form-item .ant-picker { - width: 100% !important; - min-width: 0; - } - .${gridId} .ant-table-tbody-virtual-holder .editable-cell-value-wrap { - content-visibility: ${useVirtualEditableVisibilityHints ? 'auto' : 'visible'}; - contain-intrinsic-size: ${useVirtualEditableVisibilityHints ? '24px 160px' : 'auto'}; - } - /* 虚拟表列对齐:阻止 header 通过 min-width:100% 拉伸到视口, - 使 header 列宽与虚拟 body 单元格宽度精确一致 */ - .${gridId} .ant-table-header > table { - min-width: 0 !important; - } - .${gridId} .ant-table-tbody-virtual-scrollbar.ant-table-tbody-virtual-scrollbar-horizontal { - display: none !important; - } - .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-content { - overflow-x: hidden !important; - } - .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-body { - overflow-x: hidden !important; - overflow-y: auto !important; - } - .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-tbody-virtual-holder, - .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .rc-virtual-list-holder { - overflow-x: hidden !important; - } - .${gridId} .ant-table-body { - scrollbar-width: thin; - scrollbar-color: ${floatingScrollbarThumbBg} transparent; - } - .${gridId} .ant-table-body::-webkit-scrollbar { - width: ${floatingScrollbarHeight}px; - height: 0; - } - .${gridId} .ant-table-body::-webkit-scrollbar-track { - background: ${verticalScrollbarTrackBg}; - margin: 8px 0; - border-radius: 999px; - } - .${gridId} .ant-table-body::-webkit-scrollbar-thumb { - background: ${floatingScrollbarThumbBg}; - border: 1px solid ${floatingScrollbarThumbBorderColor}; - background-clip: border-box; - border-radius: 999px; - box-shadow: ${floatingScrollbarThumbShadow}; - } - .${gridId} .ant-table-body::-webkit-scrollbar-thumb:hover { - background: ${floatingScrollbarThumbHoverBg}; - border: 1px solid ${floatingScrollbarThumbBorderColor}; - background-clip: border-box; - box-shadow: ${floatingScrollbarThumbShadow}; - } - .${gridId} .rc-virtual-list-holder { - scrollbar-width: thin; - scrollbar-color: ${floatingScrollbarThumbBg} transparent; - } - .${gridId} .rc-virtual-list-holder::-webkit-scrollbar { - width: ${floatingScrollbarHeight}px; - height: 0; - } - .${gridId} .rc-virtual-list-holder::-webkit-scrollbar-track { - background: ${verticalScrollbarTrackBg}; - margin: 8px 0; - border-radius: 999px; - } - .${gridId} .rc-virtual-list-holder::-webkit-scrollbar-thumb { - background: ${floatingScrollbarThumbBg}; - border: 1px solid ${floatingScrollbarThumbBorderColor}; - background-clip: border-box; - border-radius: 999px; - box-shadow: ${floatingScrollbarThumbShadow}; - } - .${gridId} .rc-virtual-list-holder::-webkit-scrollbar-thumb:hover { - background: ${floatingScrollbarThumbHoverBg}; - border: 1px solid ${floatingScrollbarThumbBorderColor}; - background-clip: border-box; - box-shadow: ${floatingScrollbarThumbShadow}; - } - .${gridId} .data-grid-external-horizontal-scroll { - position: absolute; - left: ${floatingScrollbarInset}px; - right: ${floatingScrollbarInset}px; - bottom: ${floatingScrollbarBottomOffset}px; - height: ${floatingScrollbarHeight + 4}px; - overflow-x: auto; - overflow-y: hidden; - background: transparent; - z-index: 24; - } - .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar { - height: ${floatingScrollbarHeight}px; - } - .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar-track { - background: ${horizontalScrollbarTrackBg}; - border: 1px solid ${horizontalScrollbarTrackBorderColor}; - border-radius: 999px; - box-shadow: ${horizontalScrollbarTrackShadow}; - } - .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar-thumb { - background: ${horizontalScrollbarThumbBg}; - border: 1px solid ${horizontalScrollbarThumbBorderColor}; - background-clip: border-box; - border-radius: 999px; - box-shadow: ${horizontalScrollbarThumbShadow}; - } - .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar-thumb:hover { - background: ${horizontalScrollbarThumbHoverBg}; - border: 1px solid ${horizontalScrollbarThumbBorderColor}; - background-clip: border-box; - box-shadow: ${horizontalScrollbarThumbShadow}; - } - .${gridId} .data-grid-external-horizontal-scroll-inner { - height: 1px; - } - .${gridId} .data-grid-pagination-shell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 10px; - flex-wrap: wrap; - max-width: 100%; - padding: 8px 10px; - border-radius: 16px; - border: 1px solid ${paginationShellBorderColor}; - background: ${paginationShellBg}; - box-shadow: ${paginationShellShadow}; - backdrop-filter: ${dataGridBackdropFilter}; - -webkit-backdrop-filter: ${dataGridBackdropFilter}; - } - .${gridId} .data-grid-pagination-summary, - .${gridId} .data-grid-pagination-page-chip { - display: inline-flex; - align-items: center; - gap: 8px; - min-height: 34px; - padding: 0 12px; - border-radius: 999px; - border: 1px solid ${paginationChipBorderColor}; - background: ${paginationChipBg}; - color: ${paginationPrimaryTextColor}; - font-size: 12px; - line-height: 1; - font-variant-numeric: tabular-nums; - white-space: nowrap; - } - .${gridId} .data-grid-pagination-kicker { - display: inline-flex; - align-items: center; - height: 20px; - padding: 0 8px; - border-radius: 999px; - background: ${paginationAccentBg}; - border: 1px solid ${paginationAccentBorderColor}; - color: ${paginationActiveItemTextColor}; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.02em; - } - .${gridId} .data-grid-pagination-summary-value { - color: ${paginationPrimaryTextColor}; - font-weight: 600; - font-variant-numeric: tabular-nums; - } - .${gridId} .data-grid-pagination-page-chip { - color: ${paginationSecondaryTextColor}; - font-weight: 600; - } - .${gridId} .ant-pagination { - display: inline-flex; - align-items: center; - gap: 6px; - margin: 0; - color: ${paginationPrimaryTextColor}; - } - .${gridId} .ant-pagination .ant-pagination-item, - .${gridId} .ant-pagination .ant-pagination-prev, - .${gridId} .ant-pagination .ant-pagination-next, - .${gridId} .ant-pagination .ant-pagination-jump-prev, - .${gridId} .ant-pagination .ant-pagination-jump-next { - min-width: 34px; - height: 34px; - margin-inline-end: 0; - border-radius: 12px; - border: 1px solid ${paginationChipBorderColor}; - background: ${paginationChipBg}; - box-shadow: none; - display: inline-flex; - align-items: center; - justify-content: center; - overflow: hidden; - transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease, box-shadow 160ms ease; - } - .${gridId} .ant-pagination .ant-pagination-item a, - .${gridId} .ant-pagination .ant-pagination-prev .ant-pagination-item-link, - .${gridId} .ant-pagination .ant-pagination-next .ant-pagination-item-link, - .${gridId} .ant-pagination .ant-pagination-prev > *, - .${gridId} .ant-pagination .ant-pagination-next > * { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - color: ${paginationPrimaryTextColor}; - font-weight: 600; - border: none; - background: transparent; - border-radius: inherit; - line-height: 1; - } - .${gridId} .ant-pagination .ant-pagination-item:hover, - .${gridId} .ant-pagination .ant-pagination-prev:hover, - .${gridId} .ant-pagination .ant-pagination-next:hover { - background: ${paginationHoverBg}; - border-color: ${paginationActiveItemBorderColor}; - transform: translateY(-1px); - } - .${gridId} .ant-pagination .ant-pagination-item-active { - border-color: ${paginationActiveItemBorderColor}; - background: ${paginationActiveItemBg}; - box-shadow: inset 0 0 0 1px ${paginationAccentBorderColor}; - } - .${gridId} .ant-pagination .ant-pagination-item-active a { - color: ${paginationActiveItemTextColor}; - } - .${gridId} .ant-pagination .ant-pagination-disabled, - .${gridId} .ant-pagination .ant-pagination-disabled:hover { - background: transparent; - border-color: ${paginationChipBorderColor}; - transform: none; - opacity: 0.42; - } - .${gridId} .ant-pagination .ant-pagination-jump-prev, - .${gridId} .ant-pagination .ant-pagination-jump-next { - padding: 0; - } - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - padding: 0; - margin: 0; - line-height: 1; - } - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-container, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-container { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - position: relative; - line-height: 1; - } - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-ellipsis, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-ellipsis, - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon { - position: absolute !important; - top: 0 !important; - right: 0 !important; - bottom: 0 !important; - left: 0 !important; - inset: 0 !important; - width: fit-content !important; - height: fit-content !important; - min-width: 0 !important; - min-height: 0 !important; - margin: auto !important; - padding: 0 !important; - transform: none !important; - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - line-height: 1 !important; - color: ${paginationSecondaryTextColor}; - } - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-ellipsis, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-ellipsis { - letter-spacing: 0.18em; - text-indent: 0.18em; - text-align: center; - } - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon .anticon, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon .anticon, - .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon svg, - .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon svg { - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - width: 1em; - height: 1em; - line-height: 1; - } - .${gridId} .data-grid-pagination-nav-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - font-size: 12px; - line-height: 1; - } - .${gridId} .data-grid-pagination-nav-icon .anticon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - } - .${gridId} .data-grid-pagination-jump { - display: inline-flex; - align-items: center; - gap: 6px; - height: 34px; - color: ${paginationSecondaryTextColor}; - font-size: 12px; - font-weight: 600; - white-space: nowrap; - } - .${gridId} .data-grid-pagination-jump-label { - color: ${paginationSecondaryTextColor}; - font-variant-numeric: tabular-nums; - } - .${gridId} .data-grid-pagination-jump-input, - .${gridId} .data-grid-pagination-jump-input.ant-input-number { - width: 64px; - min-width: 64px; - height: 34px; - display: inline-flex; - align-items: stretch; - } - .${gridId} .data-grid-pagination-jump-input .ant-input-number-input-wrap, - .${gridId} .data-grid-pagination-jump-input .ant-input-number-input { - height: 100%; - } - .${gridId} .data-grid-pagination-jump-input .ant-input-number-input { - padding: 0 10px; - text-align: center; - color: ${paginationPrimaryTextColor}; - font-weight: 600; - font-variant-numeric: tabular-nums; - line-height: 34px; - } - .${gridId} .data-grid-pagination-jump-input.ant-input-number { - border-radius: 12px; - border: 1px solid ${paginationChipBorderColor}; - background: ${paginationChipBg}; - box-shadow: none; - } - .${gridId} .data-grid-pagination-jump-button.ant-btn { - height: 34px; - min-width: 34px; - padding: 0 10px; - border-radius: 12px; - border-color: ${paginationChipBorderColor}; - background: ${paginationChipBg}; - color: ${paginationPrimaryTextColor}; - font-weight: 700; - box-shadow: none; - } - .${gridId} .data-grid-pagination-size-select { - width: 72px; - min-width: 72px; - max-width: 72px; - height: 34px; - display: inline-flex; - align-items: stretch; - } - .${gridId} .data-grid-pagination-size-select.ant-select-single, - .${gridId} .data-grid-pagination-size-select.ant-select-single.ant-select-sm { - width: 72px; - min-width: 72px; - max-width: 72px; - height: 34px; - } - .${gridId} .data-grid-pagination-size-select .ant-select-selector { - height: 34px !important; - border-radius: 12px !important; - border: 1px solid ${paginationChipBorderColor} !important; - background: ${paginationChipBg} !important; - box-shadow: none !important; - padding: 0 24px 0 10px !important; - display: flex !important; - align-items: center !important; - } - .${gridId} .data-grid-pagination-size-select .ant-select-selection-wrap { - display: flex !important; - align-items: center !important; - height: 100%; - } - .${gridId} .data-grid-pagination-size-select .ant-select-selection-search, - .${gridId} .data-grid-pagination-size-select .ant-select-selection-search-input { - height: 100% !important; - } - .${gridId} .data-grid-pagination-size-select .ant-select-selection-item, - .${gridId} .data-grid-pagination-size-select .ant-select-selection-placeholder { - display: flex; - align-items: center; - height: 100%; - line-height: 34px !important; - color: ${paginationPrimaryTextColor}; - font-weight: 600; - justify-content: flex-start; - font-variant-numeric: tabular-nums; - } - .${gridId} .data-grid-pagination-size-select .ant-select-selection-search { - inset-inline-start: 10px !important; - inset-inline-end: 24px !important; - } - .${gridId} .data-grid-pagination-size-select .ant-select-arrow { - color: ${paginationSecondaryTextColor}; - inset-inline-end: 10px; - top: 50%; - transform: translateY(-50%); - margin-top: 0; - display: inline-flex; - align-items: center; - justify-content: center; - height: 16px; - line-height: 1; - } - .${gridId} .data-grid-pagination-size-select .ant-select-arrow .anticon { - display: inline-flex; - align-items: center; - justify-content: center; - line-height: 1; - } - `; +type DataGridCssTextParams = Record; + +export const buildDataGridCssText = ({ + darkMode, + bgContent, + dataGridBackdropFilter, + dataTableVerticalBorderRule, + densityParams, + floatingScrollbarBottomOffset, + floatingScrollbarHeight, + floatingScrollbarInset, + floatingScrollbarThumbBg, + floatingScrollbarThumbBorderColor, + floatingScrollbarThumbHoverBg, + floatingScrollbarThumbShadow, + gridId, + horizontalScrollbarThumbBg, + horizontalScrollbarThumbBorderColor, + horizontalScrollbarThumbHoverBg, + horizontalScrollbarThumbShadow, + horizontalScrollbarTrackBg, + horizontalScrollbarTrackBorderColor, + horizontalScrollbarTrackShadow, + paginationAccentBg, + paginationAccentBorderColor, + paginationActiveItemBg, + paginationActiveItemBorderColor, + paginationActiveItemTextColor, + paginationChipBg, + paginationChipBorderColor, + paginationHoverBg, + paginationPrimaryTextColor, + paginationSecondaryTextColor, + paginationShellBg, + paginationShellBorderColor, + paginationShellShadow, + panelRadius, + rowAddedBg, + rowAddedHover, + rowModBg, + rowModHover, + selectionAccentHex, + selectionAccentRgb, + tableBodyBottomPadding, + useVirtualEditablePaintContain, + useVirtualEditableVisibilityHints, + useVirtualHolderPaintHints, + useVirtualRowCellContain, + verticalScrollbarTrackBg, +}: DataGridCssTextParams) => ` + + .${gridId} .data-grid-toolbar-scroll > * { + + flex-shrink: 0; + + } + + .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar { + + height: 7px; + + } + + .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar-thumb { + + background: ${darkMode ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.22)'}; + + border: 0; + + background-clip: border-box; + + border-radius: 999px; + + } + + .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar-thumb:hover { + + background: ${darkMode ? 'rgba(255,255,255,0.38)' : 'rgba(0,0,0,0.32)'}; + + border: 0; + + background-clip: border-box; + + } + + .${gridId} .data-grid-toolbar-scroll::-webkit-scrollbar-track { + + background: transparent; + + } + + .${gridId} .ant-table, + + .${gridId} .ant-table-wrapper, + + .${gridId} .ant-table-container { + + background: transparent !important; + + border-radius: ${panelRadius}px !important; + + } + + .${gridId} .ant-table-wrapper, + + .${gridId} .ant-table-container { + + border: none !important; + + overflow: hidden !important; + + } + + .${gridId} .ant-table-tbody > tr > td, + + .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell, + + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: ${dataTableVerticalBorderRule} !important; font-size: ${densityParams.dataFontSize}px !important; vertical-align: middle !important; } + + .${gridId} .ant-table-thead > tr > th { background: transparent !important; border-bottom: 1px solid ${darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'} !important; border-inline-end: ${dataTableVerticalBorderRule} !important; font-size: ${densityParams.dataFontSize}px !important; } + + .${gridId} .ant-table-tbody > tr > td:last-child, + + .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell:last-child, + + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell:last-child, + + .${gridId} .ant-table-thead > tr > th:last-child { + + border-inline-end-color: transparent !important; + + } + + /* 选择列对齐:header TH 无 class(Ant Design 虚拟模式),需用 :first-child 匹配 */ + + .${gridId} .ant-table-header th:first-child, + + .${gridId} .ant-table-thead > tr > th:first-child { + + text-align: center !important; + + padding-inline-start: 0 !important; + + padding-inline-end: 0 !important; + + padding-left: 0 !important; + + padding-right: 0 !important; + + } + + .${gridId} .ant-table-selection-column { + + vertical-align: middle !important; + + text-align: center !important; + + padding-inline-start: 0 !important; + + padding-inline-end: 0 !important; + + } + + .${gridId} .ant-table-selection-column .ant-checkbox-wrapper { + + display: inline-flex !important; + + align-items: center !important; + + justify-content: center !important; + + margin-right: 0 !important; + + } + + /* 窄表场景下 rc-table 会按视口等比放大选择列宽度,不能再额外锁死 header 宽度; + + 这里只统一 header/body 的内边距与对齐方式,避免第一列把后续数据列整体顶偏。 */ + + .${gridId} .ant-table-tbody > tr > td.ant-table-selection-column, + + .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column { + + text-align: center !important; + + vertical-align: middle !important; + + padding-inline-start: 0 !important; + + padding-inline-end: 0 !important; + + padding-left: 0 !important; + + padding-right: 0 !important; + + } + + .${gridId} .ant-table-tbody > tr > td.ant-table-selection-column .ant-checkbox-wrapper, + + .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell.ant-table-selection-column .ant-checkbox-wrapper { + + display: inline-flex !important; + + align-items: center !important; + + justify-content: center !important; + + margin-right: 0 !important; + + } + + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-selection-column { + + display: flex !important; + + align-items: center !important; + + justify-content: center !important; + + padding-inline-start: 0 !important; + + padding-inline-end: 0 !important; + + padding-left: 0 !important; + + padding-right: 0 !important; + + } + /* + * 固定列: + * - 表头:scrollLeft + sticky(保留全选 / 行号) + * - 虚拟表体:marginLeft + translateX 补偿 + * - 固定列表头 z-index 必须 > 列宽手柄(10),否则其他列拖拽/筛选图标会穿透进来 + */ + /* 普通表头压低层级,子元素(拖拽区/缩放条)不得盖过固定列 */ + .${gridId} .ant-table-header .ant-table-thead > tr > th { + z-index: 1; + } + .${gridId} .ant-table-header .ant-table-thead > tr > th .react-resizable-handle, + .${gridId} .ant-table-header .ant-table-thead > tr > th .sortable-header-cell-drag-handle { + z-index: 1 !important; + } + /* + * 固定列实心底:必须跟「非固定列透出的表格底」一致。 + * V2:ant-table 用 --gn-bg-panel / thead 用 --gn-bg-panel-2(见 v2-theme.css) + * 非 V2:回退 bgContent(colorBgContainer) + * 禁止再写死 #141414 / #1f1f1d 一类色值。 + */ + /* —— 表头固定 —— */ + .${gridId} .ant-table-header .ant-table-thead > tr > th.ant-table-cell-fix-left, + .${gridId} .ant-table-header .ant-table-thead > tr > th.ant-table-cell-fix-right, + .${gridId} .ant-table-header .ant-table-thead > tr > th.ant-table-selection-column, + .${gridId} .ant-table-header th.ant-table-cell-fix-left, + .${gridId} .ant-table-header th.ant-table-cell-fix-right, + .${gridId} .ant-table-header th.ant-table-selection-column, + .${gridId} .ant-table-header th.data-grid-row-number-cell, + .${gridId} .ant-table-thead > tr > th.ant-table-cell-fix-left, + .${gridId} .ant-table-thead > tr > th.ant-table-cell-fix-right, + .${gridId} .ant-table-thead > tr > th.ant-table-selection-column, + .${gridId} .ant-table-thead > tr > th.data-grid-row-number-cell { + position: sticky !important; + z-index: 30 !important; + /* 与非固定表头一致:本网格 thead 被设为 transparent,透出 .ant-table 的 --gn-bg-panel */ + background: var(--gn-bg-panel, ${bgContent}) !important; + background-clip: padding-box !important; + transform: none !important; + overflow: hidden !important; + isolation: isolate; + } + .${gridId} .ant-table-header .ant-table-thead > tr > th:first-child, + .${gridId} .ant-table-header th:first-child, + .${gridId} .ant-table-header th.ant-table-selection-column, + .${gridId} .ant-table-thead > tr > th:first-child, + .${gridId} .ant-table-thead > tr > th.ant-table-selection-column { + position: sticky !important; + left: 0 !important; + z-index: 32 !important; + /* 全选列与钉住列/行号同色;覆盖 v2-theme 里可能的 panel-2 */ + background: var(--gn-bg-panel, ${bgContent}) !important; + transform: none !important; + overflow: hidden !important; + isolation: isolate; + } + .${gridId} .ant-table-header .ant-table-thead > tr > th.data-grid-row-number-cell, + .${gridId} .ant-table-header th.data-grid-row-number-cell { + left: 46px !important; + z-index: 31 !important; + } + .${gridId} .ant-table-header th.ant-table-selection-column .ant-checkbox-wrapper, + .${gridId} .ant-table-header th:first-child .ant-checkbox-wrapper, + .${gridId} .ant-table-header .ant-table-selection { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + visibility: visible !important; + opacity: 1 !important; + position: relative; + z-index: 1; + } + .${gridId} .ant-table-header th.data-grid-row-number-cell, + .${gridId} .ant-table-header th.data-grid-row-number-cell * { + visibility: visible !important; + opacity: 1 !important; + } + /* —— 表体固定:与 .ant-table 的 --gn-bg-panel 同色 —— */ + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-left-first, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-left-last, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-selection-column, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-cell-fix-left-first, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-cell-fix-left-last, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-selection-column, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-right-first, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-right-last, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-cell-fix-right-first, + .${gridId} .ant-table-tbody-virtual .ant-table-row > .ant-table-cell.ant-table-cell-fix-right-last { + position: relative !important; + left: auto !important; + right: auto !important; + transform: translate3d(var(--gn-datagrid-h-scroll, 0px), 0, 0) !important; + will-change: transform; + z-index: 4 !important; + background: var(--gn-bg-panel, ${bgContent}) !important; + background-clip: padding-box !important; + overflow: hidden !important; + } + .${gridId} .ant-table-tbody > tr > td.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody > tr > td.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody > tr > td.ant-table-selection-column { + position: sticky !important; + z-index: 4 !important; + background: var(--gn-bg-panel, ${bgContent}) !important; + background-clip: padding-box !important; + transform: none !important; + } + .${gridId} .ant-table-header .ant-table-thead > tr > th.ant-table-cell-fix-left-last, + .${gridId} .ant-table-header th.data-grid-row-number-cell, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-left-last { + box-shadow: ${darkMode + ? '4px 0 6px -2px rgba(0,0,0,0.45)' + : '4px 0 6px -2px rgba(15, 23, 42, 0.16)'} !important; + } + .${gridId} .ant-table-header .ant-table-thead > tr > th.ant-table-cell-fix-right-first, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-fix-right-first { + box-shadow: ${darkMode + ? '-4px 0 6px -2px rgba(0,0,0,0.45)' + : '-4px 0 6px -2px rgba(15, 23, 42, 0.16)'} !important; + } + /* hover / 选中:与 v2-theme 的 --gn-bg-hover / --gn-bg-selected 对齐 */ + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row:hover > .ant-table-cell.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row:hover > .ant-table-cell.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row:hover > .ant-table-cell.ant-table-selection-column, + .${gridId} .ant-table-tbody > tr:hover > td.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody > tr:hover > td.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody > tr:hover > td.ant-table-selection-column { + background-color: var(--gn-bg-panel, ${bgContent}) !important; + background-image: linear-gradient( + var(--gn-bg-hover, ${darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)'}), + var(--gn-bg-hover, ${darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)'}) + ) !important; + } + /* 固定列选中:与整行同一绿色(实心底防透出滚动内容) */ + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected > .ant-table-cell.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected > .ant-table-cell.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected > .ant-table-cell.ant-table-selection-column, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-selection-column { + background: rgba(34, 197, 94, 0.14) !important; + background-color: rgba(34, 197, 94, 0.14) !important; + background-image: none !important; + } + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected:hover > .ant-table-cell.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected:hover > .ant-table-cell.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected:hover > .ant-table-cell.ant-table-selection-column, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td.ant-table-cell-fix-left, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td.ant-table-cell-fix-right, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td.ant-table-selection-column { + background: rgba(34, 197, 94, 0.18) !important; + background-color: rgba(34, 197, 94, 0.18) !important; + background-image: none !important; + } + .${gridId} .data-grid-row-number-cell { + text-align: center !important; + overflow: hidden !important; + } +.${gridId} .ant-table-thead > tr:first-child > th:first-child, + + .${gridId} .ant-table-header table > thead > tr:first-child > th:first-child { + + border-top-left-radius: ${panelRadius}px !important; + + } + + .${gridId} .ant-table-thead > tr:first-child > th:last-child, + + .${gridId} .ant-table-header table > thead > tr:first-child > th:last-child { + + border-top-right-radius: ${panelRadius}px !important; + + } + + .${gridId} .ant-table-body { + + border-bottom-left-radius: ${panelRadius}px !important; + + border-bottom-right-radius: ${panelRadius}px !important; + + } + + .${gridId} .ant-table-thead > tr > th::before { display: none !important; } + + .${gridId} .ant-table-thead > tr > th .ant-table-column-sorters { cursor: default !important; } + + .${gridId} .ant-table-thead > tr > th .ant-table-column-sorter, + + .${gridId} .ant-table-thead > tr > th .ant-table-column-sorter * { cursor: pointer !important; } + + .${gridId} .ant-table-tbody > tr:hover > td, + + .${gridId} .ant-table-tbody .ant-table-row:hover > .ant-table-cell { background-color: ${darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)'} !important; } + + /* + * 行选中:整行统一绿色。 + * 关键:virtual-holder 下有 + * .row > .ant-table-cell { background: transparent !important } + * 特异性 0,4,1。选中规则必须 ≥ 该特异性,否则非固定列选中无效, + * 只会透出底色(看起来像一半绿一半黄/棕)。 + */ + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected > .ant-table-cell, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected > .ant-table-cell.ant-table-cell-row-hover, + .${gridId} .ant-table-tbody-virtual .ant-table-row.ant-table-row-selected > .ant-table-cell, + .${gridId} .ant-table-tbody-virtual-holder-inner .ant-table-row.ant-table-row-selected > .ant-table-cell, + .${gridId} .ant-table-tbody .ant-table-row.ant-table-row-selected > .ant-table-cell, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected > td, + .${gridId} .ant-table-row.ant-table-row-selected > .ant-table-cell { + background: rgba(34, 197, 94, 0.14) !important; + background-color: rgba(34, 197, 94, 0.14) !important; + background-image: none !important; + } + + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected:hover > .ant-table-cell, + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row.ant-table-row-selected:hover > .ant-table-cell.ant-table-cell-row-hover, + .${gridId} .ant-table-tbody-virtual .ant-table-row.ant-table-row-selected:hover > .ant-table-cell, + .${gridId} .ant-table-tbody-virtual-holder-inner .ant-table-row.ant-table-row-selected:hover > .ant-table-cell, + .${gridId} .ant-table-tbody .ant-table-row.ant-table-row-selected:hover > .ant-table-cell, + .${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td, + .${gridId} .ant-table-row.ant-table-row-selected:hover > .ant-table-cell { + background: rgba(34, 197, 94, 0.18) !important; + background-color: rgba(34, 197, 94, 0.18) !important; + background-image: none !important; + } + + .${gridId} .row-added td, + + .${gridId} .row-added > .ant-table-cell { background-color: ${rowAddedBg} !important; color: ${darkMode ? '#e6fffb' : 'inherit'}; } + + .${gridId} .row-modified td, + + .${gridId} .row-modified > .ant-table-cell { background-color: ${rowModBg} !important; color: ${darkMode ? '#e6f7ff' : 'inherit'}; } + + .${gridId} .row-deleted td, + + .${gridId} .row-deleted > .ant-table-cell { background-color: ${darkMode ? '#1f1f1f' : '#f0f0f0'} !important; color: ${darkMode ? '#595959' : '#bfbfbf'} !important; text-decoration: line-through; } + + .${gridId} .ant-table-tbody > tr.row-added:hover > td, + + .${gridId} .ant-table-tbody .ant-table-row.row-added:hover > .ant-table-cell { background-color: ${rowAddedHover} !important; } + + .${gridId} .ant-table-tbody > tr.row-modified:hover > td, + + .${gridId} .ant-table-tbody .ant-table-row.row-modified:hover > .ant-table-cell { background-color: ${rowModHover} !important; } + + .${gridId} .ant-table-tbody > tr.row-deleted:hover > td, + + .${gridId} .ant-table-tbody .ant-table-row.row-deleted:hover > .ant-table-cell { background-color: ${darkMode ? '#2a2a2a' : '#e8e8e8'} !important; } + + .${gridId}.cell-edit-mode .ant-table-tbody > tr > td[data-col-name], + + .${gridId}.cell-edit-mode .ant-table-tbody .ant-table-row > .ant-table-cell[data-col-name] { user-select: none; -webkit-user-select: none; cursor: crosshair; } + + /* 单元格选区:与行选中/主题强调色统一为 V2 绿色,不用黄色 selectionAccent */ + .${gridId} .ant-table-tbody > tr > td[data-cell-selected="true"], + .${gridId} .ant-table-tbody .ant-table-row > .ant-table-cell[data-cell-selected="true"], + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell[data-cell-selected="true"], + .${gridId} [data-cell-selected="true"] { + box-shadow: inset 0 0 0 2px var(--gn-accent, #22c55e) !important; + background-color: transparent !important; + background-image: linear-gradient( + var(--gn-bg-selected, rgba(34, 197, 94, 0.14)), + var(--gn-bg-selected, rgba(34, 197, 94, 0.14)) + ) !important; + } + + .${gridId} .ant-table-content, + + .${gridId} .ant-table-body { + + scrollbar-gutter: stable; + + } + + .${gridId} .ant-table-body { + + padding-bottom: ${tableBodyBottomPadding}px; + + box-sizing: border-box; + + scroll-padding-bottom: ${tableBodyBottomPadding}px; + + contain: layout paint style; + + } + + .${gridId} .ant-table-tbody-virtual-holder, + + .${gridId} .rc-virtual-list-holder { + + padding-bottom: ${tableBodyBottomPadding}px; + + box-sizing: border-box; + + scroll-padding-bottom: ${tableBodyBottomPadding}px; + + contain: ${useVirtualHolderPaintHints ? 'layout paint style' : 'layout style'}; + + content-visibility: ${useVirtualHolderPaintHints ? 'auto' : 'visible'}; + + } + + .${gridId} .ant-table-tbody-virtual-holder-inner { + + padding-bottom: ${tableBodyBottomPadding}px; + + box-sizing: border-box; + + contain: ${useVirtualHolderPaintHints ? 'layout paint style' : 'layout style'}; + + } + + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row, + + .${gridId} .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { + + contain: ${useVirtualRowCellContain ? 'layout paint style' : 'none'}; + + } + + .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { + + overflow: hidden; + + text-overflow: ellipsis; + + white-space: nowrap; + + } + + .${gridId}.gn-v2-data-grid .ant-table-tbody > tr > td, + + .${gridId}.gn-v2-data-grid .ant-table-tbody .ant-table-row > .ant-table-cell, + + .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell { + + vertical-align: middle !important; + + } + + .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.ant-table-cell-row-hover, + + .${gridId}.gn-v2-data-grid .ant-table-tbody-virtual-holder .ant-table-row > .ant-table-cell.data-grid-virtual-inline-editing { + + overflow: visible; + + text-overflow: clip; + + white-space: normal; + + } + + .${gridId} .data-grid-table-wrap { + + width: 100%; + + max-width: 100%; + + overflow: hidden; + + } + + .${gridId} .ant-table-sticky-scroll { + + display: none !important; + + } + + .${gridId} .data-grid-find-highlight { + + padding: 0 1px; + + border-radius: 3px; + + background: ${darkMode ? 'rgba(246, 196, 83, 0.42)' : 'rgba(255, 193, 7, 0.42)'}; + + color: inherit; + + } + + .${gridId} .editable-cell-value-wrap { + + display: block; + + width: 100%; + + min-width: 0; + + min-height: 20px; + + padding-right: 0; + + position: relative; + + contain: ${useVirtualEditablePaintContain ? 'layout paint style' : 'layout style'}; + + } + + .${gridId} .editable-cell-value-wrap > * { + + min-width: 0; + + } + + .${gridId} .data-grid-inline-editor-form-item, + + .${gridId} .data-grid-inline-editor-form-item .ant-form-item-row, + + .${gridId} .data-grid-inline-editor-form-item .ant-form-item-control, + + .${gridId} .data-grid-inline-editor-form-item .ant-form-item-control-input, + + .${gridId} .data-grid-inline-editor-form-item .ant-form-item-control-input-content { + + width: 100%; + + min-width: 0; + + } + + .${gridId} .data-grid-inline-editor-input, + + .${gridId} .data-grid-inline-editor-form-item .ant-picker { + + width: 100% !important; + + min-width: 0; + + } + + .${gridId} .ant-table-tbody-virtual-holder .editable-cell-value-wrap { + + content-visibility: ${useVirtualEditableVisibilityHints ? 'auto' : 'visible'}; + + contain-intrinsic-size: ${useVirtualEditableVisibilityHints ? '24px 160px' : 'auto'}; + + } + + /* 虚拟表列对齐: + * - 去掉 min-width:100%,避免少列时 header 被强行拉到视口宽 + * - 保留 rc-table FixedHolder 写入的 width:scrollX,保证 header.scrollLeft 可滚 + * (固定列依赖 header 真实 scrollLeft + sticky,不能改成 marginLeft) + */ + .${gridId} .ant-table-header > table { + min-width: 0 !important; + } + /* 外部滚动条激活时 header 仍需可被程序设置 scrollLeft(overflow:hidden 也允许) */ + .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-header { + overflow: hidden !important; + } + + .${gridId} .ant-table-tbody-virtual-scrollbar.ant-table-tbody-virtual-scrollbar-horizontal { + + display: none !important; + + } + + .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-content { + + overflow-x: hidden !important; + + } + + .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-body { + + overflow-x: hidden !important; + + overflow-y: auto !important; + + } + + .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .ant-table-tbody-virtual-holder, + + .${gridId} .data-grid-table-wrap.data-grid-table-wrap-external-active .rc-virtual-list-holder { + + overflow-x: hidden !important; + + } + + .${gridId} .ant-table-body { + + scrollbar-width: thin; + + scrollbar-color: ${floatingScrollbarThumbBg} transparent; + + } + + .${gridId} .ant-table-body::-webkit-scrollbar { + + width: ${floatingScrollbarHeight}px; + + height: 0; + + } + + .${gridId} .ant-table-body::-webkit-scrollbar-track { + + background: ${verticalScrollbarTrackBg}; + + margin: 8px 0; + + border-radius: 999px; + + } + + .${gridId} .ant-table-body::-webkit-scrollbar-thumb { + + background: ${floatingScrollbarThumbBg}; + + border: 1px solid ${floatingScrollbarThumbBorderColor}; + + background-clip: border-box; + + border-radius: 999px; + + box-shadow: ${floatingScrollbarThumbShadow}; + + } + + .${gridId} .ant-table-body::-webkit-scrollbar-thumb:hover { + + background: ${floatingScrollbarThumbHoverBg}; + + border: 1px solid ${floatingScrollbarThumbBorderColor}; + + background-clip: border-box; + + box-shadow: ${floatingScrollbarThumbShadow}; + + } + + .${gridId} .rc-virtual-list-holder { + + scrollbar-width: thin; + + scrollbar-color: ${floatingScrollbarThumbBg} transparent; + + } + + .${gridId} .rc-virtual-list-holder::-webkit-scrollbar { + + width: ${floatingScrollbarHeight}px; + + height: 0; + + } + + .${gridId} .rc-virtual-list-holder::-webkit-scrollbar-track { + + background: ${verticalScrollbarTrackBg}; + + margin: 8px 0; + + border-radius: 999px; + + } + + .${gridId} .rc-virtual-list-holder::-webkit-scrollbar-thumb { + + background: ${floatingScrollbarThumbBg}; + + border: 1px solid ${floatingScrollbarThumbBorderColor}; + + background-clip: border-box; + + border-radius: 999px; + + box-shadow: ${floatingScrollbarThumbShadow}; + + } + + .${gridId} .rc-virtual-list-holder::-webkit-scrollbar-thumb:hover { + + background: ${floatingScrollbarThumbHoverBg}; + + border: 1px solid ${floatingScrollbarThumbBorderColor}; + + background-clip: border-box; + + box-shadow: ${floatingScrollbarThumbShadow}; + + } + + .${gridId} .data-grid-external-horizontal-scroll { + + position: absolute; + + left: ${floatingScrollbarInset}px; + + right: ${floatingScrollbarInset}px; + + bottom: ${floatingScrollbarBottomOffset}px; + + height: ${floatingScrollbarHeight + 4}px; + + overflow-x: auto; + + overflow-y: hidden; + + background: transparent; + + z-index: 24; + + } + + .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar { + + height: ${floatingScrollbarHeight}px; + + } + + .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar-track { + + background: ${horizontalScrollbarTrackBg}; + + border: 1px solid ${horizontalScrollbarTrackBorderColor}; + + border-radius: 999px; + + box-shadow: ${horizontalScrollbarTrackShadow}; + + } + + .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar-thumb { + + background: ${horizontalScrollbarThumbBg}; + + border: 1px solid ${horizontalScrollbarThumbBorderColor}; + + background-clip: border-box; + + border-radius: 999px; + + box-shadow: ${horizontalScrollbarThumbShadow}; + + } + + .${gridId} .data-grid-external-horizontal-scroll::-webkit-scrollbar-thumb:hover { + + background: ${horizontalScrollbarThumbHoverBg}; + + border: 1px solid ${horizontalScrollbarThumbBorderColor}; + + background-clip: border-box; + + box-shadow: ${horizontalScrollbarThumbShadow}; + + } + + .${gridId} .data-grid-external-horizontal-scroll-inner { + + height: 1px; + + } + + .${gridId} .data-grid-pagination-shell { + + display: inline-flex; + + align-items: center; + + justify-content: flex-end; + + gap: 10px; + + flex-wrap: wrap; + + max-width: 100%; + + padding: 8px 10px; + + border-radius: 16px; + + border: 1px solid ${paginationShellBorderColor}; + + background: ${paginationShellBg}; + + box-shadow: ${paginationShellShadow}; + + backdrop-filter: ${dataGridBackdropFilter}; + + -webkit-backdrop-filter: ${dataGridBackdropFilter}; + + } + + .${gridId} .data-grid-pagination-summary, + + .${gridId} .data-grid-pagination-page-chip { + + display: inline-flex; + + align-items: center; + + gap: 8px; + + min-height: 34px; + + padding: 0 12px; + + border-radius: 999px; + + border: 1px solid ${paginationChipBorderColor}; + + background: ${paginationChipBg}; + + color: ${paginationPrimaryTextColor}; + + font-size: 12px; + + line-height: 1; + + font-variant-numeric: tabular-nums; + + white-space: nowrap; + + } + + .${gridId} .data-grid-pagination-kicker { + + display: inline-flex; + + align-items: center; + + height: 20px; + + padding: 0 8px; + + border-radius: 999px; + + background: ${paginationAccentBg}; + + border: 1px solid ${paginationAccentBorderColor}; + + color: ${paginationActiveItemTextColor}; + + font-size: 11px; + + font-weight: 700; + + letter-spacing: 0.02em; + + } + + .${gridId} .data-grid-pagination-summary-value { + + color: ${paginationPrimaryTextColor}; + + font-weight: 600; + + font-variant-numeric: tabular-nums; + + } + + .${gridId} .data-grid-pagination-page-chip { + + color: ${paginationSecondaryTextColor}; + + font-weight: 600; + + } + + .${gridId} .ant-pagination { + + display: inline-flex; + + align-items: center; + + gap: 6px; + + margin: 0; + + color: ${paginationPrimaryTextColor}; + + } + + .${gridId} .ant-pagination .ant-pagination-item, + + .${gridId} .ant-pagination .ant-pagination-prev, + + .${gridId} .ant-pagination .ant-pagination-next, + + .${gridId} .ant-pagination .ant-pagination-jump-prev, + + .${gridId} .ant-pagination .ant-pagination-jump-next { + + min-width: 34px; + + height: 34px; + + margin-inline-end: 0; + + border-radius: 12px; + + border: 1px solid ${paginationChipBorderColor}; + + background: ${paginationChipBg}; + + box-shadow: none; + + display: inline-flex; + + align-items: center; + + justify-content: center; + + overflow: hidden; + + transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease, box-shadow 160ms ease; + + } + + .${gridId} .ant-pagination .ant-pagination-item a, + + .${gridId} .ant-pagination .ant-pagination-prev .ant-pagination-item-link, + + .${gridId} .ant-pagination .ant-pagination-next .ant-pagination-item-link, + + .${gridId} .ant-pagination .ant-pagination-prev > *, + + .${gridId} .ant-pagination .ant-pagination-next > * { + + display: flex; + + align-items: center; + + justify-content: center; + + width: 100%; + + height: 100%; + + color: ${paginationPrimaryTextColor}; + + font-weight: 600; + + border: none; + + background: transparent; + + border-radius: inherit; + + line-height: 1; + + } + + .${gridId} .ant-pagination .ant-pagination-item:hover, + + .${gridId} .ant-pagination .ant-pagination-prev:hover, + + .${gridId} .ant-pagination .ant-pagination-next:hover { + + background: ${paginationHoverBg}; + + border-color: ${paginationActiveItemBorderColor}; + + transform: translateY(-1px); + + } + + .${gridId} .ant-pagination .ant-pagination-item-active { + + border-color: ${paginationActiveItemBorderColor}; + + background: ${paginationActiveItemBg}; + + box-shadow: inset 0 0 0 1px ${paginationAccentBorderColor}; + + } + + .${gridId} .ant-pagination .ant-pagination-item-active a { + + color: ${paginationActiveItemTextColor}; + + } + + .${gridId} .ant-pagination .ant-pagination-disabled, + + .${gridId} .ant-pagination .ant-pagination-disabled:hover { + + background: transparent; + + border-color: ${paginationChipBorderColor}; + + transform: none; + + opacity: 0.42; + + } + + .${gridId} .ant-pagination .ant-pagination-jump-prev, + + .${gridId} .ant-pagination .ant-pagination-jump-next { + + padding: 0; + + } + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link { + + position: relative; + + display: flex; + + align-items: center; + + justify-content: center; + + width: 100%; + + height: 100%; + + padding: 0; + + margin: 0; + + line-height: 1; + + } + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-container, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-container { + + display: flex; + + align-items: center; + + justify-content: center; + + width: 100%; + + height: 100%; + + position: relative; + + line-height: 1; + + } + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-ellipsis, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-ellipsis, + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon { + + position: absolute !important; + + top: 0 !important; + + right: 0 !important; + + bottom: 0 !important; + + left: 0 !important; + + inset: 0 !important; + + width: fit-content !important; + + height: fit-content !important; + + min-width: 0 !important; + + min-height: 0 !important; + + margin: auto !important; + + padding: 0 !important; + + transform: none !important; + + display: inline-flex !important; + + align-items: center !important; + + justify-content: center !important; + + line-height: 1 !important; + + color: ${paginationSecondaryTextColor}; + + } + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-ellipsis, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-ellipsis { + + letter-spacing: 0.18em; + + text-indent: 0.18em; + + text-align: center; + + } + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon .anticon, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon .anticon, + + .${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon svg, + + .${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon svg { + + display: inline-flex !important; + + align-items: center !important; + + justify-content: center !important; + + width: 1em; + + height: 1em; + + line-height: 1; + + } + + .${gridId} .data-grid-pagination-nav-icon { + + display: inline-flex; + + align-items: center; + + justify-content: center; + + width: 100%; + + height: 100%; + + font-size: 12px; + + line-height: 1; + + } + + .${gridId} .data-grid-pagination-nav-icon .anticon { + + display: inline-flex; + + align-items: center; + + justify-content: center; + + width: 100%; + + height: 100%; + + } + + .${gridId} .data-grid-pagination-jump { + + display: inline-flex; + + align-items: center; + + gap: 6px; + + height: 34px; + + color: ${paginationSecondaryTextColor}; + + font-size: 12px; + + font-weight: 600; + + white-space: nowrap; + + } + + .${gridId} .data-grid-pagination-jump-label { + + color: ${paginationSecondaryTextColor}; + + font-variant-numeric: tabular-nums; + + } + + .${gridId} .data-grid-pagination-jump-input, + + .${gridId} .data-grid-pagination-jump-input.ant-input-number { + + width: 64px; + + min-width: 64px; + + height: 34px; + + display: inline-flex; + + align-items: stretch; + + } + + .${gridId} .data-grid-pagination-jump-input .ant-input-number-input-wrap, + + .${gridId} .data-grid-pagination-jump-input .ant-input-number-input { + + height: 100%; + + } + + .${gridId} .data-grid-pagination-jump-input .ant-input-number-input { + + padding: 0 10px; + + text-align: center; + + color: ${paginationPrimaryTextColor}; + + font-weight: 600; + + font-variant-numeric: tabular-nums; + + line-height: 34px; + + } + + .${gridId} .data-grid-pagination-jump-input.ant-input-number { + + border-radius: 12px; + + border: 1px solid ${paginationChipBorderColor}; + + background: ${paginationChipBg}; + + box-shadow: none; + + } + + .${gridId} .data-grid-pagination-jump-button.ant-btn { + + height: 34px; + + min-width: 34px; + + padding: 0 10px; + + border-radius: 12px; + + border-color: ${paginationChipBorderColor}; + + background: ${paginationChipBg}; + + color: ${paginationPrimaryTextColor}; + + font-weight: 700; + + box-shadow: none; + + } + + .${gridId} .data-grid-pagination-size-select { + + width: 72px; + + min-width: 72px; + + max-width: 72px; + + height: 34px; + + display: inline-flex; + + align-items: stretch; + + } + + .${gridId} .data-grid-pagination-size-select.ant-select-single, + + .${gridId} .data-grid-pagination-size-select.ant-select-single.ant-select-sm { + + width: 72px; + + min-width: 72px; + + max-width: 72px; + + height: 34px; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-selector { + + height: 34px !important; + + border-radius: 12px !important; + + border: 1px solid ${paginationChipBorderColor} !important; + + background: ${paginationChipBg} !important; + + box-shadow: none !important; + + padding: 0 24px 0 10px !important; + + display: flex !important; + + align-items: center !important; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-selection-wrap { + + display: flex !important; + + align-items: center !important; + + height: 100%; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-selection-search, + + .${gridId} .data-grid-pagination-size-select .ant-select-selection-search-input { + + height: 100% !important; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-selection-item, + + .${gridId} .data-grid-pagination-size-select .ant-select-selection-placeholder { + + display: flex; + + align-items: center; + + height: 100%; + + line-height: 34px !important; + + color: ${paginationPrimaryTextColor}; + + font-weight: 600; + + justify-content: flex-start; + + font-variant-numeric: tabular-nums; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-selection-search { + + inset-inline-start: 10px !important; + + inset-inline-end: 24px !important; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-arrow { + + color: ${paginationSecondaryTextColor}; + + inset-inline-end: 10px; + + top: 50%; + + transform: translateY(-50%); + + margin-top: 0; + + display: inline-flex; + + align-items: center; + + justify-content: center; + + height: 16px; + + line-height: 1; + + } + + .${gridId} .data-grid-pagination-size-select .ant-select-arrow .anticon { + + display: inline-flex; + + align-items: center; + + justify-content: center; + + line-height: 1; + + } + + `; diff --git a/frontend/src/components/useDataGridColumnResize.ts b/frontend/src/components/useDataGridColumnResize.ts index ce7a733c..5533afcb 100644 --- a/frontend/src/components/useDataGridColumnResize.ts +++ b/frontend/src/components/useDataGridColumnResize.ts @@ -1,7 +1,11 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { resolveDataTableColumnWidth } from '../utils/dataGridDisplay'; import { calculateAutoFitColumnWidth } from './dataGridAutoWidth'; -import { DEFAULT_GRID_MONO_FONT_FAMILY } from './DataGridCore'; +import { DEFAULT_GRID_MONO_FONT_FAMILY, GONAVI_ROW_NUMBER_COLUMN_KEY } from './DataGridCore'; + +const ROW_NUMBER_DEFAULT_WIDTH = 36; +const ROW_NUMBER_MIN_WIDTH = 28; +const ROW_NUMBER_MAX_WIDTH = 120; type UseDataGridColumnResizeContext = Record; @@ -48,10 +52,14 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => isResizingRef.current = true; const startX = e.clientX; - const currentWidth = resolveDataTableColumnWidth({ - manualWidth: columnWidths[key], - density: dataTableDensity, - }); + // 序号列默认宽度与数据列不同,不能走 density 默认列宽 + const isRowNumberColumn = key === GONAVI_ROW_NUMBER_COLUMN_KEY; + const currentWidth = isRowNumberColumn + ? (typeof columnWidths[key] === 'number' && columnWidths[key] > 0 ? columnWidths[key] : ROW_NUMBER_DEFAULT_WIDTH) + : resolveDataTableColumnWidth({ + manualWidth: columnWidths[key], + density: dataTableDensity, + }); const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0; draggingRef.current = { startX, startWidth: currentWidth, key, containerLeft }; latestClientXRef.current = startX; @@ -161,10 +169,15 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => const handleResizeAutoFit = useCallback((key: string) => (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); + // 序号列双击还原默认窄宽,不按数据内容撑开 + if (key === GONAVI_ROW_NUMBER_COLUMN_KEY) { + setColumnWidths((prev: Record) => ({ ...prev, [key]: ROW_NUMBER_DEFAULT_WIDTH })); + return; + } const handleEl = e.currentTarget as HTMLElement | null; const headerEl = handleEl?.closest('th') as HTMLElement | null; autoFitColumnWidth(key, headerEl); - }, [autoFitColumnWidth]); + }, [autoFitColumnWidth, setColumnWidths]); const handleResizeMove = useCallback((e: MouseEvent) => { if (!draggingRef.current) return; @@ -178,7 +191,10 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => const { startX, startWidth, key } = draggingRef.current; const deltaX = e.clientX - startX; - const newWidth = Math.max(50, startWidth + deltaX); + const isRowNumberColumn = key === GONAVI_ROW_NUMBER_COLUMN_KEY; + const minWidth = isRowNumberColumn ? ROW_NUMBER_MIN_WIDTH : 50; + const maxWidth = isRowNumberColumn ? ROW_NUMBER_MAX_WIDTH : Number.POSITIVE_INFINITY; + const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidth + deltaX)); setColumnWidths((prev: Record) => ({ ...prev, [key]: newWidth })); diff --git a/frontend/src/components/useDataGridV2Actions.ts b/frontend/src/components/useDataGridV2Actions.ts index a88facc2..01290ed5 100644 --- a/frontend/src/components/useDataGridV2Actions.ts +++ b/frontend/src/components/useDataGridV2Actions.ts @@ -102,6 +102,8 @@ export const useDataGridV2Actions = (ctx: DataGridV2ActionsContext) => { supportsSqlQueryExport, tableName, toggleColumnVisibility, + pinnedLeftColumnNames, + setTablePinnedLeftColumns, translateDataGrid, uniqueKeyGroups, withSortBufferTuningSQL, @@ -134,6 +136,24 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade case 'auto-fit-column': autoFitColumnWidth(columnName); break; + case 'pin-column-left': { + if (!connectionId || !dbName || !tableName) { + void message.info(translateDataGrid('data_grid.message.pin_column_unavailable')); + break; + } + const nextPinned = [ + ...pinnedLeftColumnNames.filter((col: string) => col !== columnName), + columnName, + ]; + setTablePinnedLeftColumns(connectionId, dbName, tableName, nextPinned); + break; + } + case 'unpin-column-left': { + if (!connectionId || !dbName || !tableName) break; + const nextPinned = pinnedLeftColumnNames.filter((col: string) => col !== columnName); + setTablePinnedLeftColumns(connectionId, dbName, tableName, nextPinned); + break; + } case 'hide-column': if (displayColumnNames.length <= 1) { void message.info(translateDataGrid('data_grid.message.keep_one_visible_column')); @@ -162,10 +182,15 @@ const handleV2ColumnHeaderContextMenuAction = useCallback((action: V2ColumnHeade autoFitColumnWidth, cellContextMenu.dataIndex, cellContextMenu.title, + connectionId, copyToClipboard, + dbName, displayColumnNames.length, handleCopyColumnData, + pinnedLeftColumnNames, setQueryOptions, + setTablePinnedLeftColumns, + tableName, translateDataGrid, toggleColumnVisibility, ]); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 3ec5adca..797f95f2 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1374,6 +1374,8 @@ interface AppState { tableSortPreference: Record; tableColumnOrders: Record; enableColumnOrderMemory: boolean; + /** 数据表横向滚动时左侧固定的数据列(按表维度记忆;勾选列/行号列始终固定) */ + tablePinnedLeftColumns: Record; tableHiddenColumns: Record; enableHiddenColumnMemory: boolean; pinnedSidebarTables: string[]; @@ -1566,6 +1568,17 @@ interface AppState { dbName: string, tableName: string, ) => void; + setTablePinnedLeftColumns: ( + connectionId: string, + dbName: string, + tableName: string, + columns: string[], + ) => void; + clearTablePinnedLeftColumns: ( + connectionId: string, + dbName: string, + tableName: string, + ) => void; setTableHiddenColumns: ( connectionId: string, @@ -2659,6 +2672,7 @@ export const useStore = create()( tableSortPreference: {}, tableColumnOrders: {}, enableColumnOrderMemory: true, + tablePinnedLeftColumns: {}, tableHiddenColumns: {}, enableHiddenColumnMemory: true, pinnedSidebarTables: [], @@ -4023,6 +4037,33 @@ export const useStore = create()( setEnableColumnOrderMemory: (enabled) => set({ enableColumnOrderMemory: !!enabled }), + setTablePinnedLeftColumns: (connectionId, dbName, tableName, columns) => + set((state) => { + const key = `${connectionId}-${dbName}-${tableName}`; + const normalized = Array.isArray(columns) + ? Array.from(new Set(columns.map((col) => String(col || "").trim()).filter(Boolean))) + : []; + if (normalized.length === 0) { + const next = { ...state.tablePinnedLeftColumns }; + delete next[key]; + return { tablePinnedLeftColumns: next }; + } + return { + tablePinnedLeftColumns: { + ...state.tablePinnedLeftColumns, + [key]: normalized, + }, + }; + }), + + clearTablePinnedLeftColumns: (connectionId, dbName, tableName) => + set((state) => { + const key = `${connectionId}-${dbName}-${tableName}`; + const next = { ...state.tablePinnedLeftColumns }; + delete next[key]; + return { tablePinnedLeftColumns: next }; + }), + setTableHiddenColumns: (connectionId, dbName, tableName, hiddenColumns) => set((state) => { const key = `${connectionId}-${dbName}-${tableName}`; @@ -4536,6 +4577,9 @@ export const useStore = create()( nextState.tableColumnOrders = safeOrders; nextState.enableColumnOrderMemory = state.enableColumnOrderMemory !== false; + nextState.tablePinnedLeftColumns = sanitizeTableColumnOrders( + state.tablePinnedLeftColumns, + ); const safeHidden = sanitizeTableHiddenColumns(state.tableHiddenColumns); nextState.tableHiddenColumns = safeHidden; nextState.enableHiddenColumnMemory = @@ -4619,6 +4663,9 @@ export const useStore = create()( ), tableColumnOrders: sanitizeTableColumnOrders(state.tableColumnOrders), enableColumnOrderMemory: state.enableColumnOrderMemory !== false, + tablePinnedLeftColumns: sanitizeTableColumnOrders( + state.tablePinnedLeftColumns, + ), tableHiddenColumns: sanitizeTableHiddenColumns( state.tableHiddenColumns, ), @@ -4689,6 +4736,7 @@ export const useStore = create()( tableSortPreference: state.tableSortPreference, tableColumnOrders: state.tableColumnOrders, enableColumnOrderMemory: state.enableColumnOrderMemory, + tablePinnedLeftColumns: state.tablePinnedLeftColumns, tableHiddenColumns: state.tableHiddenColumns, enableHiddenColumnMemory: state.enableHiddenColumnMemory, pinnedSidebarTables: state.pinnedSidebarTables, diff --git a/frontend/src/utils/dataGridClientFilter.test.ts b/frontend/src/utils/dataGridClientFilter.test.ts new file mode 100644 index 00000000..7e9c5133 --- /dev/null +++ b/frontend/src/utils/dataGridClientFilter.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { filterRowsByGridConditions } from './dataGridClientFilter'; + +describe('filterRowsByGridConditions', () => { + const rows = [ + { id: 1, name: 'Alice', status: 'active' }, + { id: 2, name: 'Bob', status: 'disabled' }, + { id: 3, name: 'Carol', status: null }, + ]; + + it('returns original rows when no active conditions', () => { + expect(filterRowsByGridConditions(rows, [])).toEqual(rows); + expect(filterRowsByGridConditions(rows, [{ column: 'name', op: 'CONTAINS', value: 'x', enabled: false }])).toEqual(rows); + }); + + it('filters by contains and equality', () => { + expect(filterRowsByGridConditions(rows, [ + { column: 'name', op: 'CONTAINS', value: 'a', enabled: true }, + ]).map((row) => row.id)).toEqual([1, 3]); + + expect(filterRowsByGridConditions(rows, [ + { column: 'id', op: '=', value: '2', enabled: true }, + ]).map((row) => row.id)).toEqual([2]); + }); + + it('supports null checks and OR logic', () => { + expect(filterRowsByGridConditions(rows, [ + { column: 'status', op: 'IS_NULL', enabled: true }, + ]).map((row) => row.id)).toEqual([3]); + + expect(filterRowsByGridConditions(rows, [ + { column: 'status', op: '=', value: 'active', enabled: true }, + { column: 'status', op: 'IS_NULL', logic: 'OR', enabled: true }, + ]).map((row) => row.id)).toEqual([1, 3]); + }); +}); diff --git a/frontend/src/utils/dataGridClientFilter.ts b/frontend/src/utils/dataGridClientFilter.ts new file mode 100644 index 00000000..271f683c --- /dev/null +++ b/frontend/src/utils/dataGridClientFilter.ts @@ -0,0 +1,170 @@ +import type { FilterCondition } from './sql'; +import { parseListValues } from './sql'; + +const normalizeCellText = (value: unknown): string => { + if (value === null || value === undefined) return ''; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +const isNullishCell = (value: unknown): boolean => value === null || value === undefined; + +const isEmptyCell = (value: unknown): boolean => { + if (isNullishCell(value)) return true; + if (typeof value === 'string') return value.length === 0; + return false; +}; + +const compareScalar = (cellText: string, expected: string): number => { + const leftNum = Number(cellText); + const rightNum = Number(expected); + if (cellText !== '' && expected !== '' && Number.isFinite(leftNum) && Number.isFinite(rightNum)) { + if (leftNum === rightNum) return 0; + return leftNum > rightNum ? 1 : -1; + } + return cellText.localeCompare(expected, undefined, { numeric: true, sensitivity: 'base' }); +}; + +const matchSingleCondition = (row: Record, condition: FilterCondition): boolean => { + if (condition?.enabled === false) return true; + + const op = String(condition?.op || '').trim(); + const column = String(condition?.column || '').trim(); + const value = String(condition?.value ?? ''); + const value2 = String(condition?.value2 ?? ''); + + // CUSTOM 表达式在结果页仅做客户端过滤时无法安全求值,跳过(视为通过) + if (op === 'CUSTOM') return true; + if (!column) return true; + + const cell = row?.[column]; + const cellText = normalizeCellText(cell); + + switch (op) { + case 'IS_NULL': + return isNullishCell(cell); + case 'IS_NOT_NULL': + return !isNullishCell(cell); + case 'IS_EMPTY': + return isEmptyCell(cell); + case 'IS_NOT_EMPTY': + return !isEmptyCell(cell); + case 'BETWEEN': { + const v1 = value.trim(); + const v2 = value2.trim(); + if (!v1 || !v2) return true; + return compareScalar(cellText, v1) >= 0 && compareScalar(cellText, v2) <= 0; + } + case 'NOT_BETWEEN': { + const v1 = value.trim(); + const v2 = value2.trim(); + if (!v1 || !v2) return true; + return !(compareScalar(cellText, v1) >= 0 && compareScalar(cellText, v2) <= 0); + } + case 'IN': { + const items = parseListValues(value); + if (items.length === 0) return true; + return items.some((item) => cellText === item); + } + case 'NOT_IN': { + const items = parseListValues(value); + if (items.length === 0) return true; + return items.every((item) => cellText !== item); + } + case 'CONTAINS': + case 'LIKE': { + const v = value.trim(); + if (!v) return true; + return cellText.toLowerCase().includes(v.toLowerCase()); + } + case 'NOT_CONTAINS': { + const v = value.trim(); + if (!v) return true; + return !cellText.toLowerCase().includes(v.toLowerCase()); + } + case 'STARTS_WITH': { + const v = value.trim(); + if (!v) return true; + return cellText.toLowerCase().startsWith(v.toLowerCase()); + } + case 'NOT_STARTS_WITH': { + const v = value.trim(); + if (!v) return true; + return !cellText.toLowerCase().startsWith(v.toLowerCase()); + } + case 'ENDS_WITH': { + const v = value.trim(); + if (!v) return true; + return cellText.toLowerCase().endsWith(v.toLowerCase()); + } + case 'NOT_ENDS_WITH': { + const v = value.trim(); + if (!v) return true; + return !cellText.toLowerCase().endsWith(v.toLowerCase()); + } + case '=': { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) === 0; + } + case '!=': { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) !== 0; + } + case '<': { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) < 0; + } + case '<=': { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) <= 0; + } + case '>': { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) > 0; + } + case '>=': { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) >= 0; + } + default: { + const v = value.trim(); + if (!v) return true; + return compareScalar(cellText, v) === 0; + } + } +}; + +/** + * 客户端过滤(查询结果页列头筛选等):按 AND/OR 逻辑匹配 FilterCondition 列表。 + * 逻辑语义与 buildWhereSQL 一致:第一个条件后,每个条件携带相对前序组合的 AND/OR。 + */ +export const filterRowsByGridConditions = >( + rows: T[], + conditions: FilterCondition[] | undefined, +): T[] => { + const active = (conditions || []).filter((cond) => cond && cond.enabled !== false); + if (active.length === 0) return rows; + + return rows.filter((row) => { + let result = matchSingleCondition(row, active[0]); + for (let i = 1; i < active.length; i += 1) { + const logic = String(active[i]?.logic || 'AND').trim().toUpperCase() === 'OR' ? 'OR' : 'AND'; + const matched = matchSingleCondition(row, active[i]); + result = logic === 'OR' ? (result || matched) : (result && matched); + } + return result; + }); +}; diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 5ba7bd40..4d53805c 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -3840,6 +3840,9 @@ "data_grid.context_menu.export_selected": "Ausgewählte Daten exportieren", "data_grid.context_menu.fill_to_selected_rows": "In ausgewählte Zeilen füllen ({{count}})", "data_grid.context_menu.hide_column": "Dieses Feld ausblenden", + "data_grid.context_menu.pin_column_left": "Links anheften", + "data_grid.context_menu.unpin_column_left": "Anheften aufheben", + "data_grid.message.pin_column_unavailable": "Fixierte Spalten können in dieser Ansicht nicht gespeichert werden", "data_grid.context_menu.hide_column_comment": "Feldkommentar ausblenden", "data_grid.context_menu.hide_column_type": "Feldtyp ausblenden", "data_grid.context_menu.paste_copied_columns": "Kopierte Spalten einfügen (gleiche Namen)", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 9e44ef27..6421a148 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -3840,6 +3840,9 @@ "data_grid.context_menu.export_selected": "Export selected data", "data_grid.context_menu.fill_to_selected_rows": "Fill to selected rows ({{count}})", "data_grid.context_menu.hide_column": "Hide this field", + "data_grid.context_menu.pin_column_left": "Pin to left", + "data_grid.context_menu.unpin_column_left": "Unpin column", + "data_grid.message.pin_column_unavailable": "Cannot remember pinned columns in this view", "data_grid.context_menu.hide_column_comment": "Hide field comment", "data_grid.context_menu.hide_column_type": "Hide field type", "data_grid.context_menu.paste_copied_columns": "Paste copied columns (same names)", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 625285ee..4304ad95 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -3840,6 +3840,9 @@ "data_grid.context_menu.export_selected": "選択データをエクスポート", "data_grid.context_menu.fill_to_selected_rows": "選択行へ入力({{count}})", "data_grid.context_menu.hide_column": "このフィールドを非表示", + "data_grid.context_menu.pin_column_left": "左に固定", + "data_grid.context_menu.unpin_column_left": "固定を解除", + "data_grid.message.pin_column_unavailable": "このビューでは列の固定を記憶できません", "data_grid.context_menu.hide_column_comment": "フィールドコメントを非表示", "data_grid.context_menu.hide_column_type": "フィールド型を非表示", "data_grid.context_menu.paste_copied_columns": "コピーした列を貼り付け(同名列)", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index e2f7146a..586c4b70 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -3840,6 +3840,9 @@ "data_grid.context_menu.export_selected": "Экспортировать выбранные данные", "data_grid.context_menu.fill_to_selected_rows": "Заполнить выбранные строки ({{count}})", "data_grid.context_menu.hide_column": "Скрыть это поле", + "data_grid.context_menu.pin_column_left": "Закрепить слева", + "data_grid.context_menu.unpin_column_left": "Открепить столбец", + "data_grid.message.pin_column_unavailable": "В этом представлении нельзя запомнить закреплённые столбцы", "data_grid.context_menu.hide_column_comment": "Скрыть комментарий поля", "data_grid.context_menu.hide_column_type": "Скрыть тип поля", "data_grid.context_menu.paste_copied_columns": "Вставить скопированные столбцы (с теми же именами)", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index cd4f89ab..850dc2ea 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -3840,6 +3840,9 @@ "data_grid.context_menu.export_selected": "导出选中数据", "data_grid.context_menu.fill_to_selected_rows": "填充到选中行({{count}})", "data_grid.context_menu.hide_column": "隐藏此字段", + "data_grid.context_menu.pin_column_left": "固定到左侧", + "data_grid.context_menu.unpin_column_left": "取消固定", + "data_grid.message.pin_column_unavailable": "当前视图无法记忆固定列", "data_grid.context_menu.hide_column_comment": "隐藏字段备注", "data_grid.context_menu.hide_column_type": "隐藏字段类型", "data_grid.context_menu.paste_copied_columns": "粘贴已复制列(同名列)", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index a985591c..b8ea1217 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -3840,6 +3840,9 @@ "data_grid.context_menu.export_selected": "匯出選取資料", "data_grid.context_menu.fill_to_selected_rows": "填入到選取列({{count}})", "data_grid.context_menu.hide_column": "隱藏此欄位", + "data_grid.context_menu.pin_column_left": "固定到左側", + "data_grid.context_menu.unpin_column_left": "取消固定", + "data_grid.message.pin_column_unavailable": "目前檢視無法記憶固定欄", "data_grid.context_menu.hide_column_comment": "隱藏欄位備註", "data_grid.context_menu.hide_column_type": "隱藏欄位型別", "data_grid.context_menu.paste_copied_columns": "貼上已複製欄位(同名欄位)",