feat(sql-files): 支持外部 SQL 目录树与双击打开

- 新增 SQL 目录选择、枚举与按路径读取接口,复用大文件执行能力
- Sidebar 增加外部 SQL 文件目录树、目录管理入口与双击打开查询标签
- 补充 external SQL 持久化与前后端回归测试

Fixes #319
This commit is contained in:
Syngnat
2026-04-17 21:02:48 +08:00
parent f3b78f9763
commit 65a9f4352e
11 changed files with 879 additions and 47 deletions

View File

@@ -0,0 +1,131 @@
import type { ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
export type ExternalSQLNodeType =
| 'external-sql-root'
| 'external-sql-directory'
| 'external-sql-folder'
| 'external-sql-file';
export interface ExternalSQLTreeNode {
title: string;
key: string;
isLeaf?: boolean;
children?: ExternalSQLTreeNode[];
type: ExternalSQLNodeType;
dataRef: Record<string, unknown>;
}
type BuildExternalSQLRootNodeParams = {
dbNodeKey: string;
connectionId: string;
dbName: string;
directories: ExternalSQLDirectory[];
directoryTrees: Record<string, ExternalSQLTreeEntry[]>;
};
const normalizeExternalSQLPath = (value: string): string =>
String(value || '').trim().replace(/\\/g, '/');
const resolveDirectoryDisplayName = (directory: ExternalSQLDirectory): string => {
const explicitName = String(directory.name || '').trim();
if (explicitName) return explicitName;
const normalizedPath = normalizeExternalSQLPath(directory.path);
const segments = normalizedPath.split('/').filter(Boolean);
return segments[segments.length - 1] || 'SQL目录';
};
export const buildExternalSQLDirectoryId = (connectionId: string, dbName: string, directoryPath: string): string =>
`external-sql-dir:${String(connectionId || '').trim()}:${String(dbName || '').trim()}:${normalizeExternalSQLPath(directoryPath)}`;
export const buildExternalSQLTabId = (connectionId: string, dbName: string, filePath: string): string =>
`external-sql-tab:${String(connectionId || '').trim()}:${String(dbName || '').trim()}:${normalizeExternalSQLPath(filePath)}`;
const buildExternalSQLNodeKey = (type: ExternalSQLNodeType, base: string): string =>
`${type}:${normalizeExternalSQLPath(base)}`;
const mapExternalSQLTreeEntries = (
entries: ExternalSQLTreeEntry[],
context: { connectionId: string; dbName: string; dbNodeKey: string; directoryId: string },
): ExternalSQLTreeNode[] => entries.map((entry) => {
const entryPath = normalizeExternalSQLPath(entry.path);
if (entry.isDir) {
const children = mapExternalSQLTreeEntries(entry.children || [], context);
return {
title: entry.name,
key: buildExternalSQLNodeKey('external-sql-folder', entryPath),
type: 'external-sql-folder',
isLeaf: children.length === 0,
children: children.length > 0 ? children : undefined,
dataRef: {
connectionId: context.connectionId,
dbName: context.dbName,
dbNodeKey: context.dbNodeKey,
directoryId: context.directoryId,
path: entry.path,
name: entry.name,
},
};
}
return {
title: entry.name,
key: buildExternalSQLNodeKey('external-sql-file', entryPath),
type: 'external-sql-file',
isLeaf: true,
dataRef: {
connectionId: context.connectionId,
dbName: context.dbName,
dbNodeKey: context.dbNodeKey,
directoryId: context.directoryId,
path: entry.path,
name: entry.name,
},
};
});
export const buildExternalSQLRootNode = ({
dbNodeKey,
connectionId,
dbName,
directories,
directoryTrees,
}: BuildExternalSQLRootNodeParams): ExternalSQLTreeNode => {
const sortedDirectories = [...directories].sort((left, right) =>
resolveDirectoryDisplayName(left).toLowerCase().localeCompare(resolveDirectoryDisplayName(right).toLowerCase()),
);
const children = sortedDirectories.map((directory) => {
const directoryChildren = mapExternalSQLTreeEntries(directoryTrees[directory.id] || [], {
connectionId,
dbName,
dbNodeKey,
directoryId: directory.id,
});
return {
title: resolveDirectoryDisplayName(directory),
key: buildExternalSQLNodeKey('external-sql-directory', directory.id),
type: 'external-sql-directory' as const,
isLeaf: directoryChildren.length === 0,
children: directoryChildren.length > 0 ? directoryChildren : undefined,
dataRef: {
...directory,
connectionId,
dbName,
dbNodeKey,
},
};
});
return {
title: children.length > 0 ? `外部 SQL 文件 (${children.length})` : '外部 SQL 文件',
key: `${dbNodeKey}-external-sql`,
type: 'external-sql-root',
isLeaf: children.length === 0,
children: children.length > 0 ? children : undefined,
dataRef: {
connectionId,
dbName,
dbNodeKey,
},
};
};