mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-23 01:11:06 +08:00
- 重构批量改单元格的状态流,减少高频交互时的无效重渲染 - 优化大数据量场景下的表格交互流畅度与响应延迟 - 调整单元格编辑细节,增强与 Navicat 编辑习惯的一致性 🔧 fix(sidebar-connection): 修复多数据源切换后旧连接节点无响应问题 - 修复新建并连接新数据源后,旧数据源点击无响应的问题 ✨ feat(tab-manager): 表与设计标签支持环境前缀显示 - 基于连接名识别 DEV/UAT/PROD/SIT/STG/TEST 环境标记 - 仅对 table/design 标签添加环境前缀,查询等标签保持原样 - 无法识别标准环境时回退显示连接名,提升多环境可辨识性 ✨ feat(connection-config): 新增连接URI复制解析并支持MySQL/Mongo主从配置 - 连接弹窗新增 URI 生成、解析、复制能力,支持参数回填 - MySQL 支持多地址主从拓扑、从库地址列表与从库独立凭据 - Mongo 支持多节点配置、replicaSet、authSource、readPreference - 扩展前后端连接配置模型并同步 Wails 生成类型文件 - 后端接入主从凭据回退策略,保持旧配置兼容 ✨ feat(mongodb-replica): 对齐Navicat主从配置并补齐成员发现能力 - 新增 mongoSrv、mongoAuthMechanism、savePassword 配置项 - 支持 mongodb+srv URI 构建与解析,并透传 authMechanism - 新增 MongoDiscoverMembers 接口,返回成员与状态信息 - 驱动侧实现 replSetGetStatus -> hello/isMaster 回退发现链路 - 前端弹窗新增 SRV 开关、验证方式、成员发现按钮与状态表 - 增加 SRV+SSH 冲突提示与后端保护,避免无效连接路径 🔧 fix(app-error-text): 修复连接测试错误信息乱码并完善日志提示 - 新增错误文本编码纠正能力,处理混合编码导致的中文乱码 - 连接错误提示统一走 normalizeErrorMessage 输出 - 增加 GB18030 纠正相关单元测试覆盖 PostgreSQL 认证失败场景 - go.mod 显式引入 golang.org/x/text 依赖 ✨ feat(filter-panel): 筛选条件支持启用停用与批量开关 - 筛选条件新增 enabled 状态,支持按条件勾选启用/停用 - 筛选面板新增“全启用”“全停用”快捷操作 - SQL 组装时自动跳过已停用条件,保留条件内容便于复用 - 同步 DataViewer 与 SQL 工具层类型,确保筛选链路一致性 🔧 fix(connection-modal-scroll): 修复连接弹窗滚动行为并去除外层滚动条 - 连接配置步骤设置弹窗 body 最大高度与内部滚动 - 为连接弹窗增加专用 wrapClassName 并禁用外层滚动 - 修复出现双滚动条的问题,确保仅保留弹窗内部滚动条
168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { Tabs, Dropdown } from 'antd';
|
|
import type { MenuProps } from 'antd';
|
|
import { useStore } from '../store';
|
|
import DataViewer from './DataViewer';
|
|
import QueryEditor from './QueryEditor';
|
|
import TableDesigner from './TableDesigner';
|
|
import RedisViewer from './RedisViewer';
|
|
import RedisCommandEditor from './RedisCommandEditor';
|
|
import TriggerViewer from './TriggerViewer';
|
|
import type { TabData } from '../types';
|
|
|
|
const detectConnectionEnvLabel = (connectionName: string): string | null => {
|
|
const tokens = connectionName.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
if (tokens.includes('prod') || tokens.includes('production')) return 'PROD';
|
|
if (tokens.includes('uat')) return 'UAT';
|
|
if (tokens.includes('dev') || tokens.includes('development')) return 'DEV';
|
|
if (tokens.includes('sit')) return 'SIT';
|
|
if (tokens.includes('stg') || tokens.includes('stage') || tokens.includes('staging') || tokens.includes('pre')) return 'STG';
|
|
if (tokens.includes('test') || tokens.includes('qa')) return 'TEST';
|
|
return null;
|
|
};
|
|
|
|
const buildTabDisplayTitle = (tab: TabData, connectionName: string | undefined): string => {
|
|
if (tab.type !== 'table' && tab.type !== 'design') return tab.title;
|
|
if (!connectionName) return tab.title;
|
|
const prefix = detectConnectionEnvLabel(connectionName) || connectionName;
|
|
return `[${prefix}] ${tab.title}`;
|
|
};
|
|
|
|
const TabManager: React.FC = () => {
|
|
const tabs = useStore(state => state.tabs);
|
|
const connections = useStore(state => state.connections);
|
|
const activeTabId = useStore(state => state.activeTabId);
|
|
const setActiveTab = useStore(state => state.setActiveTab);
|
|
const closeTab = useStore(state => state.closeTab);
|
|
const closeOtherTabs = useStore(state => state.closeOtherTabs);
|
|
const closeTabsToLeft = useStore(state => state.closeTabsToLeft);
|
|
const closeTabsToRight = useStore(state => state.closeTabsToRight);
|
|
const closeAllTabs = useStore(state => state.closeAllTabs);
|
|
|
|
const onChange = (newActiveKey: string) => {
|
|
setActiveTab(newActiveKey);
|
|
};
|
|
|
|
const onEdit = (targetKey: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => {
|
|
if (action === 'remove') {
|
|
closeTab(targetKey as string);
|
|
}
|
|
};
|
|
|
|
const items = useMemo(() => tabs.map((tab, index) => {
|
|
const connectionName = connections.find((conn) => conn.id === tab.connectionId)?.name;
|
|
const displayTitle = buildTabDisplayTitle(tab, connectionName);
|
|
let content;
|
|
if (tab.type === 'query') {
|
|
content = <QueryEditor tab={tab} />;
|
|
} else if (tab.type === 'table') {
|
|
content = <DataViewer tab={tab} />;
|
|
} else if (tab.type === 'design') {
|
|
content = <TableDesigner tab={tab} />;
|
|
} else if (tab.type === 'redis-keys') {
|
|
content = <RedisViewer connectionId={tab.connectionId} redisDB={tab.redisDB ?? 0} />;
|
|
} else if (tab.type === 'redis-command') {
|
|
content = <RedisCommandEditor connectionId={tab.connectionId} redisDB={tab.redisDB ?? 0} />;
|
|
} else if (tab.type === 'trigger') {
|
|
content = <TriggerViewer tab={tab} />;
|
|
}
|
|
|
|
const menuItems: MenuProps['items'] = [
|
|
{
|
|
key: 'close-other',
|
|
label: '关闭其他页',
|
|
disabled: tabs.length <= 1,
|
|
onClick: () => closeOtherTabs(tab.id),
|
|
},
|
|
{
|
|
key: 'close-left',
|
|
label: '关闭左侧',
|
|
disabled: index === 0,
|
|
onClick: () => closeTabsToLeft(tab.id),
|
|
},
|
|
{
|
|
key: 'close-right',
|
|
label: '关闭右侧',
|
|
disabled: index === tabs.length - 1,
|
|
onClick: () => closeTabsToRight(tab.id),
|
|
},
|
|
{ type: 'divider' },
|
|
{
|
|
key: 'close-all',
|
|
label: '关闭所有',
|
|
disabled: tabs.length === 0,
|
|
onClick: () => closeAllTabs(),
|
|
},
|
|
];
|
|
|
|
return {
|
|
label: (
|
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
|
<span onContextMenu={(e) => e.preventDefault()}>{displayTitle}</span>
|
|
</Dropdown>
|
|
),
|
|
key: tab.id,
|
|
children: content,
|
|
};
|
|
}), [tabs, connections, closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs]);
|
|
|
|
return (
|
|
<>
|
|
<style>{`
|
|
.main-tabs {
|
|
height: 100%;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
.main-tabs .ant-tabs-nav {
|
|
flex: 0 0 auto;
|
|
}
|
|
.main-tabs .ant-tabs-content-holder {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.main-tabs .ant-tabs-content {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.main-tabs .ant-tabs-tabpane {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
.main-tabs .ant-tabs-tabpane > div {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
.main-tabs .ant-tabs-tabpane-hidden {
|
|
display: none !important;
|
|
}
|
|
.main-tabs .ant-tabs-nav::before {
|
|
border-bottom: none !important;
|
|
}
|
|
`}</style>
|
|
<Tabs
|
|
className="main-tabs"
|
|
type="editable-card"
|
|
onChange={onChange}
|
|
activeKey={activeTabId || undefined}
|
|
onEdit={onEdit}
|
|
items={items}
|
|
hideAdd
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default TabManager;
|