mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-12 12:19:47 +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 并禁用外层滚动 - 修复出现双滚动条的问题,确保仅保留弹窗内部滚动条
203 lines
6.5 KiB
TypeScript
203 lines
6.5 KiB
TypeScript
export type FilterCondition = {
|
||
id?: number;
|
||
enabled?: boolean;
|
||
column?: string;
|
||
op?: string;
|
||
value?: string;
|
||
value2?: string;
|
||
};
|
||
|
||
const normalizeIdentPart = (ident: string) => {
|
||
let raw = (ident || '').trim();
|
||
if (!raw) return raw;
|
||
const first = raw[0];
|
||
const last = raw[raw.length - 1];
|
||
if ((first === '"' && last === '"') || (first === '`' && last === '`')) {
|
||
raw = raw.slice(1, -1).trim();
|
||
}
|
||
raw = raw.replace(/["`]/g, '').trim();
|
||
return raw;
|
||
};
|
||
|
||
// 检查标识符是否需要引号(包含特殊字符或是保留字)
|
||
const needsQuote = (ident: string): boolean => {
|
||
if (!ident) return false;
|
||
// 如果包含特殊字符(非字母、数字、下划线)则需要引号
|
||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(ident)) return true;
|
||
// PostgreSQL 会将未加引号的标识符折叠为小写,含大写字母时必须加引号
|
||
if (/[A-Z]/.test(ident)) return true;
|
||
// 常见 SQL 保留字列表(简化版)
|
||
const reserved = ['select', 'from', 'where', 'table', 'index', 'user', 'order', 'group', 'by', 'limit', 'offset', 'and', 'or', 'not', 'null', 'true', 'false', 'key', 'primary', 'foreign', 'references', 'default', 'constraint', 'create', 'drop', 'alter', 'insert', 'update', 'delete', 'set', 'values', 'into', 'join', 'left', 'right', 'inner', 'outer', 'on', 'as', 'is', 'in', 'like', 'between', 'case', 'when', 'then', 'else', 'end', 'having', 'distinct', 'all', 'any', 'exists', 'union', 'except', 'intersect'];
|
||
return reserved.includes(ident.toLowerCase());
|
||
};
|
||
|
||
export const quoteIdentPart = (dbType: string, ident: string) => {
|
||
const raw = normalizeIdentPart(ident);
|
||
if (!raw) return raw;
|
||
const dbTypeLower = (dbType || '').toLowerCase();
|
||
|
||
if (dbTypeLower === 'mysql' || dbTypeLower === 'tdengine') {
|
||
return `\`${raw.replace(/`/g, '``')}\``;
|
||
}
|
||
|
||
// 对于 KingBase/PostgreSQL,只在必要时加引号
|
||
if (dbTypeLower === 'kingbase' || dbTypeLower === 'postgres') {
|
||
if (needsQuote(raw)) {
|
||
return `"${raw.replace(/"/g, '""')}"`;
|
||
}
|
||
// 不加引号,保持原样(数据库会自动转小写处理)
|
||
return raw;
|
||
}
|
||
|
||
// 其他数据库默认加双引号
|
||
return `"${raw.replace(/"/g, '""')}"`;
|
||
};
|
||
|
||
export const quoteQualifiedIdent = (dbType: string, ident: string) => {
|
||
const raw = (ident || '').trim();
|
||
if (!raw) return raw;
|
||
const parts = raw.split('.').map(normalizeIdentPart).filter(Boolean);
|
||
if (parts.length <= 1) return quoteIdentPart(dbType, raw);
|
||
return parts.map(p => quoteIdentPart(dbType, p)).join('.');
|
||
};
|
||
|
||
export const escapeLiteral = (val: string) => (val || '').replace(/'/g, "''");
|
||
|
||
export const parseListValues = (val: string) => {
|
||
const raw = (val || '').trim();
|
||
if (!raw) return [];
|
||
return raw
|
||
.split(/[\n,,]+/)
|
||
.map(s => s.trim())
|
||
.filter(Boolean);
|
||
};
|
||
|
||
export const buildWhereSQL = (dbType: string, conditions: FilterCondition[]) => {
|
||
const whereParts: string[] = [];
|
||
|
||
(conditions || []).forEach((cond) => {
|
||
if (cond?.enabled === false) return;
|
||
|
||
const op = (cond?.op || '').trim();
|
||
const column = (cond?.column || '').trim();
|
||
const value = (cond?.value ?? '').toString();
|
||
const value2 = (cond?.value2 ?? '').toString();
|
||
|
||
if (op === 'CUSTOM') {
|
||
const expr = value.trim();
|
||
if (expr) whereParts.push(`(${expr})`);
|
||
return;
|
||
}
|
||
|
||
if (!column) return;
|
||
|
||
const col = quoteIdentPart(dbType, column);
|
||
|
||
switch (op) {
|
||
case 'IS_NULL':
|
||
whereParts.push(`${col} IS NULL`);
|
||
return;
|
||
case 'IS_NOT_NULL':
|
||
whereParts.push(`${col} IS NOT NULL`);
|
||
return;
|
||
case 'IS_EMPTY':
|
||
// 兼容:空值通常理解为 NULL 或空字符串
|
||
whereParts.push(`(${col} IS NULL OR ${col} = '')`);
|
||
return;
|
||
case 'IS_NOT_EMPTY':
|
||
whereParts.push(`(${col} IS NOT NULL AND ${col} <> '')`);
|
||
return;
|
||
case 'BETWEEN': {
|
||
const v1 = value.trim();
|
||
const v2 = value2.trim();
|
||
if (!v1 || !v2) return;
|
||
whereParts.push(`${col} BETWEEN '${escapeLiteral(v1)}' AND '${escapeLiteral(v2)}'`);
|
||
return;
|
||
}
|
||
case 'NOT_BETWEEN': {
|
||
const v1 = value.trim();
|
||
const v2 = value2.trim();
|
||
if (!v1 || !v2) return;
|
||
whereParts.push(`${col} NOT BETWEEN '${escapeLiteral(v1)}' AND '${escapeLiteral(v2)}'`);
|
||
return;
|
||
}
|
||
case 'IN': {
|
||
const items = parseListValues(value);
|
||
if (items.length === 0) return;
|
||
const list = items.map(v => `'${escapeLiteral(v)}'`).join(', ');
|
||
whereParts.push(`${col} IN (${list})`);
|
||
return;
|
||
}
|
||
case 'NOT_IN': {
|
||
const items = parseListValues(value);
|
||
if (items.length === 0) return;
|
||
const list = items.map(v => `'${escapeLiteral(v)}'`).join(', ');
|
||
whereParts.push(`${col} NOT IN (${list})`);
|
||
return;
|
||
}
|
||
case 'CONTAINS': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} LIKE '%${escapeLiteral(v)}%'`);
|
||
return;
|
||
}
|
||
case 'NOT_CONTAINS': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} NOT LIKE '%${escapeLiteral(v)}%'`);
|
||
return;
|
||
}
|
||
case 'STARTS_WITH': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} LIKE '${escapeLiteral(v)}%'`);
|
||
return;
|
||
}
|
||
case 'NOT_STARTS_WITH': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} NOT LIKE '${escapeLiteral(v)}%'`);
|
||
return;
|
||
}
|
||
case 'ENDS_WITH': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} LIKE '%${escapeLiteral(v)}'`);
|
||
return;
|
||
}
|
||
case 'NOT_ENDS_WITH': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} NOT LIKE '%${escapeLiteral(v)}'`);
|
||
return;
|
||
}
|
||
case '=':
|
||
case '!=':
|
||
case '<':
|
||
case '<=':
|
||
case '>':
|
||
case '>=': {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} ${op} '${escapeLiteral(v)}'`);
|
||
return;
|
||
}
|
||
default: {
|
||
// 兼容旧值:LIKE
|
||
if (op.toUpperCase() === 'LIKE') {
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} LIKE '%${escapeLiteral(v)}%'`);
|
||
return;
|
||
}
|
||
|
||
const v = value.trim();
|
||
if (!v) return;
|
||
whereParts.push(`${col} ${op} '${escapeLiteral(v)}'`);
|
||
}
|
||
}
|
||
});
|
||
|
||
return whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
|
||
};
|