mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 17:31:32 +08:00
🐛 fix(ui): 修复新版数据视图布局与 AI 面板加载容错
- 修复新版数据视图底部分页、列快速定位与当前页查找的对齐和压缩问题 - 优化窄屏下 AI 面板布局,避免挤压工作区并增加懒加载失败重试兜底 - 补充窗口运行时、AI 面板布局与 UI 回归测试,更新相关样式快照
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
77
frontend/src/components/DataGridColumnQuickFind.tsx
Normal file
77
frontend/src/components/DataGridColumnQuickFind.tsx
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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} 个单元格
|
||||
|
||||
@@ -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="每页条数"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user