From 87bd16c4ba586c7dc947a9b7caa7fd4c49479183 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 19 Jun 2026 14:11:54 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(sidebar):=20?= =?UTF-8?q?=E6=8A=BD=E7=A6=BB=E7=8B=AC=E7=AB=8B=E5=B7=A5=E5=85=B7=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=88=B0=20sidebarHelpers=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 sidebar/sidebarHelpers.ts:迁出 6 个无内部类型依赖的纯函数(formatSidebarRowCount/hasSidebarLazyChildren/getV2RailConnectionGroupBadgeText 等)+ V2ExplorerFilter 类型 + V2_RAIL_UNGROUPED 常量 - Sidebar.tsx 通过 import + re-export 双向引用,外部测试文件的 `from './Sidebar'` 保持兼容 - 文件规模:Sidebar.tsx 从 10275 减至 10243 行,建立 sidebar/ 子目录作为后续拆分的目标归宿 --- frontend/src/components/Sidebar.tsx | 68 ++++-------- .../src/components/sidebar/sidebarHelpers.ts | 100 ++++++++++++++++++ 2 files changed, 118 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/sidebar/sidebarHelpers.ts diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 9e61ea3..d164996 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,4 +1,22 @@ import Modal from './common/ResizableDraggableModal'; +import { + V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID, + formatSidebarRowCount, + hasSidebarLazyChildren, + shouldClearSidebarActiveContextOnEmptySelect, + getV2RailConnectionGroupBadgeText, + isV2SidebarObjectNode, + type V2ExplorerFilter, +} from './sidebar/sidebarHelpers'; +// 重新导出,保持外部测试文件的 `from './Sidebar'` 兼容 +export { + V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID, + formatSidebarRowCount, + hasSidebarLazyChildren, + shouldClearSidebarActiveContextOnEmptySelect, + getV2RailConnectionGroupBadgeText, + isV2SidebarObjectNode, +} from './sidebar/sidebarHelpers'; import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react'; import { createPortal } from 'react-dom'; import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Badge, Checkbox, Space, Select, Popover, Tooltip, Progress, Switch } from 'antd'; @@ -278,19 +296,6 @@ export const SQLFileExecutionProgressContent: React.FC ); -const isV2SidebarObjectNode = (node: Pick | null | undefined): boolean => { - return node?.type === 'table' - || node?.type === 'view' - || node?.type === 'materialized-view' - || node?.type === 'db-trigger' - || node?.type === 'db-event' - || node?.type === 'routine'; -}; - -export const hasSidebarLazyChildren = (children: unknown): boolean => { - return Array.isArray(children) && children.length > 0; -}; - export const shouldLoadSidebarNodeOnExpand = ( node: Pick | null | undefined, ): boolean => { @@ -399,12 +404,6 @@ export const buildSidebarTableChildrenForUi = ( return buildV2SidebarTableSectionedChildren(parentKey, tableNodes); }; -export const formatSidebarRowCount = (count: number): string => { - if (!Number.isFinite(count) || count < 0) return ''; - if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`; - if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`; - return String(Math.round(count)); -}; const buildConnectionRootQueryTabTitle = () => t('query.new'); @@ -418,9 +417,6 @@ type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly'; type BatchObjectType = 'table' | 'view'; type BatchObjectFilterType = 'all' | BatchObjectType; type BatchSelectionScope = 'filtered' | 'all'; -export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events'; - -export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-connections__'; export interface V2RailConnectionGroup { id: string; @@ -508,33 +504,6 @@ export const buildV2RailConnectionGroups = ( return groups; }; -export const getV2RailConnectionGroupBadgeText = (name: unknown, fallback = t('connection.sidebar.group.badge')): string => { - const trimmed = String(name ?? '').trim(); - if (!trimmed) return fallback; - const cjkParts = trimmed.match(/[\u4e00-\u9fa5]/g); - if (cjkParts && cjkParts.length > 0) { - return cjkParts.slice(0, 1).join(''); - } - const latinTokens = trimmed.match(/[a-z0-9]+/gi) || []; - if (latinTokens.length >= 2) { - const firstToken = latinTokens[0] || ''; - const secondToken = latinTokens[1] || ''; - return `${firstToken[0] || ''}${secondToken[0] || ''}`.toUpperCase(); - } - if (latinTokens.length === 1) { - const token = latinTokens[0] || ''; - const alphaPrefix = token.match(/^[a-z]+/i)?.[0] || ''; - if (alphaPrefix) { - return alphaPrefix.slice(0, 2).toUpperCase(); - } - const trailingDigits = token.match(/(\d{2,})$/)?.[1]; - if (trailingDigits) { - return trailingDigits.slice(-2).toUpperCase(); - } - return token.slice(0, 2).toUpperCase(); - } - return trimmed.slice(0, 2); -}; const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: string }> = [ { key: 'all', labelKey: 'sidebar.command_search.object_kind.all' }, @@ -894,7 +863,6 @@ export const resolveV2ActiveConnectionId = ({ || ''; }; -export const shouldClearSidebarActiveContextOnEmptySelect = (isV2Ui: boolean): boolean => !isV2Ui; type DriverStatusSnapshot = { type: string; diff --git a/frontend/src/components/sidebar/sidebarHelpers.ts b/frontend/src/components/sidebar/sidebarHelpers.ts new file mode 100644 index 0000000..e326adb --- /dev/null +++ b/frontend/src/components/sidebar/sidebarHelpers.ts @@ -0,0 +1,100 @@ +// Sidebar 工具函数集合(第一期:纯函数 + 共享常量/类型)。 +// +// 本文件是 Sidebar.tsx 拆分的第一步,只搬迁完全独立、无内部类型依赖的工具函数。 +// 后续 PR 会继续搬迁更多工具函数和子组件。 +// +// 设计原则: +// - 只放纯函数(无副作用、无 React state) +// - 不依赖 Sidebar.tsx 内部的 TreeNode 类型(用结构化类型参数代替) +// - 共享常量和类型集中管理,便于跨文件复用 + +import { t } from '../../i18n'; + +// === 共享常量 === + +/** V2 Rail 中"未分组连接"组的固定 ID */ +export const V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID = '__gonavi-v2-ungrouped-connections__'; + +// === 共享类型 === + +/** V2 资源管理器过滤维度 */ +export type V2ExplorerFilter = 'all' | 'tables' | 'views' | 'routines' | 'events'; + +// === 纯函数 === + +/** + * formatSidebarRowCount 把行数格式化为人类可读的简短形式。 + * - >= 1M 显示为 "1.2M" + * - >= 1K 显示为 "1.2K" + * - 否则显示原数字 + * - 非法值(NaN/负数)返回空字符串 + */ +export const formatSidebarRowCount = (count: number): string => { + if (!Number.isFinite(count) || count < 0) return ''; + if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`; + if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`; + return String(Math.round(count)); +}; + +/** + * hasSidebarLazyChildren 判断树节点的 children 是否已加载(用于按需展开)。 + */ +export const hasSidebarLazyChildren = (children: unknown): boolean => { + return Array.isArray(children) && children.length > 0; +}; + +/** + * shouldClearSidebarActiveContextOnEmptySelect 判断在空选择时是否清空激活上下文。 + * 仅 legacy UI 需要清空;V2 UI 保留上下文。 + */ +export const shouldClearSidebarActiveContextOnEmptySelect = (isV2Ui: boolean): boolean => !isV2Ui; + +/** + * getV2RailConnectionGroupBadgeText 从组名生成 1-2 字符的徽章文本。 + * 中文取首字;英文取前两个 token 的首字母大写;其他取前 2 字符。 + */ +export const getV2RailConnectionGroupBadgeText = ( + name: unknown, + fallback = t('connection.sidebar.group.badge'), +): string => { + const trimmed = String(name ?? '').trim(); + if (!trimmed) return fallback; + const cjkParts = trimmed.match(/[一-龥]/g); + if (cjkParts && cjkParts.length > 0) { + return cjkParts.slice(0, 1).join(''); + } + const latinTokens = trimmed.match(/[a-z0-9]+/gi) || []; + if (latinTokens.length >= 2) { + const firstToken = latinTokens[0] || ''; + const secondToken = latinTokens[1] || ''; + return `${firstToken[0] || ''}${secondToken[0] || ''}`.toUpperCase(); + } + if (latinTokens.length === 1) { + const token = latinTokens[0] || ''; + const alphaPrefix = token.match(/^[a-z]+/i)?.[0] || ''; + if (alphaPrefix) { + return alphaPrefix.slice(0, 2).toUpperCase(); + } + const trailingDigits = token.match(/(\d{2,})$/)?.[1]; + if (trailingDigits) { + return trailingDigits.slice(-2).toUpperCase(); + } + return token.slice(0, 2).toUpperCase(); + } + return trimmed.slice(0, 2); +}; + +/** + * isV2SidebarObjectNode 判断节点是否是 SQL 对象类型(表/视图/触发器/事件/存储过程)。 + * 接收结构化类型而非 TreeNode,避免对 Sidebar 内部类型的硬依赖。 + */ +export const isV2SidebarObjectNode = ( + node: { type?: string } | null | undefined, +): boolean => { + return node?.type === 'table' + || node?.type === 'view' + || node?.type === 'materialized-view' + || node?.type === 'db-trigger' + || node?.type === 'db-event' + || node?.type === 'routine'; +};