This commit is contained in:
Ethanfly 2025-12-31 12:19:05 +08:00
parent 8761b7cc23
commit bfb1d31cb0
3889 changed files with 12069 additions and 65729 deletions

View File

@ -3,10 +3,10 @@
<div align="center">
![EasySQL](https://img.shields.io/badge/EasySQL-v2.0-06b6d4?style=for-the-badge)
![Tauri](https://img.shields.io/badge/Tauri-2.0-ffc131?style=for-the-badge&logo=tauri)
![Electron](https://img.shields.io/badge/Electron-33-47848f?style=for-the-badge&logo=electron)
![React](https://img.shields.io/badge/React-18-61dafb?style=for-the-badge&logo=react)
![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?style=for-the-badge&logo=typescript)
![Rust](https://img.shields.io/badge/Rust-orange?style=for-the-badge&logo=rust)
![Node.js](https://img.shields.io/badge/Node.js-18+-339933?style=for-the-badge&logo=node.js)
**现代化多数据库管理工具**
@ -18,36 +18,35 @@
## ✨ 特性
- 🚀 **超轻量** - 基于 Tauri 2.0 + Rust安装包仅 ~10MB
- ⚡ **高性能** - Rust 原生数据库驱动,毫秒级响应
- 🎨 **精美 UI** - Windows Metro 风格,深色主题
- 🔌 **多数据库** - 支持 MySQL、PostgreSQL、SQLite、SQL Server 等
- 🔐 **SSH 隧道** - 安全连接远程数据库
- 📝 **智能编辑器** - SQL 语法高亮、智能补全、代码片段
- 📊 **数据编辑** - 支持直接编辑表格数据
- 📤 **导入导出** - 支持 JSON、Navicat NCX 格式
- 🚀 **跨平台** - 基于 Electron支持 Windows、macOS、Linux
- ⚡ **高性能** - 原生数据库驱动,毫秒级响应
- 🎨 **精美 UI** - Windows Metro 风格,深色主题,无边框窗口
- 🔌 **多数据库** - 支持 MySQL、PostgreSQL、SQLite、SQL Server、MongoDB、Redis、MariaDB
- 📝 **智能编辑器** - Monaco EditorSQL 语法高亮、智能补全
- 📊 **数据编辑** - 支持直接编辑表格数据,虚拟滚动大数据量
- 🛠️ **表设计器** - Navicat 风格,可视化编辑字段、索引、外键、表选项
- 🗃️ **完整管理** - 创建/删除/重命名/复制数据库和表
- 📤 **导入导出** - 支持 JSON、Navicat NCX 格式连接配置导入导出
- 🔄 **批量操作** - 支持多选连接批量删除管理
## 🗃️ 支持的数据库
| 数据库 | 状态 | 说明 |
| 数据库 | 状态 | 驱动 |
|--------|------|------|
| 🐬 MySQL | ✅ | 完全支持 |
| 🐘 PostgreSQL | ✅ | 完全支持 |
| 💾 SQLite | ✅ | 完全支持 |
| 📊 SQL Server | ✅ | 完全支持 |
| 🦭 MariaDB | ✅ | 完全支持 |
| 🍃 MongoDB | 🔜 | 开发中 |
| ⚡ Redis | 🔜 | 开发中 |
| 🔶 Oracle | 🔜 | 计划中 |
| ❄️ Snowflake | 🔜 | 计划中 |
| 🐬 MySQL | ✅ | mysql2 |
| 🐘 PostgreSQL | ✅ | pg |
| 💾 SQLite | ✅ | sql.js |
| 📊 SQL Server | ✅ | mssql |
| 🦭 MariaDB | ✅ | mysql2 |
| 🍃 MongoDB | ✅ | mongodb |
| ⚡ Redis | ✅ | ioredis |
## 🚀 快速开始
### 环境要求
- Node.js 18+
- Rust (rustup)
- [Tauri 依赖](https://tauri.app/v1/guides/getting-started/prerequisites)
- npm 或 yarn
### 安装
@ -60,10 +59,10 @@ cd easysql
npm install
# 开发模式运行
npm run tauri:dev
npm run electron:dev
# 构建应用
npm run tauri:build
npm run electron:build
```
## 📸 界面预览
@ -92,35 +91,34 @@ npm run tauri:build
## 🛠️ 技术栈
- **运行时**: Tauri 2.0 (Rust + WebView)
- **后端**: Rust + SQLx + Tiberius
- **运行时**: Electron 33
- **前端**: React 18 + TypeScript 5
- **样式**: Tailwind CSS 3
- **构建**: Vite 5
- **编辑器**: Monaco Editor
- **数据库驱动**: mysql2, pg, sql.js, mssql, mongodb, ioredis
## 📁 项目结构
```
easysql/
├── src-tauri/ # Tauri/Rust 后端
│ ├── src/
│ │ ├── main.rs # 主程序入口
│ │ ├── commands.rs # Tauri 命令
│ │ ├── database.rs # 数据库连接管理
│ │ ├── config.rs # 配置管理
│ │ └── ssh.rs # SSH 隧道
│ ├── Cargo.toml
│ └── tauri.conf.json
├── electron/ # Electron 主进程
│ ├── main.js # 主程序入口
│ └── preload.js # 预加载脚本
├── src/ # React 前端
│ ├── components/ # UI 组件
│ │ ├── TitleBar.tsx
│ │ ├── Sidebar.tsx
│ │ ├── MainContent.tsx
│ │ ├── SqlEditor.tsx
│ │ └── ConnectionModal.tsx
│ │ ├── TitleBar.tsx # 标题栏
│ │ ├── Sidebar.tsx # 侧边栏(连接/数据库/表树)
│ │ ├── MainContent.tsx # 主内容区
│ │ ├── SqlEditor.tsx # SQL 编辑器Monaco
│ │ ├── VirtualDataTable.tsx # 虚拟滚动数据表格
│ │ ├── TableDesigner.tsx # 表设计器Navicat 风格)
│ │ ├── ConnectionModal.tsx # 连接配置弹窗
│ │ ├── CreateDatabaseModal.tsx # 新建数据库弹窗
│ │ ├── CreateTableModal.tsx # 快速新建表弹窗
│ │ └── InputDialog.tsx # 通用输入对话框
│ ├── lib/
│ │ ├── tauri-api.ts # Tauri API 封装
│ │ ├── electron-api.ts # Electron API 封装
│ │ └── hooks.ts # 自定义 Hooks
│ ├── App.tsx
│ ├── types.ts
@ -143,6 +141,8 @@ easysql/
| `Ctrl+Q` | 新建查询 |
| `Ctrl+W` | 关闭当前标签 |
| `Ctrl+F` | 搜索(侧边栏/表格) |
| `双击连接` | 快速连接数据库 |
| `右键菜单` | 连接/数据库/表操作 |
## 🔧 配置说明
@ -151,6 +151,15 @@ easysql/
- macOS: `~/Library/Application Support/easysql/connections.json`
- Linux: `~/.config/easysql/connections.json`
## 📦 npm 脚本
| 命令 | 说明 |
|------|------|
| `npm run dev` | 启动 Vite 开发服务器 |
| `npm run build` | 构建前端资源 |
| `npm run electron:dev` | 开发模式运行 Electron |
| `npm run electron:build` | 打包 Electron 应用 |
## 🤝 贡献
欢迎提交 Issue 和 Pull Request
@ -162,5 +171,5 @@ MIT
---
<div align="center">
Made with ❤️ using Tauri + React + Rust
Made with ❤️ using Electron + React + Node.js
</div>

35
app-icon.svg Normal file
View File

@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#4f46e5"/>
</linearGradient>
<linearGradient id="db" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#ffffff"/>
<stop offset="100%" style="stop-color:#e0e7ff"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1024" height="1024" rx="180" fill="url(#bg)"/>
<!-- Database Icon -->
<g transform="translate(512, 512)">
<!-- Database cylinder -->
<ellipse cx="0" cy="-200" rx="220" ry="70" fill="url(#db)" stroke="#c7d2fe" stroke-width="8"/>
<rect x="-220" y="-200" width="440" height="400" fill="url(#db)"/>
<ellipse cx="0" cy="200" rx="220" ry="70" fill="url(#db)" stroke="#c7d2fe" stroke-width="8"/>
<!-- Middle lines -->
<ellipse cx="0" cy="-50" rx="220" ry="70" fill="none" stroke="#a5b4fc" stroke-width="6"/>
<ellipse cx="0" cy="100" rx="220" ry="70" fill="none" stroke="#a5b4fc" stroke-width="6"/>
<!-- Side borders -->
<line x1="-220" y1="-200" x2="-220" y2="200" stroke="#c7d2fe" stroke-width="8"/>
<line x1="220" y1="-200" x2="220" y2="200" stroke="#c7d2fe" stroke-width="8"/>
<!-- SQL text -->
<text x="0" y="350" text-anchor="middle" font-family="Arial, sans-serif" font-size="120" font-weight="bold" fill="#ffffff">SQL</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1981
electron/main.js Normal file

File diff suppressed because it is too large Load Diff

74
electron/preload.js Normal file
View File

@ -0,0 +1,74 @@
const { contextBridge, ipcRenderer } = require('electron')
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 窗口控制
minimize: () => ipcRenderer.invoke('window:minimize'),
maximize: () => ipcRenderer.invoke('window:maximize'),
close: () => ipcRenderer.invoke('window:close'),
// 配置存储
loadConnections: () => ipcRenderer.invoke('config:load'),
saveConnections: (connections) => ipcRenderer.invoke('config:save', connections),
// 数据库操作
testConnection: (config) => ipcRenderer.invoke('db:test', config),
connect: (config) => ipcRenderer.invoke('db:connect', config),
disconnect: (id) => ipcRenderer.invoke('db:disconnect', id),
query: (id, sql) => ipcRenderer.invoke('db:query', id, sql),
getDatabases: (id) => ipcRenderer.invoke('db:getDatabases', id),
getTables: (id, database) => ipcRenderer.invoke('db:getTables', id, database),
getColumns: (id, database, table) => ipcRenderer.invoke('db:getColumns', id, database, table),
getTableData: (id, database, table, page, pageSize) =>
ipcRenderer.invoke('db:getTableData', id, database, table, page, pageSize),
updateRow: (id, database, table, primaryKey, updates) =>
ipcRenderer.invoke('db:updateRow', id, database, table, primaryKey, updates),
deleteRow: (id, database, table, primaryKey) =>
ipcRenderer.invoke('db:deleteRow', id, database, table, primaryKey),
// 数据库管理
createDatabase: (id, dbName, charset, collation) =>
ipcRenderer.invoke('db:createDatabase', id, dbName, charset, collation),
dropDatabase: (id, dbName) =>
ipcRenderer.invoke('db:dropDatabase', id, dbName),
// 表管理
createTable: (id, database, tableName, columns) =>
ipcRenderer.invoke('db:createTable', id, database, tableName, columns),
dropTable: (id, database, tableName) =>
ipcRenderer.invoke('db:dropTable', id, database, tableName),
truncateTable: (id, database, tableName) =>
ipcRenderer.invoke('db:truncateTable', id, database, tableName),
renameTable: (id, database, oldName, newName) =>
ipcRenderer.invoke('db:renameTable', id, database, oldName, newName),
duplicateTable: (id, database, sourceTable, newTable, withData) =>
ipcRenderer.invoke('db:duplicateTable', id, database, sourceTable, newTable, withData),
// 列管理
addColumn: (id, database, tableName, column) =>
ipcRenderer.invoke('db:addColumn', id, database, tableName, column),
modifyColumn: (id, database, tableName, oldName, column) =>
ipcRenderer.invoke('db:modifyColumn', id, database, tableName, oldName, column),
dropColumn: (id, database, tableName, columnName) =>
ipcRenderer.invoke('db:dropColumn', id, database, tableName, columnName),
// 表设计器相关
getTableInfo: (id, database, tableName) =>
ipcRenderer.invoke('db:getTableInfo', id, database, tableName),
getIndexes: (id, database, tableName) =>
ipcRenderer.invoke('db:getIndexes', id, database, tableName),
getForeignKeys: (id, database, tableName) =>
ipcRenderer.invoke('db:getForeignKeys', id, database, tableName),
getColumnNames: (id, database, tableName) =>
ipcRenderer.invoke('db:getColumnNames', id, database, tableName),
executeMultiSQL: (id, sqls) =>
ipcRenderer.invoke('db:executeMultiSQL', id, sqls),
// 文件操作
openFile: () => ipcRenderer.invoke('file:open'),
saveFile: (filePath, content) => ipcRenderer.invoke('file:save', filePath, content),
selectFile: (extensions) => ipcRenderer.invoke('file:select', extensions),
saveDialog: (options) => ipcRenderer.invoke('file:saveDialog', options),
writeFile: (filePath, content) => ipcRenderer.invoke('file:write', filePath, content),
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath)
})

5567
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,33 +2,65 @@
"name": "easysql",
"version": "1.0.0",
"description": "Modern Database Management Tool",
"main": "electron/main.js",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
"electron:build": "vite build && electron-builder"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@tauri-apps/api": "^2.9.1",
"ioredis": "^5.8.2",
"lucide-react": "^0.294.0",
"monaco-editor": "^0.55.1",
"sql-formatter": "^15.6.12"
"mongodb": "^6.21.0",
"mssql": "^11.0.1",
"mysql2": "^3.11.0",
"pg": "^8.13.0",
"sql-formatter": "^15.6.12",
"sql.js": "^1.11.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.9.6",
"@types/node": "^20.10.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"concurrently": "^9.1.0",
"electron": "^33.2.1",
"electron-builder": "^25.1.8",
"postcss": "^8.4.32",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.6",
"typescript": "^5.3.0",
"vite": "^5.0.0"
"vite": "^5.0.0",
"wait-on": "^8.0.1"
},
"build": {
"appId": "com.easysql.app",
"productName": "EasySQL",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"electron/**/*"
],
"win": {
"target": "nsis",
"icon": "public/icon.ico"
},
"mac": {
"target": "dmg",
"icon": "public/icon.icns"
},
"linux": {
"target": "AppImage",
"icon": "public/icon.png"
}
}
}

6847
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +0,0 @@
[package]
name = "easysql"
version = "1.0.0"
description = "A modern database management tool"
authors = ["EasySQL"]
edition = "2021"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-single-instance = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "postgres", "sqlite"] }
tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono"] }
tokio-util = { version = "0.7", features = ["compat"] }
async-std = { version = "1", features = ["attributes"] }
ssh2 = "0.9"
uuid = { version = "1", features = ["v4"] }
dirs = "5"
chrono = { version = "0.4", features = ["serde"] }
thiserror = "1"
parking_lot = "0.12"
once_cell = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
rfd = "0.14"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
strip = true

View File

@ -1,4 +0,0 @@
fn main() {
tauri_build::build()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
use crate::database::ConnectionConfig;
use std::fs;
use std::path::PathBuf;
fn get_config_path() -> PathBuf {
let config_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("easysql");
fs::create_dir_all(&config_dir).ok();
config_dir.join("connections.json")
}
pub fn save_connections(connections: &[ConnectionConfig]) -> Result<(), std::io::Error> {
let path = get_config_path();
let json = serde_json::to_string_pretty(connections)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
fs::write(path, json)
}
pub fn load_connections() -> Result<Vec<ConnectionConfig>, std::io::Error> {
let path = get_config_path();
if !path.exists() {
return Ok(vec![]);
}
let content = fs::read_to_string(path)?;
serde_json::from_str(&content)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}

View File

@ -1,262 +0,0 @@
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DbError {
#[error("连接失败: {0}")]
ConnectionError(String),
#[error("查询失败: {0}")]
QueryError(String),
#[error("未连接")]
NotConnected,
#[error("不支持的数据库类型: {0}")]
UnsupportedType(String),
#[error("SSH 隧道错误: {0}")]
SshError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConnectionConfig {
pub id: String,
#[serde(rename = "type")]
pub db_type: String,
pub name: String,
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub database: Option<String>,
pub ssh_enabled: Option<bool>,
pub ssh_host: Option<String>,
pub ssh_port: Option<u16>,
pub ssh_user: Option<String>,
pub ssh_password: Option<String>,
pub ssh_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableInfo {
pub name: String,
pub rows: i64,
#[serde(rename = "isView")]
pub is_view: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnInfo {
pub name: String,
#[serde(rename = "type")]
pub data_type: String,
pub nullable: bool,
pub key: Option<String>,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Vec<serde_json::Value>>,
pub error: Option<String>,
#[serde(rename = "affectedRows")]
pub affected_rows: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableDataResult {
pub columns: Vec<ColumnInfo>,
pub rows: Vec<Vec<serde_json::Value>>,
pub total: i64,
pub page: i32,
#[serde(rename = "pageSize")]
pub page_size: i32,
}
// 数据库连接枚举
pub enum DbConnection {
MySql(sqlx::MySqlPool),
Postgres(sqlx::PgPool),
Sqlite(sqlx::SqlitePool),
SqlServer(SqlServerConnection),
}
pub struct SqlServerConnection {
pub config: tiberius::Config,
}
// 连接信息存储
pub struct ConnectionInfo {
pub connection: DbConnection,
pub config: ConnectionConfig,
pub ssh_tunnel: Option<crate::ssh::SshTunnel>,
}
// 全局连接管理器
pub static CONNECTIONS: Lazy<RwLock<HashMap<String, Arc<ConnectionInfo>>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub fn init() {
tracing::info!("数据库管理器初始化完成");
}
impl DbConnection {
pub async fn test_mysql(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<(), DbError> {
let db = database.unwrap_or("mysql");
let url = format!("mysql://{}:{}@{}:{}/{}", user, password, host, port, db);
let pool = sqlx::mysql::MySqlPoolOptions::new()
.max_connections(1)
.acquire_timeout(std::time::Duration::from_secs(10))
.connect(&url)
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
sqlx::query("SELECT 1")
.execute(&pool)
.await
.map_err(|e| DbError::QueryError(e.to_string()))?;
pool.close().await;
Ok(())
}
pub async fn test_postgres(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<(), DbError> {
let db = database.unwrap_or("postgres");
let url = format!("postgres://{}:{}@{}:{}/{}", user, password, host, port, db);
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.acquire_timeout(std::time::Duration::from_secs(10))
.connect(&url)
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
sqlx::query("SELECT 1")
.execute(&pool)
.await
.map_err(|e| DbError::QueryError(e.to_string()))?;
pool.close().await;
Ok(())
}
pub async fn test_sqlite(path: &str) -> Result<(), DbError> {
let url = format!("sqlite:{}?mode=rwc", path);
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect(&url)
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
sqlx::query("SELECT 1")
.execute(&pool)
.await
.map_err(|e| DbError::QueryError(e.to_string()))?;
pool.close().await;
Ok(())
}
pub async fn test_sqlserver(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<(), DbError> {
use tiberius::{Client, Config, AuthMethod};
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
let mut config = Config::new();
config.host(host);
config.port(port);
config.authentication(AuthMethod::sql_server(user, password));
if let Some(db) = database {
config.database(db);
}
config.trust_cert();
let tcp = TcpStream::connect(config.get_addr())
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
tcp.set_nodelay(true).ok();
let mut client = Client::connect(config, tcp.compat_write())
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
client.simple_query("SELECT 1")
.await
.map_err(|e| DbError::QueryError(e.to_string()))?;
Ok(())
}
pub async fn connect_mysql(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<Self, DbError> {
let db = database.unwrap_or("mysql");
let url = format!("mysql://{}:{}@{}:{}/{}", user, password, host, port, db);
let pool = sqlx::mysql::MySqlPoolOptions::new()
.max_connections(10)
.min_connections(1)
.acquire_timeout(std::time::Duration::from_secs(30))
.idle_timeout(std::time::Duration::from_secs(600))
.connect(&url)
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
Ok(DbConnection::MySql(pool))
}
pub async fn connect_postgres(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<Self, DbError> {
let db = database.unwrap_or("postgres");
let url = format!("postgres://{}:{}@{}:{}/{}", user, password, host, port, db);
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(10)
.min_connections(1)
.acquire_timeout(std::time::Duration::from_secs(30))
.idle_timeout(std::time::Duration::from_secs(600))
.connect(&url)
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
Ok(DbConnection::Postgres(pool))
}
pub async fn connect_sqlite(path: &str) -> Result<Self, DbError> {
let url = format!("sqlite:{}?mode=rwc", path);
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(5)
.connect(&url)
.await
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
Ok(DbConnection::Sqlite(pool))
}
pub async fn connect_sqlserver(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<Self, DbError> {
let mut config = tiberius::Config::new();
config.host(host);
config.port(port);
config.authentication(tiberius::AuthMethod::sql_server(user, password));
if let Some(db) = database {
config.database(db);
}
config.trust_cert();
Ok(DbConnection::SqlServer(SqlServerConnection { config }))
}
}
// 解析 localhost 为 127.0.0.1
pub fn resolve_host(host: &str) -> String {
if host == "localhost" {
"127.0.0.1".to_string()
} else {
host.to_string()
}
}

View File

@ -1,74 +0,0 @@
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod database;
mod commands;
mod config;
mod ssh;
use tauri::Manager;
use tracing_subscriber;
fn main() {
// 初始化日志
tracing_subscriber::fmt::init();
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// 当尝试打开第二个实例时,聚焦现有窗口
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
let _ = window.unminimize();
}
}))
.setup(|app| {
// 初始化数据库连接管理器
database::init();
// 获取主窗口并设置
if let Some(window) = app.get_webview_window("main") {
// Windows 上启用窗口阴影效果
#[cfg(target_os = "windows")]
{
use tauri::WebviewWindow;
let _ = window.set_decorations(false);
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
// 窗口控制
commands::window_minimize,
commands::window_maximize,
commands::window_close,
// 数据库操作
commands::db_test,
commands::db_connect,
commands::db_disconnect,
commands::db_query,
commands::db_get_databases,
commands::db_get_tables,
commands::db_get_columns,
commands::db_get_table_data,
commands::db_update_row,
commands::db_delete_row,
commands::db_backup,
commands::db_export_table,
// 配置操作
commands::config_save,
commands::config_load,
commands::config_export,
commands::config_import,
// 文件操作
commands::file_open,
commands::file_save,
commands::file_select,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,167 +0,0 @@
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::sync::Arc;
use std::thread;
use ssh2::Session;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SshError {
#[error("连接失败: {0}")]
ConnectionError(String),
#[error("认证失败: {0}")]
AuthError(String),
#[error("隧道创建失败: {0}")]
TunnelError(String),
}
pub struct SshTunnel {
pub local_port: u16,
_handle: Option<thread::JoinHandle<()>>,
}
impl SshTunnel {
pub async fn create(
ssh_host: &str,
ssh_port: u16,
ssh_user: &str,
ssh_password: Option<&str>,
ssh_key: Option<&str>,
remote_host: &str,
remote_port: u16,
) -> Result<Self, SshError> {
// 找一个可用的本地端口
let listener = TcpListener::bind("127.0.0.1:0")
.map_err(|e| SshError::TunnelError(e.to_string()))?;
let local_port = listener.local_addr()
.map_err(|e| SshError::TunnelError(e.to_string()))?
.port();
let ssh_host = ssh_host.to_string();
let ssh_user = ssh_user.to_string();
let ssh_password = ssh_password.map(|s| s.to_string());
let ssh_key = ssh_key.map(|s| s.to_string());
let remote_host = remote_host.to_string();
// 在后台线程中运行隧道
let handle = thread::spawn(move || {
run_tunnel(
listener,
&ssh_host,
ssh_port,
&ssh_user,
ssh_password.as_deref(),
ssh_key.as_deref(),
&remote_host,
remote_port,
);
});
// 等待一小段时间确保隧道建立
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
Ok(SshTunnel {
local_port,
_handle: Some(handle),
})
}
}
fn run_tunnel(
listener: TcpListener,
ssh_host: &str,
ssh_port: u16,
ssh_user: &str,
ssh_password: Option<&str>,
ssh_key: Option<&str>,
remote_host: &str,
remote_port: u16,
) {
// 连接 SSH 服务器
let tcp = match TcpStream::connect(format!("{}:{}", ssh_host, ssh_port)) {
Ok(t) => t,
Err(e) => {
tracing::error!("SSH 连接失败: {}", e);
return;
}
};
let mut sess = match Session::new() {
Ok(s) => s,
Err(e) => {
tracing::error!("创建 SSH 会话失败: {}", e);
return;
}
};
sess.set_tcp_stream(tcp);
if let Err(e) = sess.handshake() {
tracing::error!("SSH 握手失败: {}", e);
return;
}
// 认证
let auth_result = if let Some(key_path) = ssh_key {
sess.userauth_pubkey_file(ssh_user, None, std::path::Path::new(key_path), None)
} else if let Some(password) = ssh_password {
sess.userauth_password(ssh_user, password)
} else {
tracing::error!("SSH 需要密码或密钥");
return;
};
if let Err(e) = auth_result {
tracing::error!("SSH 认证失败: {}", e);
return;
}
let sess = Arc::new(sess);
// 监听本地连接并转发
for stream in listener.incoming() {
match stream {
Ok(mut local_stream) => {
let sess = sess.clone();
let remote_host = remote_host.to_string();
thread::spawn(move || {
match sess.channel_direct_tcpip(&remote_host, remote_port, None) {
Ok(mut channel) => {
let mut buf = [0u8; 8192];
loop {
// 从本地读取
match local_stream.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if channel.write_all(&buf[..n]).is_err() {
break;
}
}
Err(_) => break,
}
// 从远程读取
match channel.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if local_stream.write_all(&buf[..n]).is_err() {
break;
}
}
Err(_) => break,
}
}
}
Err(e) => {
tracing::error!("创建 SSH 通道失败: {}", e);
}
}
});
}
Err(e) => {
tracing::error!("接受本地连接失败: {}", e);
}
}
}
}

View File

@ -1 +0,0 @@
{"rustc_fingerprint":2442455887007604170,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Ethan\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: x86_64-pc-windows-msvc\nrelease: 1.92.0\nLLVM version: 21.1.3\n","stderr":""}},"successes":{}}

View File

@ -1,3 +0,0 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":2225463790103693989,"path":14185826767970760088,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\adler2-963931fa5ded092c\\dep-lib-adler2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":15657897354478470176,"path":5842747710153497419,"deps":[[198136567835728122,"memchr",false,14663827455635897954]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\aho-corasick-753d9ff25945fcf1\\dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"unsafe\"]","target":1942380541186272485,"profile":15657897354478470176,"path":15208557227947978230,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\alloc-no-stdlib-df988be783143443\\dep-lib-alloc_no_stdlib","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"unsafe\"]","target":8756844401079878655,"profile":15657897354478470176,"path":13188036246537209495,"deps":[[9611597350722197978,"alloc_no_stdlib",false,13084831196644280031]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\alloc-stdlib-0b919bce3bf82dab\\dep-lib-alloc_stdlib","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"alloc\"]","declared_features":"[\"alloc\", \"default\", \"fresh-rust\", \"nightly\", \"serde\", \"std\"]","target":5388200169723499962,"profile":12994027242049262075,"path":9537980989915801285,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\allocator-api2-8865e6a00b4e72ee\\dep-lib-allocator_api2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[1852463361802237065,"build_script_build",false,11562533667458260168]],"local":[{"RerunIfChanged":{"output":"debug\\build\\anyhow-5f742c575df35aa8\\output","paths":["src/nightly.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":16100955855663461252,"profile":15657897354478470176,"path":4793198230926724775,"deps":[[1852463361802237065,"build_script_build",false,18037571225574304381]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\anyhow-aa601dd51ab6285d\\dep-lib-anyhow","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":17883862002600103897,"profile":2225463790103693989,"path":16467683319812530953,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\anyhow-d034abc77daf7f05\\dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"portable-atomic\", \"std\"]","target":2348331682808714104,"profile":15657897354478470176,"path":7386643265001597429,"deps":[[1906322745568073236,"pin_project_lite",false,2763931684887686990],[7620660491849607393,"futures_core",false,13482104094475111900],[12100481297174703255,"concurrent_queue",false,9710515496302887928],[17148897597675491682,"event_listener_strategy",false,17468647974699608726]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-channel-012b9538802153a7\\dep-lib-async_channel","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[15550619062825872913,"build_script_build",false,11785065948707700023]],"local":[{"Precalculated":"2.6.0"}],"rustflags":[],"config":0,"compile_kind":0}

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"tracing\"]","target":5408242616063297496,"profile":4831801323318853768,"path":7761328004364916373,"deps":[[13927012481677012980,"autocfg",false,10807305518230055627]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-io-6ca54b3d18dcec1d\\dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"tracing\"]","target":10084595033463382892,"profile":17582455124764123298,"path":14659531420356540962,"deps":[[5103565458935487,"futures_io",false,952584329958915671],[189982446159473706,"parking",false,15973587823657450042],[6568467691589961976,"windows_sys",false,7246976454518788371],[7667230146095136825,"cfg_if",false,2895588346767177823],[9090520973410485560,"futures_lite",false,2780583266334981273],[12100481297174703255,"concurrent_queue",false,9710515496302887928],[14271827750077741315,"polling",false,3541362140770724921],[14767213526276824509,"slab",false,2368147902018235069],[15550619062825872913,"build_script_build",false,14040693424745460393],[18377328279789821306,"rustix",false,14707779014435926099]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-io-ff38d4d37548e5d4\\dep-lib-async_io","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"portable-atomic\", \"std\"]","target":9397226730057430065,"profile":15657897354478470176,"path":10776882996198157094,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-task-83aaeca8303bab8f\\dep-lib-async_task","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
91f45a8f710d237a

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":2515742790907851906,"profile":15657897354478470176,"path":14408917710461266522,"deps":[[5157631553186200874,"num_traits",false,2487166575225505192]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\atoi-3b1ff09aa596eaea\\dep-lib-atoi","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
ae00a7479dbd0d3e

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":2515742790907851906,"profile":15657897354478470176,"path":14408917710461266522,"deps":[[5157631553186200874,"num_traits",false,6589197341473135743]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\atoi-88fbb6b4610daefc\\dep-lib-atoi","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":15657897354478470176,"path":9403445180054510354,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\atomic-waker-0cc2f644b7474aaa\\dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":2225463790103693989,"path":11879731547235993323,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\autocfg-0837c6aef9c33bfd\\dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":15657897354478470176,"path":13544571482266270501,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64-15167b4203d0c3e5\\dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":15657897354478470176,"path":17663575163679045343,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64-859384f4ec5c3793\\dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":2225463790103693989,"path":13544571482266270501,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64-fbf8e75ae7768384\\dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@ -1 +0,0 @@
{"rustc":7895727629726570510,"features":"[\"alloc\"]","declared_features":"[\"alloc\", \"std\"]","target":15548948006327107948,"profile":15657897354478470176,"path":16698247819254552203,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64ct-b7895d159a254796\\dep-lib-base64ct","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@ -1 +0,0 @@
This file has an mtime of when this was started.

Some files were not shown because too many files have changed in this diff Show More