mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-23 06:53:52 +08:00
♻️ refactor(sidebar): 抽出 Command Search 面板组件
- 新建 SidebarSearchPanel,承接 V2 命令搜索弹层的行、分组和空状态渲染 - Sidebar.tsx 保留搜索状态、过滤和执行逻辑,仅通过 typed props 传入子组件 - 保持 @ 对象搜索、? AI 提问、同步过滤与键盘导航行为不变
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import Modal from './common/ResizableDraggableModal';
|
||||
import SidebarConnectionRail from './sidebar/SidebarConnectionRail';
|
||||
import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel';
|
||||
import {
|
||||
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
|
||||
formatSidebarRowCount,
|
||||
@@ -7527,101 +7528,6 @@ const Sidebar: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const renderV2CommandSearchRow = (item: V2CommandSearchItem, active: boolean) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`gn-v2-command-row${active ? ' is-active' : ''}`}
|
||||
onMouseEnter={() => setV2CommandActiveIndex(commandSearchFlatItems.findIndex((entry) => entry.key === item.key))}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => runCommandSearchItem(item)}
|
||||
>
|
||||
<span className={`gn-v2-command-row-icon is-${item.kind}`}>{item.icon}</span>
|
||||
<span className="gn-v2-command-row-main">
|
||||
<strong>{item.title}</strong>
|
||||
{item.meta ? <small>{item.meta}</small> : null}
|
||||
</span>
|
||||
{item.kind === 'action' && item.shortcut ? <kbd>{item.shortcut}</kbd> : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
const renderV2CommandSearchSection = (title: string, items: V2CommandSearchItem[]) => {
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<section className="gn-v2-command-section">
|
||||
<div className="gn-v2-command-section-title">{title}</div>
|
||||
{items.map((item) => renderV2CommandSearchRow(
|
||||
item,
|
||||
commandSearchFlatItems[v2CommandActiveIndex]?.key === item.key,
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const renderV2CommandSearchOverlay = () => {
|
||||
if (!isV2CommandSearchOpen) return null;
|
||||
const emptyCopy = v2CommandSearchAiMode
|
||||
? '输入「?」后加问题,按 Enter 发送到 AI 面板。'
|
||||
: (v2CommandSearchObjectMode
|
||||
? '未找到匹配的表、视图或物化视图。'
|
||||
: '未找到匹配项。可输入 @表名 只搜表对象,或输入 ?问题 让 AI 回答。');
|
||||
return (
|
||||
<div className="gn-v2-command-backdrop" data-v2-command-search="true" onMouseDown={closeV2CommandSearch}>
|
||||
<div className="gn-v2-command-palette" role="dialog" aria-modal="true" aria-label={v2CommandSearchLabel} onMouseDown={(event) => event.stopPropagation()}>
|
||||
<div className="gn-v2-command-searchbar">
|
||||
<SearchOutlined />
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
ref={commandSearchInputRef}
|
||||
variant="borderless"
|
||||
value={v2CommandSearchValue}
|
||||
onChange={(event) => handleV2CommandSearchValueChange(event.target.value)}
|
||||
onKeyDown={handleV2CommandSearchKeyDown}
|
||||
placeholder={v2CommandSearchPlaceholder}
|
||||
/>
|
||||
<Tooltip title={t('sidebar.command_search.sync_to_filter_tooltip')}>
|
||||
<span className="gn-v2-command-filter-switch" aria-label={t('sidebar.command_search.sync_to_filter_aria')}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={v2CommandSearchPersistentFilterEnabled}
|
||||
onChange={toggleV2CommandSearchPersistentFilter}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={v2PersistedSidebarFilter ? t('sidebar.command_search.reset_filter') : t('sidebar.command_search.no_synced_filter')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
aria-label={t('sidebar.command_search.reset_filter')}
|
||||
disabled={!v2PersistedSidebarFilter}
|
||||
onClick={resetV2SidebarFilter}
|
||||
/>
|
||||
</Tooltip>
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
<div className="gn-v2-command-list">
|
||||
{renderV2CommandSearchSection('跳转 · GO TO', filteredCommandSearchTreeItems)}
|
||||
{renderV2CommandSearchSection('AI · ASK', commandSearchAiItem)}
|
||||
{renderV2CommandSearchSection('动作 · ACTIONS', filteredCommandSearchActionItems)}
|
||||
{renderV2CommandSearchSection('近期查询 · RECENT', filteredCommandSearchRecentItems)}
|
||||
{commandSearchFlatItems.length === 0 ? (
|
||||
<div className="gn-v2-command-empty">
|
||||
{emptyCopy}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="gn-v2-command-footer">
|
||||
<span><kbd>↑</kbd><kbd>↓</kbd>导航</span>
|
||||
<span><kbd>↵</kbd>选择</span>
|
||||
<span><TableOutlined /> <kbd>@</kbd>只搜表对象</span>
|
||||
<span><RobotOutlined /> <kbd>?</kbd>发送给 AI</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
expandConnectionFromRailRef.current = (connectionId: string) => {
|
||||
const conn = connections.find((item) => item.id === connectionId);
|
||||
if (conn) {
|
||||
@@ -9163,6 +9069,35 @@ const Sidebar: React.FC<{
|
||||
const v2CommandSearchLabel = t('sidebar.command_search.label');
|
||||
const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder');
|
||||
|
||||
const v2CommandSearchPanelProps: SidebarSearchPanelProps<V2CommandSearchItem> = {
|
||||
isOpen: isV2CommandSearchOpen,
|
||||
searchValue: v2CommandSearchValue,
|
||||
activeIndex: v2CommandActiveIndex,
|
||||
label: v2CommandSearchLabel,
|
||||
placeholder: v2CommandSearchPlaceholder,
|
||||
persistedFilter: v2PersistedSidebarFilter,
|
||||
persistentFilterEnabled: v2CommandSearchPersistentFilterEnabled,
|
||||
aiMode: v2CommandSearchAiMode,
|
||||
objectMode: v2CommandSearchObjectMode,
|
||||
flatItems: commandSearchFlatItems,
|
||||
sections: {
|
||||
goTo: filteredCommandSearchTreeItems,
|
||||
ai: commandSearchAiItem,
|
||||
actions: filteredCommandSearchActionItems,
|
||||
recent: filteredCommandSearchRecentItems,
|
||||
},
|
||||
inputRef: commandSearchInputRef,
|
||||
handlers: {
|
||||
onSearchValueChange: handleV2CommandSearchValueChange,
|
||||
onKeyDown: handleV2CommandSearchKeyDown,
|
||||
onClose: closeV2CommandSearch,
|
||||
onItemSelect: (item: V2CommandSearchItem) => runCommandSearchItem(item),
|
||||
onItemHover: (key: string) => setV2CommandActiveIndex(commandSearchFlatItems.findIndex((entry) => entry.key === key)),
|
||||
onTogglePersistentFilter: toggleV2CommandSearchPersistentFilter,
|
||||
onResetFilter: resetV2SidebarFilter,
|
||||
},
|
||||
};
|
||||
|
||||
// V2 Connection Rail 子组件 props(从原 renderV2ConnectionRail 抽出,保留所有原行为)
|
||||
const v2ConnectionRailProps = {
|
||||
labels: {
|
||||
@@ -9511,7 +9446,7 @@ const Sidebar: React.FC<{
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderV2CommandSearchOverlay()}
|
||||
<SidebarSearchPanel {...v2CommandSearchPanelProps} />
|
||||
|
||||
{contextMenu?.kind && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
|
||||
167
frontend/src/components/sidebar/SidebarSearchPanel.tsx
Normal file
167
frontend/src/components/sidebar/SidebarSearchPanel.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
import { Input, Button, Switch, Tooltip } from 'antd';
|
||||
import { SearchOutlined, ReloadOutlined, TableOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import { noAutoCapInputProps } from '../../utils/inputAutoCap';
|
||||
import { t } from '../../i18n';
|
||||
|
||||
// V2 Command Search 子组件(从 Sidebar.tsx 抽取)。
|
||||
//
|
||||
// 设计:把 renderV2CommandSearchRow/Section/Overlay 三个闭包函数合并为一个独立组件。
|
||||
// Props 聚合为 5 个对象(state + items + handlers + flags + labels),避免 22+ 个独立 props。
|
||||
//
|
||||
// 注意:组件接收 V2CommandSearchItem[],类型由 Sidebar 主组件定义(含 React.ReactNode 字段),
|
||||
// 这里用结构化类型 V2CommandSearchItemLike 代替,避免循环依赖。
|
||||
|
||||
export interface V2CommandSearchItemLike {
|
||||
key: string;
|
||||
kind: 'node' | 'action' | 'recent';
|
||||
title: string;
|
||||
meta?: string;
|
||||
icon?: React.ReactNode;
|
||||
shortcut?: string;
|
||||
}
|
||||
|
||||
export interface SidebarSearchPanelProps<TItem extends V2CommandSearchItemLike = V2CommandSearchItemLike> {
|
||||
isOpen: boolean;
|
||||
searchValue: string;
|
||||
activeIndex: number;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
persistedFilter: string;
|
||||
persistentFilterEnabled: boolean;
|
||||
aiMode: boolean;
|
||||
objectMode: boolean;
|
||||
flatItems: TItem[];
|
||||
sections: {
|
||||
goTo: TItem[];
|
||||
ai: TItem[];
|
||||
actions: TItem[];
|
||||
recent: TItem[];
|
||||
};
|
||||
inputRef: React.Ref<any>;
|
||||
handlers: {
|
||||
onSearchValueChange: (value: string) => void;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onClose: () => void;
|
||||
onItemSelect: (item: TItem) => void;
|
||||
onItemHover: (key: string) => void;
|
||||
onTogglePersistentFilter: (enabled: boolean) => void;
|
||||
onResetFilter: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
const SidebarSearchPanel = <TItem extends V2CommandSearchItemLike>({
|
||||
isOpen,
|
||||
searchValue,
|
||||
activeIndex,
|
||||
label,
|
||||
placeholder,
|
||||
persistedFilter,
|
||||
persistentFilterEnabled,
|
||||
aiMode,
|
||||
objectMode,
|
||||
flatItems,
|
||||
sections,
|
||||
inputRef,
|
||||
handlers,
|
||||
}: SidebarSearchPanelProps<TItem>) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const emptyCopy = aiMode
|
||||
? '输入「?」后加问题,按 Enter 发送到 AI 面板。'
|
||||
: objectMode
|
||||
? '未找到匹配的表、视图或物化视图。'
|
||||
: '未找到匹配项。可输入 @表名 只搜表对象,或输入 ?问题 让 AI 回答。';
|
||||
|
||||
const renderRow = (item: TItem, active: boolean) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`gn-v2-command-row${active ? ' is-active' : ''}`}
|
||||
onMouseEnter={() => handlers.onItemHover(item.key)}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => handlers.onItemSelect(item)}
|
||||
>
|
||||
<span className={`gn-v2-command-row-icon is-${item.kind}`}>{item.icon}</span>
|
||||
<span className="gn-v2-command-row-main">
|
||||
<strong>{item.title}</strong>
|
||||
{item.meta ? <small>{item.meta}</small> : null}
|
||||
</span>
|
||||
{item.kind === 'action' && item.shortcut ? <kbd>{item.shortcut}</kbd> : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
const renderSection = (title: string, items: TItem[]) => {
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<section className="gn-v2-command-section">
|
||||
<div className="gn-v2-command-section-title">{title}</div>
|
||||
{items.map((item) =>
|
||||
renderRow(item, flatItems[activeIndex]?.key === item.key),
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gn-v2-command-backdrop" data-v2-command-search="true" onMouseDown={handlers.onClose}>
|
||||
<div
|
||||
className="gn-v2-command-palette"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={label}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="gn-v2-command-searchbar">
|
||||
<SearchOutlined />
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
ref={inputRef}
|
||||
variant="borderless"
|
||||
value={searchValue}
|
||||
onChange={(event) => handlers.onSearchValueChange(event.target.value)}
|
||||
onKeyDown={handlers.onKeyDown}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<Tooltip title={t('sidebar.command_search.sync_to_filter_tooltip')}>
|
||||
<span className="gn-v2-command-filter-switch" aria-label={t('sidebar.command_search.sync_to_filter_aria')}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={persistentFilterEnabled}
|
||||
onChange={handlers.onTogglePersistentFilter}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={persistedFilter ? t('sidebar.command_search.reset_filter') : t('sidebar.command_search.no_synced_filter')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
aria-label={t('sidebar.command_search.reset_filter')}
|
||||
disabled={!persistedFilter}
|
||||
onClick={handlers.onResetFilter}
|
||||
/>
|
||||
</Tooltip>
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
<div className="gn-v2-command-list">
|
||||
{renderSection('跳转 · GO TO', sections.goTo)}
|
||||
{renderSection('AI · ASK', sections.ai)}
|
||||
{renderSection('动作 · ACTIONS', sections.actions)}
|
||||
{renderSection('近期查询 · RECENT', sections.recent)}
|
||||
{flatItems.length === 0 ? (
|
||||
<div className="gn-v2-command-empty">{emptyCopy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="gn-v2-command-footer">
|
||||
<span><kbd>↑</kbd><kbd>↓</kbd>导航</span>
|
||||
<span><kbd>↵</kbd>选择</span>
|
||||
<span><TableOutlined /> <kbd>@</kbd>只搜表对象</span>
|
||||
<span><RobotOutlined /> <kbd>?</kbd>发送给 AI</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarSearchPanel;
|
||||
Reference in New Issue
Block a user