diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 7396e24..bed8925 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -d0464f9da25e9356e61652e638c99ffe \ No newline at end of file +0295a42fd931778d85157816d79d29e5 \ No newline at end of file diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index f52c19b..8f1444e 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -49,6 +49,23 @@ describe('tool center menu entries', () => { expect(appSource).not.toContain('const sqlLogs = useStore(state => state.sqlLogs);'); }); + it('lets the v2 Sidebar own the entire left layout instead of stacking legacy controls above it', () => { + const siderIndex = appSource.indexOf("className={isV2Ui ? 'gn-v2-app-sider' : undefined}"); + const legacyGuardIndex = appSource.indexOf('{!isV2Ui && (', siderIndex); + const legacyCreateIndex = appSource.indexOf('新建连接', legacyGuardIndex); + const sidebarIndex = appSource.indexOf(' { expect(appSource).toContain('const resizeGuideColor = isV2Ui'); expect(appSource).toContain("'var(--gn-accent, #16a34a)'"); @@ -78,7 +95,7 @@ describe('tool center menu entries', () => { it('renders recorded shortcuts with platform-specific display labels', () => { expect(appSource).toContain('getShortcutDisplayLabel'); - expect(appSource).toContain("getShortcutDisplayLabel(binding.combo, platform)"); + expect(appSource).toContain('getShortcutDisplayLabel(binding.combo, activeShortcutPlatform)'); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6dbf6ba..5055029 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,7 +19,12 @@ import SecurityUpdateSettingsModal from './components/SecurityUpdateSettingsModa import { DEFAULT_APPEARANCE, useStore } from './store'; import { SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types'; import { blurToFilter, isMacLikePlatform, normalizeBlurForPlatform, normalizeOpacityForPlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; -import { DENSITY_OPTIONS, sanitizeDataTableDensity } from './utils/dataGridDisplay'; +import { + DENSITY_OPTIONS, + sanitizeDataTableDensity, + sanitizeDataTableFontSize, + sanitizeSidebarTreeFontSize, +} from './utils/dataGridDisplay'; import { getMacNativeTitlebarPaddingLeft, getMacNativeTitlebarPaddingRight, shouldHandleMacNativeFullscreenShortcut, shouldSuppressMacNativeEscapeExit } from './utils/macWindow'; import { shouldEnableMacWindowDiagnostics } from './utils/macWindowDiagnostics'; import { resolveAboutDisplayVersion } from './utils/appVersionDisplay'; @@ -66,6 +71,7 @@ import { eventToShortcut, findReservedConflicts, getShortcutDisplay, + getShortcutDisplayLabel, getShortcutPlatform, isEditableElement, isShortcutMatch, @@ -79,12 +85,13 @@ import { resolveVisibleStartupWindowBounds } from './utils/windowRestoreBounds'; import { SIDEBAR_UTILITY_ITEM_KEYS, resolveAIEntryPlacement, - resolveAIEdgeHandleAttachment, - resolveAIEdgeHandleDockStyle, - resolveAIEdgeHandleStyle, + resolveLegacyAIEdgeHandleAttachment, + resolveLegacyAIEdgeHandleDockStyle, + resolveLegacyAIEdgeHandleStyle, } from './utils/aiEntryLayout'; import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App'; import './App.css'; +import './v2-theme.css'; const AIChatPanel = React.lazy(() => import('./components/AIChatPanel')); @@ -174,6 +181,7 @@ function App() { const updateShortcut = useStore(state => state.updateShortcut); const resetShortcutOptions = useStore(state => state.resetShortcutOptions); const darkMode = themeMode === 'dark'; + const isV2Ui = appearance.uiVersion === 'v2'; const effectiveUiScale = Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Number(uiScale) || DEFAULT_UI_SCALE)); const effectiveFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(Number(fontSize) || DEFAULT_FONT_SIZE))); const tokenFontSize = Math.round(effectiveFontSize * effectiveUiScale); @@ -185,6 +193,14 @@ function App() { const tokenControlHeight = Math.max(24, Math.round(32 * effectiveUiScale)); const tokenControlHeightSM = Math.max(20, Math.round(24 * effectiveUiScale)); const tokenControlHeightLG = Math.max(30, Math.round(40 * effectiveUiScale)); + const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false; + const sidebarTreeFontSizeFollowsGlobal = appearance.sidebarTreeFontSizeFollowGlobal !== false; + const effectiveDataTableFontSize = dataTableFontSizeFollowsGlobal + ? effectiveFontSize + : (sanitizeDataTableFontSize(appearance.dataTableFontSize) ?? effectiveFontSize); + const effectiveSidebarTreeFontSize = sidebarTreeFontSizeFollowsGlobal + ? effectiveFontSize + : (sanitizeSidebarTreeFontSize(appearance.sidebarTreeFontSize) ?? effectiveFontSize); const appComponentSize: 'small' | 'middle' | 'large' = effectiveUiScale <= 0.92 ? 'small' : (effectiveUiScale >= 1.12 ? 'large' : 'middle'); const titleBarHeight = Math.max(28, Math.round(32 * effectiveUiScale)); const titleBarButtonWidth = Math.max(40, Math.round(46 * effectiveUiScale)); @@ -217,6 +233,7 @@ function App() { const aiPanelVisible = useStore(state => state.aiPanelVisible); const toggleAIPanel = useStore(state => state.toggleAIPanel); const setAIPanelVisible = useStore(state => state.setAIPanelVisible); + const sqlLogCount = useStore(state => state.sqlLogs.length); const globalProxyInvalidHintShownRef = React.useRef(false); const windowDiagSequenceRef = React.useRef(0); const windowDiagLastSignatureRef = React.useRef(''); @@ -1976,13 +1993,13 @@ function App() { const [dataRootApplying, setDataRootApplying] = useState(false); const [isAISettingsOpen, setIsAISettingsOpen] = useState(false); const aiEntryPlacement = resolveAIEntryPlacement(); - const aiEdgeHandleAttachment = resolveAIEdgeHandleAttachment(aiPanelVisible); - const aiEdgeHandleDockStyle = useMemo( - () => resolveAIEdgeHandleDockStyle(aiEdgeHandleAttachment), - [aiEdgeHandleAttachment], + const legacyAiEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment(aiPanelVisible); + const legacyAiEdgeHandleDockStyle = useMemo( + () => resolveLegacyAIEdgeHandleDockStyle(legacyAiEdgeHandleAttachment), + [legacyAiEdgeHandleAttachment], ); - const aiEdgeHandleStyle = useMemo(() => ( - resolveAIEdgeHandleStyle({ + const legacyAiEdgeHandleStyle = useMemo(() => ( + resolveLegacyAIEdgeHandleStyle({ darkMode, aiPanelVisible, effectiveUiScale, @@ -2006,13 +2023,23 @@ function App() { return SIDEBAR_UTILITY_ITEM_KEYS.map((key) => itemMap[key]); }, []); - const renderAIEdgeHandle = () => ( + const handleOpenToolsModal = useCallback(() => { + setIsToolsModalOpen(true); + }, []); + const handleOpenSettingsModal = useCallback(() => { + setIsSettingsModalOpen(true); + }, []); + const handleFocusSidebarSearch = useCallback(() => { + window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); + }, []); + const renderLegacyAIEdgeHandle = () => ( @@ -2108,6 +2135,9 @@ function App() { const [isLogPanelOpen, setIsLogPanelOpen] = useState(false); const logResizeRef = React.useRef<{ startY: number, startHeight: number } | null>(null); const logGhostRef = React.useRef(null); + const handleToggleLogPanel = useCallback(() => { + setIsLogPanelOpen((prev) => !prev); + }, []); const handleLogResizeStart = (e: React.MouseEvent) => { e.preventDefault(); @@ -2476,7 +2506,25 @@ function App() { document.body.setAttribute('data-ui-version', appearance.uiVersion); document.body.style.fontSize = `${effectiveFontSize}px`; document.documentElement.style.setProperty('--gonavi-font-size', `${effectiveFontSize}px`); - }, [appearance.uiVersion, darkMode, effectiveFontSize]); + document.documentElement.style.setProperty('--gn-ui-scale', `${effectiveUiScale}`); + document.documentElement.style.setProperty('--gn-font-size', `${effectiveFontSize}px`); + document.documentElement.style.setProperty('--gn-font-size-sm', `${Math.max(10, Math.round(effectiveFontSize * 0.86))}px`); + document.documentElement.style.setProperty('--gn-font-size-xs', `${Math.max(9, Math.round(effectiveFontSize * 0.76))}px`); + document.documentElement.style.setProperty('--gn-font-size-mono', `${Math.max(10, Math.round(effectiveDataTableFontSize * 0.92))}px`); + document.documentElement.style.setProperty('--gn-data-table-font-size', `${effectiveDataTableFontSize}px`); + document.documentElement.style.setProperty('--gn-sidebar-tree-font-size', `${effectiveSidebarTreeFontSize}px`); + document.documentElement.style.setProperty('--gn-control-height', `${tokenControlHeight}px`); + document.documentElement.style.setProperty('--gn-control-height-sm', `${tokenControlHeightSM}px`); + }, [ + appearance.uiVersion, + darkMode, + effectiveDataTableFontSize, + effectiveFontSize, + effectiveSidebarTreeFontSize, + effectiveUiScale, + tokenControlHeight, + tokenControlHeightSM, + ]); useEffect(() => { isAboutOpenRef.current = isAboutOpen; @@ -2628,7 +2676,7 @@ function App() { handleNewQuery(); break; case 'toggleLogPanel': - setIsLogPanelOpen((prev) => !prev); + handleToggleLogPanel(); break; case 'toggleTheme': setTheme(themeMode === 'dark' ? 'light' : 'dark'); @@ -2651,7 +2699,7 @@ function App() { return () => { window.removeEventListener('keydown', handleGlobalShortcut); }; - }, [activeShortcutPlatform, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, isMacRuntime, shortcutOptions, themeMode, setTheme, useNativeMacWindowControls]); + }, [activeShortcutPlatform, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, themeMode, setTheme, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { @@ -2726,66 +2774,78 @@ function App() { } as any; const showLinuxResizeHandles = isLinuxRuntime; - const resizeGuideColor = darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'; + const resizeGuideColor = isV2Ui + ? 'var(--gn-accent, #16a34a)' + : (darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'); + const antdTheme = useMemo(() => ({ + algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm, + token: { + fontSize: tokenFontSize, + fontSizeSM: tokenFontSizeSM, + fontSizeLG: tokenFontSizeLG, + controlHeight: tokenControlHeight, + controlHeightSM: tokenControlHeightSM, + controlHeightLG: tokenControlHeightLG, + colorBgLayout: 'transparent', + colorBgContainer: darkMode + ? `rgba(29, 29, 29, ${effectiveOpacity})` + : `rgba(255, 255, 255, ${effectiveOpacity})`, + colorBgElevated: darkMode + ? '#1f1f1f' + : '#ffffff', + colorFillAlter: darkMode + ? `rgba(38, 38, 38, ${effectiveOpacity})` + : `rgba(250, 250, 250, ${effectiveOpacity})`, + colorPrimary: darkMode ? '#f6c453' : '#1677ff', + colorPrimaryHover: darkMode ? '#ffd666' : '#4096ff', + colorPrimaryActive: darkMode ? '#d8a93b' : '#0958d9', + colorInfo: darkMode ? '#f6c453' : '#1677ff', + colorLink: darkMode ? '#ffd666' : '#1677ff', + colorLinkHover: darkMode ? '#ffe58f' : '#4096ff', + colorLinkActive: darkMode ? '#d8a93b' : '#0958d9', + colorPrimaryBg: darkMode ? 'rgba(246, 196, 83, 0.22)' : '#e6f4ff', + colorPrimaryBgHover: darkMode ? 'rgba(246, 196, 83, 0.30)' : '#bae0ff', + colorPrimaryBorder: darkMode ? 'rgba(246, 196, 83, 0.45)' : '#91caff', + colorPrimaryBorderHover: darkMode ? 'rgba(246, 196, 83, 0.60)' : '#69b1ff', + controlItemBgActive: darkMode ? 'rgba(246, 196, 83, 0.20)' : 'rgba(22, 119, 255, 0.12)', + controlItemBgActiveHover: darkMode ? 'rgba(246, 196, 83, 0.28)' : 'rgba(22, 119, 255, 0.18)', + controlOutline: darkMode ? 'rgba(246, 196, 83, 0.50)' : 'rgba(5, 145, 255, 0.24)', + }, + components: { + Layout: { + bodyBg: 'transparent', + headerBg: 'transparent', + siderBg: 'transparent', + triggerBg: 'transparent' + }, + Table: { + headerBg: 'transparent', + rowHoverBg: darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)', + }, + Tabs: { + cardBg: 'transparent', + itemActiveColor: darkMode ? '#ffd666' : '#1890ff', + itemHoverColor: darkMode ? '#ffe58f' : '#40a9ff', + itemSelectedColor: darkMode ? '#ffd666' : '#1677ff', + inkBarColor: darkMode ? '#ffd666' : '#1677ff', + } + } + }), [ + darkMode, + effectiveOpacity, + tokenControlHeight, + tokenControlHeightLG, + tokenControlHeightSM, + tokenFontSize, + tokenFontSizeLG, + tokenFontSizeSM, + ]); return (
+ {!isV2Ui && ( + <>
{sidebarUtilityItems.map((item) => ( @@ -2882,10 +2945,22 @@ function App() {
+ + )} -
+
- +
{!connectionWorkbenchState.ready && (
{/* Floating SQL Log Toggle */} + {!isV2Ui && (
} - onClick={() => setIsLogPanelOpen(!isLogPanelOpen)} + onClick={handleToggleLogPanel} style={isLogPanelOpen ? { width: '100%', height: floatingLogButtonHeight, @@ -2978,6 +3054,7 @@ function App() { SQL 执行日志
+ )}
@@ -3000,18 +3077,26 @@ function App() {
- {aiEntryPlacement === 'content-edge' && aiEdgeHandleAttachment === 'content-shell' && ( -
- {renderAIEdgeHandle()} + {!isV2Ui && !aiPanelVisible && ( + <> + {aiEntryPlacement === 'content-edge' && legacyAiEdgeHandleAttachment === 'content-shell' && ( +
+ {renderLegacyAIEdgeHandle()}
)} + + )} {aiPanelVisible && (
- {aiEntryPlacement === 'content-edge' && aiEdgeHandleAttachment === 'panel-shell' && ( -
- {renderAIEdgeHandle()} + {!isV2Ui && ( + <> + {aiEntryPlacement === 'content-edge' && legacyAiEdgeHandleAttachment === 'panel-shell' && ( +
+ {renderLegacyAIEdgeHandle()}
)} + + )}
}> setAIPanelVisible(false)} onOpenSettings={() => { handleOpenAISettings(); @@ -3029,13 +3114,16 @@ function App() { )} - + )} + {isToolsModalOpen && ( , '工具中心', '集中处理连接配置、同步、驱动和快捷键相关操作。')} open={isToolsModalOpen} @@ -3141,6 +3229,8 @@ function App() { ))}
+ )} + {isSettingsModalOpen && ( , '设置中心', '集中处理代理、主题、AI 与关于等通用配置入口。')} open={isSettingsModalOpen} @@ -3206,6 +3296,8 @@ function App() { ))}
+ )} + {isDataRootModalOpen && ( , '数据存储位置', '统一管理连接、代理、AI 配置与驱动等文件型数据的根目录。')} open={isDataRootModalOpen} @@ -3274,15 +3366,20 @@ function App() {
)} + )} + {isSyncModalOpen && ( setIsSyncModalOpen(false)} /> + )} + {isDriverModalOpen && ( + )} + {isAISettingsOpen && ( + )} + {isThemeModalOpen && ( : , @@ -3495,6 +3595,98 @@ function App() {
{themeModalSection === 'theme' ? (
+
+
+ 界面版本 + + NEW + +
+
+ 在保留全部功能的前提下切换整体外观,新版采用更紧凑的信息层级与更现代的视觉语言。 +
+
+ {[ + { key: 'legacy', label: '旧版 UI', description: '当前稳定界面,所有功能完整可用。', badge: '默认' }, + { key: 'v2', label: '新版 UI', description: '重新设计的紧凑界面,强化 AI 入口与表概览。', badge: 'Beta' }, + ].map((item) => { + const active = (appearance.uiVersion ?? 'legacy') === item.key; + return ( + + ); + })} +
+
+ Windows、macOS 与 Linux 均可切换;切换后立即生效,部分弹窗会在下次打开时使用新样式。 +
+ {appearance.uiVersion === 'v2' && ( +
+ 新版 UI 仍在 Beta,部分屏幕样式可能与旧版有差异,遇到问题可随时切回。 +
+ )} +
主题模式
@@ -3645,6 +3837,70 @@ function App() { 控制行高、列宽和内边距。舒适适合大屏细看;紧凑适合最大化信息密度。已手动拖拽的列宽优先保留。
+
+
+
数据表字体大小
+ +
+
+ setAppearance({ + dataTableFontSize: sanitizeDataTableFontSize(value), + dataTableFontSizeFollowGlobal: false, + })} + style={{ flex: 1 }} + /> + {effectiveDataTableFontSize}px +
+
+
+
+
左侧库表字体大小
+ +
+
+ setAppearance({ + sidebarTreeFontSize: sanitizeSidebarTreeFontSize(value), + sidebarTreeFontSizeFollowGlobal: false, + })} + style={{ flex: 1 }} + /> + {effectiveSidebarTreeFontSize}px +
+
{isMacRuntime ? ( @@ -3691,7 +3947,9 @@ function App() {
+ )} + {isShortcutModalOpen && ( , '快捷键管理', '统一查看、录制与启停常用快捷键,保持操作习惯一致。')} open={isShortcutModalOpen} @@ -3700,7 +3958,19 @@ function App() { setCapturingShortcutAction(null); }} width={760} - styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} + centered + style={{ top: 0, maxHeight: 'calc(100vh - 80px)' }} + styles={{ + content: { + ...utilityModalShellStyle, + height: 'min(760px, calc(100vh - 80px))', + display: 'flex', + flexDirection: 'column', + }, + header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, + body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, + footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } + }} footer={[