🐛 fix(tab-manager): 精简标签冗余信息并优化悬浮卡触发与对齐

- 去掉重复连接后缀、圆点和表类型图标,收敛标签信息密度
- 调整悬浮信息布局为对齐网格,提升字段可读性
- 延长悬浮卡触发时间,减少切换 Tab 时的误触发
This commit is contained in:
Syngnat
2026-05-30 17:27:40 +08:00
parent 5a52b141ed
commit 6934285d83
3 changed files with 54 additions and 43 deletions

View File

@@ -3,7 +3,13 @@ import { readFileSync } from 'node:fs';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import { TAB_WORKBENCH_CLASS_NAME, resolveTabHoverOpen, TabHoverInfo, stopTabHoverDragPropagation } from './TabManager';
import {
TAB_WORKBENCH_CLASS_NAME,
resolveTabHoverOpen,
shouldShowV2ConnectionLabel,
TabHoverInfo,
stopTabHoverDragPropagation,
} from './TabManager';
import type { TabData } from '../types';
describe('TabManager hover info', () => {
@@ -97,6 +103,21 @@ describe('TabManager hover info', () => {
expect(resolveTabHoverOpen(false, true)).toBe(false);
});
it('hides the v2 gray connection suffix when the title already carries the same prefix', () => {
expect(shouldShowV2ConnectionLabel('[本地] videos', '本地')).toBe(false);
expect(shouldShowV2ConnectionLabel('[缓存 | 10.0.0.8] db2', '缓存')).toBe(false);
expect(shouldShowV2ConnectionLabel('新建查询', '本地')).toBe(true);
});
it('does not render the v2 leading icon or connection accent dot in tab labels', () => {
const source = readFileSync(new URL('./TabManager.tsx', import.meta.url), 'utf8');
expect(source).not.toContain('gn-v2-tab-kind-icon');
expect(source).not.toContain('tab-connection-accent');
expect(source).not.toContain('has-connection-accent');
expect(source).not.toContain('resolveConnectionAccentColor');
});
it('wires hover card tab-switch and drag-blocking handlers with selectable text styles', () => {
const source = readFileSync(new URL('./TabManager.tsx', import.meta.url), 'utf8');
@@ -114,7 +135,11 @@ describe('TabManager hover info', () => {
expect(source).toContain('open={resolveTabHoverOpen(isHoverInfoOpen, isTabMenuOpen)}');
expect(source).toContain('onOpenChange={handleHoverInfoOpenChange}');
expect(source).toContain('onOpenChange={handleTabMenuOpenChange}');
expect(source).toContain('mouseEnterDelay={1.2}');
expect(source).toMatch(/\.gn-v2-tab-hover-card \{[^}]*cursor: text;[^}]*user-select: text;/s);
expect(source).toContain("--gn-v2-tab-hover-grid-columns: 56px minmax(0, 1fr);");
expect(source).toMatch(/\.gn-v2-tab-hover-head \{[^}]*display: grid;[^}]*grid-template-columns: var\(--gn-v2-tab-hover-grid-columns\);/s);
expect(source).toMatch(/\.gn-v2-tab-hover-row \{[^}]*grid-template-columns: var\(--gn-v2-tab-hover-grid-columns\);/s);
expect(source).toMatch(/\.gn-v2-tab-hover-card \* \{[^}]*user-select: text;/s);
});
});

View File

@@ -25,7 +25,6 @@ import JVMMonitoringDashboard from './JVMMonitoringDashboard';
import type { TabData } from '../types';
import { buildTabDisplayTitle } from '../utils/tabDisplay';
import { resolveConnectionHostSummary } from '../utils/tabDisplay';
import { resolveConnectionAccentColor } from '../utils/connectionVisual';
const getTabKindLabel = (tab: TabData): string => {
if (tab.type === 'query') return 'SQL';
@@ -43,14 +42,6 @@ const getTabKindLabel = (tab: TabData): string => {
export const TAB_WORKBENCH_CLASS_NAME = 'tab-workbench';
const getTabKindIcon = (tab: TabData): React.ReactNode => {
if (tab.type === 'query') return <ConsoleSqlOutlined />;
if (tab.type === 'table-overview') return <DatabaseOutlined />;
if (tab.type.startsWith('redis')) return <DatabaseOutlined />;
if (tab.type.startsWith('jvm')) return <AppstoreOutlined />;
return <DatabaseOutlined />;
};
const getTabKindTooltipLabel = (tab: TabData): string => {
if (tab.type === 'query') return 'SQL 查询';
if (tab.type === 'table') return '表数据';
@@ -90,6 +81,22 @@ export const stopTabHoverDragPropagation = (event: React.SyntheticEvent<HTMLElem
export const resolveTabHoverOpen = (isHoverInfoOpen: boolean, isTabMenuOpen: boolean) =>
isHoverInfoOpen && !isTabMenuOpen;
export const shouldShowV2ConnectionLabel = (displayTitle: string, connectionLabel?: string): boolean => {
const normalizedConnectionLabel = String(connectionLabel || '').trim();
if (!normalizedConnectionLabel) {
return false;
}
const normalizedDisplayTitle = String(displayTitle || '').trim();
if (!normalizedDisplayTitle) {
return true;
}
const escapedConnectionLabel = normalizedConnectionLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const prefixedConnectionPattern = new RegExp(`^\\[${escapedConnectionLabel}(?:\\s*[|\\]])`, 'i');
return !prefixedConnectionPattern.test(normalizedDisplayTitle);
};
type TabHoverInfoProps = {
tab: TabData;
displayTitle: string;
@@ -150,7 +157,6 @@ type SortableTabLabelProps = {
tab: TabData;
displayTitle: string;
menuItems: MenuProps['items'];
accentColor?: string;
connectionLabel?: string;
hostSummary?: string;
isV2Ui?: boolean;
@@ -161,7 +167,6 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
tab,
displayTitle,
menuItems,
accentColor,
connectionLabel,
hostSummary,
isV2Ui,
@@ -169,9 +174,6 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
}) => {
const [isHoverInfoOpen, setIsHoverInfoOpen] = useState(false);
const [isTabMenuOpen, setIsTabMenuOpen] = useState(false);
const labelStyle = accentColor
? ({ '--connection-accent': accentColor } as React.CSSProperties)
: undefined;
const handleTabLabelContextMenu = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
@@ -190,20 +192,15 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
const labelNode = (
<span
className={`tab-dnd-label${accentColor ? ' has-connection-accent' : ''}${isV2Ui ? ' gn-v2-tab-label' : ''}`}
className={`tab-dnd-label${isV2Ui ? ' gn-v2-tab-label' : ''}`}
onContextMenu={handleTabLabelContextMenu}
title={isV2Ui ? undefined : displayTitle}
style={labelStyle}
>
{isV2Ui ? (
<span className="gn-v2-tab-kind-icon" aria-hidden="true">
{getTabKindIcon(tab)}
</span>
) : null}
{isV2Ui ? <span className="gn-v2-tab-kind">{getTabKindLabel(tab)}</span> : null}
{accentColor ? <span className="tab-connection-accent" aria-hidden="true" /> : null}
<span className="tab-title-text">{displayTitle}</span>
{isV2Ui && connectionLabel ? <span className="gn-v2-tab-conn">{connectionLabel}</span> : null}
{isV2Ui && shouldShowV2ConnectionLabel(displayTitle, connectionLabel) ? (
<span className="gn-v2-tab-conn">{connectionLabel}</span>
) : null}
{isV2Ui && onClose ? (
<button
type="button"
@@ -232,7 +229,7 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
/>
)}
placement="bottomLeft"
mouseEnterDelay={0.25}
mouseEnterDelay={1.2}
open={resolveTabHoverOpen(isHoverInfoOpen, isTabMenuOpen)}
onOpenChange={handleHoverInfoOpenChange}
destroyOnHidden
@@ -441,7 +438,6 @@ const TabManager: React.FC = React.memo(() => {
const items = useMemo(() => tabs.map((tab, index) => {
const connection = connections.find((conn) => conn.id === tab.connectionId);
const displayTitle = buildTabDisplayTitle(tab, connection);
const accentColor = connection ? resolveConnectionAccentColor(connection) : undefined;
const hostSummary = resolveConnectionHostSummary(connection?.config);
const tabIsActive = tab.id === activeTabId;
@@ -479,7 +475,6 @@ const TabManager: React.FC = React.memo(() => {
tab={tab}
displayTitle={displayTitle}
menuItems={menuItems}
accentColor={accentColor}
connectionLabel={connection?.name}
hostSummary={hostSummary}
isV2Ui={isV2Ui}
@@ -622,17 +617,6 @@ const TabManager: React.FC = React.memo(() => {
gap: 7px;
max-width: 100%;
}
.main-tabs .tab-dnd-label.has-connection-accent {
position: relative;
}
.main-tabs .tab-connection-accent {
width: 9px;
height: 9px;
border-radius: 999px;
background: var(--connection-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--connection-accent) 22%, transparent);
flex: 0 0 auto;
}
.main-tabs .tab-title-text {
min-width: 0;
overflow: hidden;
@@ -675,6 +659,7 @@ body[data-theme='dark'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
pointer-events: auto;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-card {
--gn-v2-tab-hover-grid-columns: 56px minmax(0, 1fr);
display: flex;
flex-direction: column;
gap: 8px;
@@ -689,13 +674,14 @@ body[data-theme='dark'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
-webkit-user-select: text;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-head {
display: flex;
align-items: center;
display: grid;
grid-template-columns: var(--gn-v2-tab-hover-grid-columns);
align-items: start;
gap: 8px;
min-width: 0;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-head > span {
flex: 0 0 auto;
justify-self: start;
padding: 2px 6px;
border-radius: 5px;
background: var(--gn-bg-active);
@@ -721,7 +707,7 @@ body[data-theme='dark'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
}
body[data-ui-version='v2'] .gn-v2-tab-hover-row {
display: grid;
grid-template-columns: 52px minmax(0, 1fr);
grid-template-columns: var(--gn-v2-tab-hover-grid-columns);
align-items: start;
gap: 8px;
font-size: var(--gn-font-size-sm, 12px);