🐛 fix(ui): 修复新版数据视图布局与 AI 面板加载容错

- 修复新版数据视图底部分页、列快速定位与当前页查找的对齐和压缩问题
- 优化窄屏下 AI 面板布局,避免挤压工作区并增加懒加载失败重试兜底
- 补充窗口运行时、AI 面板布局与 UI 回归测试,更新相关样式快照
This commit is contained in:
Syngnat
2026-05-28 07:05:48 +08:00
parent fac826b335
commit 8131ea8fc8
19 changed files with 1281 additions and 129 deletions

View File

@@ -246,6 +246,11 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
const pendingJVMPlanContextRef = useRef<JVMAIPlanContext | undefined>(undefined);
const pendingJVMDiagnosticPlanContextRef = useRef<JVMDiagnosticPlanContext | undefined>(undefined);
useEffect(() => {
setPanelWidth(width);
dragWidthRef.current = width;
}, [width]);
const aiChatHistory = useStore(state => state.aiChatHistory);
const aiActiveSessionId = useStore(state => state.aiActiveSessionId);
const appearance = useStore(state => state.appearance);

View File

@@ -91,13 +91,14 @@ describe('DataGrid layout', () => {
expect(markup).toContain('data-grid-secondary-actions="true"');
expect(markup).toContain('data-grid-view-switcher="true"');
expect(markup).toContain('data-grid-column-display-action="true"');
expect(markup).toContain('data-grid-column-quick-find-action="true"');
expect(markup).toContain('字段显示');
expect(markup).toContain('跳列');
expect(markup).toContain('data-grid-page-find="true"');
expect(markup).toContain('data-grid-page-find-prev="true"');
expect(markup).toContain('data-grid-page-find-next="true"');
expect(markup).not.toContain('gn-v2-data-grid-status-right');
expect(markup).not.toContain('gn-v2-data-grid-status-spacer');
expect(markup).toContain('gn-v2-data-grid-pagination-spacer');
expect(markup).toContain('gn-v2-data-grid-status-main');
expect(markup).toContain('gn-v2-data-grid-status-right');
expect(markup).toContain('data-grid-v2-pagination="true"');
expect(markup).toContain('data-grid-v2-page-chip="true"');
expect(markup).toContain('data-grid-v2-pagination-prev="true"');
@@ -367,8 +368,19 @@ describe('DataGrid layout', () => {
it('keeps DataGrid scroll synchronization throttled to animation frames', () => {
const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8');
const secondaryActionsSource = readFileSync(new URL('./DataGridSecondaryActions.tsx', import.meta.url), 'utf8');
const columnTitleSource = readFileSync(new URL('./DataGridColumnTitle.tsx', import.meta.url), 'utf8');
const columnQuickFindSource = readFileSync(new URL('./DataGridColumnQuickFind.tsx', import.meta.url), 'utf8');
const paginationBarSource = readFileSync(new URL('./DataGridPaginationBar.tsx', import.meta.url), 'utf8');
const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
expect(source).toContain('virtualHorizontalElementsRef');
expect(source).toContain('const handleSubmitColumnQuickFind = useCallback(() => {');
expect(source).toContain('resolveDataGridColumnQuickFindScrollLeft({');
expect(source).toContain('const applied = applyVirtualHorizontalOffset(tableContainer, nextScrollLeft);');
expect(source).toContain('syncExternalScrollFromTargets();');
expect(source).toContain("const columnQuickFindContent = isTableSurfaceActive ? (");
expect(secondaryActionsSource).toContain('data-grid-column-quick-find-action="true"');
expect(source).toContain('type VirtualTableScrollReference = TableReference & {');
expect(source).toContain('const tableRef = useRef<VirtualTableScrollReference | null>(null);');
expect(source).toContain('resolveDataGridHorizontalWheelDelta({');
@@ -406,6 +418,17 @@ describe('DataGrid layout', () => {
expect(source).toContain('scrollSnapshotRafRef.current = requestAnimationFrame');
expect(source).toContain("const dataGridBackdropFilter = isV2Ui || isMacLike ? 'none' : (opacity < 0.999 ? 'blur(14px)' : 'none');");
expect(source).toContain('rowHoverable={!enableVirtual}');
expect(columnTitleSource).toContain("data-grid-column-highlighted={highlighted ? 'true' : undefined}");
expect(columnTitleSource).toContain('data-column-name={normalizedName}');
expect(columnQuickFindSource).toContain('AutoComplete');
expect(columnQuickFindSource).toContain('placeholder="跳到字段列..."');
expect(secondaryActionsSource.indexOf('{pageFindContent}')).toBeLessThan(secondaryActionsSource.indexOf('gn-v2-data-grid-status-center'));
expect(css).toContain('width: 66px !important;');
expect(css).toContain('grid-template-columns: 160px 26px 26px !important;');
expect(css).toContain('.data-grid-pagination-size-select.ant-select-focused .ant-select-selector');
expect(css).toContain('overflow-x: auto;');
expect(paginationBarSource).toContain("label: `${value}/页`");
expect(css).toContain('background: transparent !important;');
});
it('keeps the DataGrid performance harness aligned with legacy and v2 comparison controls', () => {

View File

@@ -42,6 +42,7 @@ import {
calculateExternalHorizontalScrollInnerWidth,
calculateTableBodyBottomPadding,
calculateVirtualTableScrollX,
resolveDataGridColumnQuickFindScrollLeft,
resolveDataGridHorizontalWheelDelta,
} from './dataGridLayout';
import {
@@ -110,6 +111,7 @@ import {
} from './V2TableContextMenu';
import DataGridColumnTitle from './DataGridColumnTitle';
import DataGridColumnInfoPopoverContent from './DataGridColumnInfoPopoverContent';
import DataGridColumnQuickFind from './DataGridColumnQuickFind';
import DataGridPageFind from './DataGridPageFind';
import DataGridPaginationBar from './DataGridPaginationBar';
import DataGridResultViewSwitcher from './DataGridResultViewSwitcher';
@@ -1559,16 +1561,32 @@ const DataGrid: React.FC<DataGridProps> = ({
const [displayColumnNames, setDisplayColumnNames] = useState<string[]>([]);
const [localHiddenColumns, setLocalHiddenColumns] = useState<string[]>([]);
const [columnSearchText, setColumnSearchText] = useState('');
const [columnQuickFindText, setColumnQuickFindText] = useState('');
const [highlightedColumnName, setHighlightedColumnName] = useState('');
const [pageFindText, setPageFindText] = useState('');
const [activePageFindMatchIndex, setActivePageFindMatchIndex] = useState(-1);
const columnQuickFindHighlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const deferredPageFindText = useDeferredValue(pageFindText);
const deferredColumnQuickFindText = useDeferredValue(columnQuickFindText);
const normalizedPageFindText = useMemo(() => normalizeDataGridFindQuery(deferredPageFindText), [deferredPageFindText]);
const normalizedColumnQuickFindText = useMemo(
() => normalizeDataGridFindQuery(deferredColumnQuickFindText),
[deferredColumnQuickFindText],
);
useEffect(() => {
setColumnQuickFindText('');
setHighlightedColumnName('');
setPageFindText('');
setActivePageFindMatchIndex(-1);
}, [connectionId, dbName, tableName]);
useEffect(() => () => {
if (columnQuickFindHighlightTimerRef.current) {
clearTimeout(columnQuickFindHighlightTimerRef.current);
}
}, []);
// Sync hidden columns from store
useEffect(() => {
if (enableHiddenColumnMemory && connectionId && dbName && tableName) {
@@ -2349,10 +2367,11 @@ const DataGrid: React.FC<DataGridProps> = ({
columnMetaHintColor={columnMetaHintColor}
columnMetaTooltipColor={columnMetaTooltipColor}
darkMode={darkMode}
highlighted={highlightedColumnName === normalizedName}
onOpenForeignKey={foreignKeyTarget ? () => openForeignKeyTarget(foreignKeyTarget) : undefined}
/>
);
}, [columnMetaHintColor, columnMetaTooltipColor, columnMetaMap, columnMetaMapByLowerName, darkMode, densityParams.metaFontSize, foreignKeyMap, foreignKeyMapByLowerName, openForeignKeyTarget, showColumnComment, showColumnType]);
}, [columnMetaHintColor, columnMetaTooltipColor, columnMetaMap, columnMetaMapByLowerName, darkMode, densityParams.metaFontSize, foreignKeyMap, foreignKeyMapByLowerName, highlightedColumnName, openForeignKeyTarget, showColumnComment, showColumnType]);
const lockVirtualInlineTableScroll = useCallback((lock: boolean) => {
if (lock) {
@@ -2875,13 +2894,18 @@ const DataGrid: React.FC<DataGridProps> = ({
height: 100%;
}
.${gridId} .data-grid-pagination-size-select {
min-width: 112px;
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 {
@@ -2890,7 +2914,7 @@ const DataGrid: React.FC<DataGridProps> = ({
border: 1px solid ${paginationChipBorderColor} !important;
background: ${paginationChipBg} !important;
box-shadow: none !important;
padding: 0 12px !important;
padding: 0 24px 0 10px !important;
display: flex !important;
align-items: center !important;
}
@@ -2911,15 +2935,16 @@ const DataGrid: React.FC<DataGridProps> = ({
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: 12px !important;
inset-inline-end: 32px !important;
inset-inline-start: 10px !important;
inset-inline-end: 24px !important;
}
.${gridId} .data-grid-pagination-size-select .ant-select-arrow {
color: ${paginationSecondaryTextColor};
inset-inline-end: 12px;
inset-inline-end: 10px;
top: 50%;
transform: translateY(-50%);
margin-top: 0;
@@ -6246,6 +6271,37 @@ const DataGrid: React.FC<DataGridProps> = ({
if (match) focusPageFindMatch(match);
}, [activePageFindMatchIndex, pageFindMatches, focusPageFindMatch]);
const visibleColumnQuickFindMatches = useMemo(() => {
if (!normalizedColumnQuickFindText) return [];
return displayColumnNames.filter((columnName) => (
normalizeDataGridFindQuery(columnName).includes(normalizedColumnQuickFindText)
));
}, [displayColumnNames, normalizedColumnQuickFindText]);
const columnQuickFindOptions = useMemo(
() => visibleColumnQuickFindMatches.slice(0, 12).map((columnName) => ({ value: columnName, label: columnName })),
[visibleColumnQuickFindMatches],
);
const resolveColumnQuickFindTarget = useCallback((): string => {
const exactMatch = displayColumnNames.find((columnName) => (
normalizeDataGridFindQuery(columnName) === normalizedColumnQuickFindText
));
if (exactMatch) return exactMatch;
return visibleColumnQuickFindMatches[0] || '';
}, [displayColumnNames, normalizedColumnQuickFindText, visibleColumnQuickFindMatches]);
const highlightColumnQuickFindTarget = useCallback((columnName: string) => {
setHighlightedColumnName(columnName);
if (columnQuickFindHighlightTimerRef.current) {
clearTimeout(columnQuickFindHighlightTimerRef.current);
}
columnQuickFindHighlightTimerRef.current = setTimeout(() => {
setHighlightedColumnName((prev) => (prev === columnName ? '' : prev));
columnQuickFindHighlightTimerRef.current = null;
}, 1600);
}, []);
const syncExternalScrollFromTargets = useCallback((targets?: HTMLElement[], source?: HTMLElement | null) => {
const externalScroll = externalHorizontalScrollRef.current;
if (!(externalScroll instanceof HTMLDivElement) || horizontalSyncSourceRef.current === 'external') {
@@ -6392,6 +6448,107 @@ const DataGrid: React.FC<DataGridProps> = ({
});
}, [applyVirtualHorizontalOffset, enableVirtual, readVirtualHorizontalOffset, syncVirtualHorizontalVisualOffset]);
const focusColumnQuickFindTarget = useCallback((columnName: string): boolean => {
const root = rootRef.current;
const tableContainer = tableContainerRef.current;
if (!(root instanceof HTMLElement) || !(tableContainer instanceof HTMLElement)) return false;
const headerTarget = Array.from(root.querySelectorAll('[data-column-name]')).find((node) => {
const el = node as HTMLElement;
return el.getAttribute('data-column-name') === columnName;
}) as HTMLElement | undefined;
if (!headerTarget) return false;
const externalScroll = externalHorizontalScrollRef.current;
const tableToExternalTargets = pickTableToExternalSyncTargets(tableContainer);
const referenceScrollTarget =
tableToExternalTargets.find((target) => target.scrollWidth > target.clientWidth + 1)
|| tableToExternalTargets[0]
|| (tableContainer.querySelector('.ant-table-header') as HTMLElement | null);
if (!(referenceScrollTarget instanceof HTMLElement)) {
return false;
}
const currentScrollLeft = enableVirtual
? readVirtualHorizontalOffset(tableContainer)
: referenceScrollTarget.scrollLeft;
const targetRect = headerTarget.getBoundingClientRect();
const viewportRect = referenceScrollTarget.getBoundingClientRect();
const nextScrollLeft = resolveDataGridColumnQuickFindScrollLeft({
currentScrollLeft,
columnLeft: currentScrollLeft + (targetRect.left - viewportRect.left),
columnWidth: targetRect.width,
viewportWidth: referenceScrollTarget.clientWidth,
scrollWidth: referenceScrollTarget.scrollWidth,
});
if (enableVirtual) {
const applied = applyVirtualHorizontalOffset(tableContainer, nextScrollLeft);
if (applied) {
lastTableScrollLeftRef.current = readVirtualHorizontalOffset(tableContainer);
syncExternalScrollFromTargets();
requestAnimationFrame(() => {
syncExternalScrollFromTargets();
});
} else {
tableToExternalTargets.forEach((target) => {
if (target.scrollWidth <= target.clientWidth + 1) {
return;
}
if (Math.abs(target.scrollLeft - nextScrollLeft) > 1) {
target.scrollLeft = nextScrollLeft;
}
});
lastTableScrollLeftRef.current = nextScrollLeft;
syncExternalScrollFromTargets(tableToExternalTargets, tableToExternalTargets[0] ?? referenceScrollTarget);
}
} else {
const targets = pickHorizontalScrollTargets(tableContainer);
const liveTargets = targets.length > 0 ? targets : tableToExternalTargets;
liveTargets.forEach((target) => {
if (target.scrollWidth <= target.clientWidth + 1) {
return;
}
if (Math.abs(target.scrollLeft - nextScrollLeft) > 1) {
target.scrollLeft = nextScrollLeft;
}
});
lastTableScrollLeftRef.current = nextScrollLeft;
scheduleSyncExternalScrollFromTargets(liveTargets[0] ?? referenceScrollTarget);
}
highlightColumnQuickFindTarget(columnName);
return true;
}, [
applyVirtualHorizontalOffset,
enableVirtual,
highlightColumnQuickFindTarget,
pickHorizontalScrollTargets,
pickTableToExternalSyncTargets,
readVirtualHorizontalOffset,
scheduleSyncExternalScrollFromTargets,
syncExternalScrollFromTargets,
]);
const handleSubmitColumnQuickFind = useCallback(() => {
const targetColumnName = resolveColumnQuickFindTarget();
if (!targetColumnName) {
if (columnQuickFindText.trim()) {
void message.warning(`未找到字段列:${columnQuickFindText.trim()}`);
}
return;
}
setColumnQuickFindText(targetColumnName);
const tryFocus = () => focusColumnQuickFindTarget(targetColumnName);
if (tryFocus()) return;
requestAnimationFrame(() => {
if (tryFocus()) return;
requestAnimationFrame(() => {
if (tryFocus()) return;
void message.warning(`字段列“${targetColumnName}”当前未渲染,无法定位`);
});
});
}, [columnQuickFindText, focusColumnQuickFindTarget, resolveColumnQuickFindTarget]);
// 外部水平滚动条的 wheel 处理(通过原生事件绑定,确保 preventDefault 生效)
useEffect(() => {
const externalScroll = externalHorizontalScrollRef.current;
@@ -6872,6 +7029,18 @@ const DataGrid: React.FC<DataGridProps> = ({
onNavigateNext={() => handleNavigatePageFind('next')}
/>
);
const columnQuickFindContent = isTableSurfaceActive ? (
<DataGridColumnQuickFind
isV2Ui={isV2Ui}
darkMode={darkMode}
inputProps={noAutoCapInputProps as Record<string, unknown>}
value={columnQuickFindText}
options={columnQuickFindOptions}
hasTarget={!!resolveColumnQuickFindTarget()}
onChange={setColumnQuickFindText}
onSubmit={handleSubmitColumnQuickFind}
/>
) : null;
const resultViewSwitcher = (
<DataGridResultViewSwitcher
isV2Ui={isV2Ui}
@@ -7344,6 +7513,7 @@ const DataGrid: React.FC<DataGridProps> = ({
pendingChangeCount={pendingChangeCount}
resultViewSwitcher={resultViewSwitcher}
columnInfoSettingContent={columnInfoSettingContent}
columnQuickFindContent={columnQuickFindContent}
pageFindContent={pageFindContent}
paginationContent={paginationContent}
onViewModeChange={handleViewModeChange}

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { AutoComplete, Button, Input, Tooltip } from 'antd';
import { AimOutlined, SearchOutlined } from '@ant-design/icons';
export interface DataGridColumnQuickFindProps {
isV2Ui: boolean;
darkMode: boolean;
inputProps?: Record<string, unknown>;
value: string;
options: Array<{ value: string; label?: React.ReactNode }>;
hasTarget: boolean;
onChange: (value: string) => void;
onSubmit: () => void;
}
const DataGridColumnQuickFind: React.FC<DataGridColumnQuickFindProps> = ({
isV2Ui,
darkMode,
inputProps,
value,
options,
hasTarget,
onChange,
onSubmit,
}) => (
<Tooltip title="输入字段名,回车或点定位按钮即可跳到对应列">
<div
data-grid-column-quick-find="true"
className={isV2Ui ? 'gn-v2-data-grid-column-quick-find' : undefined}
style={isV2Ui ? undefined : { display: 'flex', alignItems: 'center', gap: 6 }}
>
<div className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-row' : undefined}>
<div className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-field' : undefined}>
<AutoComplete
className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-autocomplete' : undefined}
options={options}
value={value}
onChange={onChange}
onSelect={onChange}
filterOption={false}
popupMatchSelectWidth={280}
>
<Input
{...inputProps}
allowClear
size="small"
variant="borderless"
prefix={<SearchOutlined />}
placeholder="跳到字段列..."
value={value}
onChange={(event) => onChange(event.target.value)}
onPressEnter={onSubmit}
style={isV2Ui ? undefined : { width: 220 }}
/>
</AutoComplete>
</div>
<Button
data-grid-column-quick-find-submit="true"
className={isV2Ui ? 'gn-v2-data-grid-column-quick-find-submit' : undefined}
size="small"
icon={<AimOutlined />}
disabled={!hasTarget}
onClick={onSubmit}
>
{isV2Ui ? null : '跳转'}
</Button>
</div>
{!isV2Ui && (
<span style={{ fontSize: 12, color: darkMode ? '#999' : '#666', whiteSpace: 'nowrap' }}>
</span>
)}
</div>
</Tooltip>
);
export default DataGridColumnQuickFind;

View File

@@ -18,6 +18,7 @@ export interface DataGridColumnTitleProps {
columnMetaHintColor: string;
columnMetaTooltipColor: string;
darkMode: boolean;
highlighted?: boolean;
onOpenForeignKey?: () => void;
}
@@ -31,6 +32,7 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
columnMetaHintColor,
columnMetaTooltipColor,
darkMode,
highlighted = false,
onOpenForeignKey,
}) => {
const normalizedName = String(columnName || '');
@@ -90,6 +92,8 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
const titleNode = (
<div
className={isSingleLineColumnTitle ? 'gn-v2-column-title is-single-line' : 'gn-v2-column-title'}
data-grid-column-highlighted={highlighted ? 'true' : undefined}
data-column-name={normalizedName}
data-grid-column-title-single-line={isSingleLineColumnTitle ? 'true' : undefined}
style={{
display: 'flex',
@@ -99,6 +103,11 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
minWidth: 0,
maxWidth: '100%',
lineHeight: 1.2,
borderRadius: highlighted ? 8 : undefined,
background: highlighted ? (darkMode ? 'rgba(250, 173, 20, 0.18)' : 'rgba(250, 173, 20, 0.16)') : undefined,
boxShadow: highlighted ? `inset 0 0 0 1px ${darkMode ? 'rgba(250, 173, 20, 0.5)' : 'rgba(250, 173, 20, 0.55)'}` : undefined,
padding: highlighted ? '4px 6px' : undefined,
transition: 'background 160ms ease, box-shadow 160ms ease',
}}
>
{fieldLabel}

View File

@@ -39,34 +39,40 @@ const DataGridPageFind: React.FC<DataGridPageFindProps> = ({
className={isV2Ui ? 'gn-v2-data-grid-page-find' : undefined}
style={isV2Ui ? undefined : { display: 'flex', alignItems: 'center', gap: 6 }}
>
<Input
{...inputProps}
allowClear
size="small"
prefix={<SearchOutlined />}
placeholder="当前页查找..."
value={pageFindText}
onChange={(event) => onPageFindTextChange(event.target.value)}
style={isV2Ui ? undefined : { width: 220 }}
/>
<Button
data-grid-page-find-prev="true"
size="small"
icon={<LeftOutlined />}
disabled={!hasMatches}
onClick={onNavigatePrevious}
>
{isV2Ui ? null : '上一个'}
</Button>
<Button
data-grid-page-find-next="true"
size="small"
icon={<RightOutlined />}
disabled={!hasMatches}
onClick={onNavigateNext}
>
{isV2Ui ? null : '下一个'}
</Button>
<div className={isV2Ui ? 'gn-v2-data-grid-page-find-row' : undefined}>
<Input
className={isV2Ui ? 'gn-v2-data-grid-page-find-input' : undefined}
{...inputProps}
allowClear
size="small"
variant="borderless"
prefix={<SearchOutlined />}
placeholder="当前页查找..."
value={pageFindText}
onChange={(event) => onPageFindTextChange(event.target.value)}
style={isV2Ui ? undefined : { width: 220 }}
/>
<Button
data-grid-page-find-prev="true"
className={isV2Ui ? 'gn-v2-data-grid-page-find-prev' : undefined}
size="small"
icon={<LeftOutlined />}
disabled={!hasMatches}
onClick={onNavigatePrevious}
>
{isV2Ui ? null : '上一个'}
</Button>
<Button
data-grid-page-find-next="true"
className={isV2Ui ? 'gn-v2-data-grid-page-find-next' : undefined}
size="small"
icon={<RightOutlined />}
disabled={!hasMatches}
onClick={onNavigateNext}
>
{isV2Ui ? null : '下一个'}
</Button>
</div>
{normalizedPageFindText && (
<span aria-live="polite" style={isV2Ui ? undefined : { fontSize: 12, color: darkMode ? '#999' : '#666', whiteSpace: 'nowrap' }}>
{hasMatches ? `${activePageFindPosition} / ${matchCount} · ` : ''} {occurrenceCount} / {matchedCellCount}

View File

@@ -78,7 +78,7 @@ const DataGridPaginationBar: React.FC<DataGridPaginationBarProps> = ({
popupMatchSelectWidth={false}
value={String(pagination.pageSize)}
onChange={onPageSizeChange}
options={paginationPageSizeOptions.map((value) => ({ value, label: `${value} /页` }))}
options={paginationPageSizeOptions.map((value) => ({ value, label: `${value}/页` }))}
className="data-grid-pagination-size-select"
aria-label="每页条数"
/>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Button, Popover } from 'antd';
import {
AimOutlined,
ConsoleSqlOutlined,
EditOutlined,
FileTextOutlined,
@@ -21,6 +22,7 @@ export interface DataGridSecondaryActionsProps {
pendingChangeCount: number;
resultViewSwitcher: React.ReactNode;
columnInfoSettingContent: React.ReactNode;
columnQuickFindContent: React.ReactNode;
pageFindContent: React.ReactNode;
paginationContent: React.ReactNode;
onViewModeChange: (nextMode: GridViewMode) => void;
@@ -41,6 +43,7 @@ const DataGridSecondaryActions: React.FC<DataGridSecondaryActionsProps> = ({
pendingChangeCount,
resultViewSwitcher,
columnInfoSettingContent,
columnQuickFindContent,
pageFindContent,
paginationContent,
onViewModeChange,
@@ -59,48 +62,61 @@ const DataGridSecondaryActions: React.FC<DataGridSecondaryActionsProps> = ({
return (
<div data-grid-secondary-actions="true" className="gn-v2-data-grid-statusbar">
<div className="gn-v2-data-grid-view-tabs">
{viewTabItems.map((item) => (
<div className="gn-v2-data-grid-status-main">
<div className="gn-v2-data-grid-view-tabs">
{viewTabItems.map((item) => (
<Button
data-grid-ddl-action={item.key === 'ddl' && canViewDdl ? 'true' : undefined}
key={item.key}
size="small"
type={viewMode === item.key || (item.key === 'table' && (viewMode === 'json' || viewMode === 'text')) ? 'primary' : 'text'}
icon={item.icon}
disabled={item.disabled}
loading={item.key === 'ddl' && ddlLoading}
onClick={() => {
if (item.key === 'table') {
onViewModeChange('table');
return;
}
onViewModeChange(item.key);
}}
>
{item.label}
</Button>
))}
</div>
<div className="gn-v2-toolbar-divider" />
{resultViewSwitcher}
<Popover trigger="click" placement="topRight" content={columnInfoSettingContent}>
<Button
data-grid-ddl-action={item.key === 'ddl' && canViewDdl ? 'true' : undefined}
key={item.key}
data-grid-column-display-action="true"
size="small"
type={viewMode === item.key || (item.key === 'table' && (viewMode === 'json' || viewMode === 'text')) ? 'primary' : 'text'}
icon={item.icon}
disabled={item.disabled}
loading={item.key === 'ddl' && ddlLoading}
onClick={() => {
if (item.key === 'table') {
onViewModeChange('table');
return;
}
onViewModeChange(item.key);
}}
type={showColumnComment || showColumnType ? 'primary' : 'text'}
icon={<FileTextOutlined />}
>
{item.label}
</Button>
))}
</Popover>
<Popover trigger="click" placement="topRight" content={<div style={{ padding: 4 }}>{columnQuickFindContent}</div>}>
<Button
data-grid-column-quick-find-action="true"
size="small"
type="text"
icon={<AimOutlined />}
>
</Button>
</Popover>
{pageFindContent}
<div className="gn-v2-data-grid-status-center">
<span className="gn-v2-data-grid-live">live</span>
<span>{mergedDisplayCount} </span>
<span> {pendingChangeCount}</span>
</div>
</div>
<div className="gn-v2-toolbar-divider" />
{resultViewSwitcher}
<Popover trigger="click" placement="topRight" content={columnInfoSettingContent}>
<Button
data-grid-column-display-action="true"
size="small"
type={showColumnComment || showColumnType ? 'primary' : 'text'}
icon={<FileTextOutlined />}
>
</Button>
</Popover>
<div className="gn-v2-data-grid-status-center">
<span className="gn-v2-data-grid-live">live</span>
<span>{mergedDisplayCount} </span>
<span> {pendingChangeCount}</span>
<div className="gn-v2-data-grid-status-right">
{paginationContent}
</div>
{pageFindContent}
<div className="gn-v2-data-grid-pagination-spacer" aria-hidden="true" />
{paginationContent}
</div>
);
}
@@ -130,6 +146,7 @@ const DataGridSecondaryActions: React.FC<DataGridSecondaryActionsProps> = ({
<Popover trigger="click" placement="bottomRight" content={columnInfoSettingContent}>
<Button data-grid-column-display-action="true" icon={<FileTextOutlined />}></Button>
</Popover>
{columnQuickFindContent}
{canViewDdl && (
<Button
data-grid-ddl-action="true"

View File

@@ -4,6 +4,7 @@ import {
calculateExternalHorizontalScrollInnerWidth,
calculateTableBodyBottomPadding,
calculateVirtualTableScrollX,
resolveDataGridColumnQuickFindScrollLeft,
resolveDataGridHorizontalWheelDelta,
} from './dataGridLayout';
@@ -47,6 +48,50 @@ describe('dataGridLayout helpers', () => {
})).toBe(1);
});
it('resolves quick-find target scrollLeft by centering the target column when possible', () => {
expect(resolveDataGridColumnQuickFindScrollLeft({
currentScrollLeft: 0,
columnLeft: 900,
columnWidth: 120,
viewportWidth: 600,
scrollWidth: 2000,
})).toBe(660);
expect(resolveDataGridColumnQuickFindScrollLeft({
currentScrollLeft: 0,
columnLeft: 40,
columnWidth: 120,
viewportWidth: 600,
scrollWidth: 2000,
})).toBe(0);
expect(resolveDataGridColumnQuickFindScrollLeft({
currentScrollLeft: 200,
columnLeft: 1750,
columnWidth: 140,
viewportWidth: 600,
scrollWidth: 2000,
})).toBe(1400);
});
it('falls back safely when quick-find scroll metrics are degenerate', () => {
expect(resolveDataGridColumnQuickFindScrollLeft({
currentScrollLeft: 120,
columnLeft: 900,
columnWidth: 720,
viewportWidth: 600,
scrollWidth: 2000,
})).toBe(900);
expect(resolveDataGridColumnQuickFindScrollLeft({
currentScrollLeft: 120,
columnLeft: Number.NaN,
columnWidth: Number.NaN,
viewportWidth: 0,
scrollWidth: 2000,
})).toBe(0);
});
it('only treats wheel gestures as horizontal when the horizontal intent is strong enough', () => {
expect(resolveDataGridHorizontalWheelDelta({
deltaX: 18,

View File

@@ -21,6 +21,14 @@ export interface ExternalHorizontalScrollInnerWidthOptions {
trackInset: number;
}
export interface DataGridColumnQuickFindScrollLeftOptions {
currentScrollLeft: number;
columnLeft: number;
columnWidth: number;
viewportWidth: number;
scrollWidth: number;
}
const MIN_SCROLLBAR_CLEARANCE = 8;
const FLOATING_SCROLLBAR_VISUAL_EXTRA = 4;
const HORIZONTAL_WHEEL_MIN_DELTA = 0.5;
@@ -70,6 +78,35 @@ export const calculateExternalHorizontalScrollInnerWidth = ({
return Math.max(1, safeTableScrollWidth - safeTrackInset * 2);
};
export const resolveDataGridColumnQuickFindScrollLeft = ({
currentScrollLeft,
columnLeft,
columnWidth,
viewportWidth,
scrollWidth,
}: DataGridColumnQuickFindScrollLeftOptions): number => {
const safeViewportWidth = Math.max(0, Math.floor(viewportWidth));
const safeScrollWidth = Math.max(0, Math.ceil(scrollWidth));
const maxScrollLeft = Math.max(0, safeScrollWidth - safeViewportWidth);
if (safeViewportWidth <= 0 || maxScrollLeft <= 0) {
return 0;
}
const safeCurrentScrollLeft = Number.isFinite(currentScrollLeft)
? Math.max(0, Math.min(maxScrollLeft, currentScrollLeft))
: 0;
const safeColumnLeft = Number.isFinite(columnLeft) ? columnLeft : safeCurrentScrollLeft;
const safeColumnWidth = Math.max(0, Number.isFinite(columnWidth) ? columnWidth : 0);
if (safeColumnWidth >= safeViewportWidth) {
return Math.max(0, Math.min(maxScrollLeft, safeColumnLeft));
}
const centeredScrollLeft = safeColumnLeft - (safeViewportWidth - safeColumnWidth) / 2;
return Math.max(0, Math.min(maxScrollLeft, centeredScrollLeft));
};
export const resolveDataGridHorizontalWheelDelta = ({
deltaX,
deltaY,