diff --git a/.gitignore b/.gitignore index 83f0230..42237f5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ pnpm-lock.yaml *.json !config*.json !src/components/charts/geo/*.json +!src/i18n/locales/*.json go.mod .RData .Rhistory diff --git a/src-tauri/src/db_connections.rs b/src-tauri/src/db_connections.rs new file mode 100644 index 0000000..c70c63d --- /dev/null +++ b/src-tauri/src/db_connections.rs @@ -0,0 +1,143 @@ +//! 数据库连接的独立存储表(codeforge.sqlite 中的 db_connections)。 +//! 从 KV 的 sql-connections JSON blob 抽出,便于将来按需扩展/分页/检索。 + +use crate::execution::get_codeforge_db_path; +use rusqlite::{Connection, OptionalExtension, params}; +use serde::{Deserialize, Serialize}; +use std::sync::Mutex as StdMutex; +use tauri::State; + +#[derive(Serialize, Deserialize, Clone)] +pub struct DbConnection { + pub id: String, + pub name: String, + pub kind: String, + #[serde(default)] + pub file: Option, + #[serde(default)] + pub host: Option, + #[serde(default)] + pub port: Option, + #[serde(default)] + pub user: Option, + #[serde(default)] + pub password: Option, + #[serde(default)] + pub database: Option, +} + +pub struct DbConnStore { + conn: StdMutex, +} + +impl DbConnStore { + pub fn new() -> Result { + let db_path = get_codeforge_db_path()?; + let conn = Connection::open(&db_path).map_err(|e| format!("打开数据库失败: {}", e))?; + let _ = conn.pragma_update(None, "journal_mode", "WAL"); + let _ = conn.pragma_update(None, "synchronous", "NORMAL"); + let _ = conn.busy_timeout(std::time::Duration::from_secs(5)); + conn.execute( + "CREATE TABLE IF NOT EXISTS db_connections ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + kind TEXT NOT NULL, + file TEXT, + host TEXT, + port INTEGER, + user TEXT, + password TEXT, + database TEXT, + sort_order INTEGER NOT NULL DEFAULT 0 + )", + [], + ) + .map_err(|e| format!("初始化连接表失败: {}", e))?; + Ok(Self { + conn: StdMutex::new(conn), + }) + } +} + +/// 列出全部连接(按 sort_order, name) +#[tauri::command] +pub async fn db_connections_list( + state: State<'_, DbConnStore>, +) -> Result, String> { + let conn = state.conn.lock().map_err(|_| "数据库锁错误".to_string())?; + let mut stmt = conn + .prepare( + "SELECT id, name, kind, file, host, port, user, password, database + FROM db_connections ORDER BY sort_order, name", + ) + .map_err(|e| format!("查询连接失败: {}", e))?; + let rows = stmt + .query_map([], |row| { + Ok(DbConnection { + id: row.get(0)?, + name: row.get(1)?, + kind: row.get(2)?, + file: row.get(3)?, + host: row.get(4)?, + port: row.get::<_, Option>(5)?.map(|v| v as u16), + user: row.get(6)?, + password: row.get(7)?, + database: row.get(8)?, + }) + }) + .map_err(|e| format!("读取连接失败: {}", e))?; + let mut out = Vec::new(); + for r in rows { + out.push(r.map_err(|e| format!("读取连接失败: {}", e))?); + } + Ok(out) +} + +/// 新增或更新一个连接(按 id upsert,更新时保留原有排序) +#[tauri::command] +pub async fn db_connection_save( + c: DbConnection, + state: State<'_, DbConnStore>, +) -> Result<(), String> { + let conn = state.conn.lock().map_err(|_| "数据库锁错误".to_string())?; + let exists = conn + .query_row( + "SELECT 1 FROM db_connections WHERE id = ?1", + params![c.id], + |_| Ok(()), + ) + .optional() + .map_err(|e| format!("查询连接失败: {}", e))? + .is_some(); + if exists { + conn.execute( + "UPDATE db_connections SET name=?2, kind=?3, file=?4, host=?5, port=?6, user=?7, password=?8, database=?9 WHERE id=?1", + params![c.id, c.name, c.kind, c.file, c.host, c.port, c.user, c.password, c.database], + ) + .map_err(|e| format!("更新连接失败: {}", e))?; + } else { + let next: i64 = conn + .query_row( + "SELECT COALESCE(MAX(sort_order), 0) + 1 FROM db_connections", + [], + |r| r.get(0), + ) + .unwrap_or(1); + conn.execute( + "INSERT INTO db_connections (id, name, kind, file, host, port, user, password, database, sort_order) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![c.id, c.name, c.kind, c.file, c.host, c.port, c.user, c.password, c.database, next], + ) + .map_err(|e| format!("保存连接失败: {}", e))?; + } + Ok(()) +} + +/// 删除一个连接 +#[tauri::command] +pub async fn db_connection_delete(id: String, state: State<'_, DbConnStore>) -> Result<(), String> { + let conn = state.conn.lock().map_err(|_| "数据库锁错误".to_string())?; + conn.execute("DELETE FROM db_connections WHERE id = ?1", params![id]) + .map_err(|e| format!("删除连接失败: {}", e))?; + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 444f908..85bcdb7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,6 +9,7 @@ mod cache; mod config; mod custom_plugin_commands; mod db; +mod db_connections; mod env_commands; mod env_manager; mod env_providers; @@ -38,6 +39,9 @@ use crate::custom_plugin_commands::{ update_custom_plugin, }; use crate::db::{run_sql, run_sql_paged}; +use crate::db_connections::{ + DbConnStore, db_connection_delete, db_connection_save, db_connections_list, +}; use crate::env_commands::{ EnvironmentManagerState, download_and_install_version, get_environment_info, get_supported_environment_languages, switch_environment_version, uninstall_environment_version, @@ -100,6 +104,7 @@ fn main() { .manage(AiHistory::new().expect("failed to initialize ai history database")) .manage(Snippets::new().expect("failed to initialize snippets database")) .manage(KvStore::new().expect("failed to initialize kv store database")) + .manage(DbConnStore::new().expect("failed to initialize db connections database")) .manage(TerminalState::new()) .manage(LspState::new()) .manage(ExecutionPluginManagerState::new(PluginManager::new())) @@ -225,6 +230,10 @@ fn main() { kv_get_all, kv_set, kv_delete, + // 数据库连接(独立表) + db_connections_list, + db_connection_save, + db_connection_delete, // 集成终端 terminal_create, terminal_write, diff --git a/src/App.vue b/src/App.vue index ffe2a55..a5897e6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -23,27 +23,27 @@
- - + +
- - + +
- - + +
@@ -78,7 +78,7 @@
-

{{ getLanguageDisplayName(currentLanguage) }} 代码编辑器

+

{{ getLanguageDisplayName(currentLanguage) }} {{ t('app.codeEditor') }}

· {{ currentFileName }} - + @@ -96,13 +96,13 @@
- AI 预测中… + {{ t('app.aiPredicting') }} - Tab 接受 · Esc 取消 - 行 {{ cursorInfo.line }}, 列 {{ cursorInfo.col }} - 已选 {{ cursorInfo.selLen }} - {{ (code || '').length }} 字符 - {{ (code || '').split('\n').length }} + {{ t('app.ghostHint') }} + {{ t('app.cursorPos', { line: cursorInfo.line, col: cursorInfo.col }) }} + {{ t('app.selected') }} {{ cursorInfo.selLen }} + {{ (code || '').length }} {{ t('app.chars') }} + {{ (code || '').split('\n').length }} {{ t('app.lines') }}
@@ -122,8 +122,8 @@
-

控制台

-
@@ -216,7 +216,7 @@
-

{{ getLanguageDisplayName(currentLanguage) }} 代码编辑器

+

{{ getLanguageDisplayName(currentLanguage) }} {{ t('app.codeEditor') }}

· {{ currentFileName }} - + @@ -234,13 +234,13 @@
- AI 预测中… + {{ t('app.aiPredicting') }} - Tab 接受 · Esc 取消 - 行 {{ cursorInfo.line }}, 列 {{ cursorInfo.col }} - 已选 {{ cursorInfo.selLen }} - {{ (code || '').length }} 字符 - {{ (code || '').split('\n').length }} + {{ t('app.ghostHint') }} + {{ t('app.cursorPos', { line: cursorInfo.line, col: cursorInfo.col }) }} + {{ t('app.selected') }} {{ cursorInfo.selLen }} + {{ (code || '').length }} {{ t('app.chars') }} + {{ (code || '').split('\n').length }} {{ t('app.lines') }}
@@ -285,15 +285,15 @@ @open-ai="openAiForExecution"/> - +

- 当前文件 {{ currentFileName }} 有未保存的修改,如何运行? + {{ t('app.runUnsavedPre') }}{{ currentFileName }}{{ t('app.runUnsavedPost') }}

- - - + + +
@@ -343,9 +343,9 @@ @@ -374,20 +374,20 @@ :style="{ top: `${editorCtx.y}px`, left: `${editorCtx.x}px` }" @click.stop>
@@ -413,6 +413,7 @@ diff --git a/src/components/LargeFileViewer.vue b/src/components/LargeFileViewer.vue index a5d8c7e..4874f1f 100644 --- a/src/components/LargeFileViewer.vue +++ b/src/components/LargeFileViewer.vue @@ -5,12 +5,12 @@
{{ fileName }} - 只读 - {{ humanSize }} · 共 {{ lineCount.toLocaleString() }} 行 + {{ t('largeFile.readonly') }} + {{ humanSize }} · {{ t('largeFile.lineCount', { n: lineCount.toLocaleString() }) }}
{{ rangeText }} -
@@ -34,8 +34,11 @@ diff --git a/src/components/QuickOpen.vue b/src/components/QuickOpen.vue index 6cfe82b..44fec6d 100644 --- a/src/components/QuickOpen.vue +++ b/src/components/QuickOpen.vue @@ -7,7 +7,7 @@
-
加载文件列表…
-
无匹配文件
+
{{ t('dialog.quickOpenLoading') }}
+
{{ t('dialog.quickOpenEmpty') }}
@@ -37,9 +37,12 @@ diff --git a/src/components/setting/Language.vue b/src/components/setting/Language.vue index f75d7f5..7f52871 100644 --- a/src/components/setting/Language.vue +++ b/src/components/setting/Language.vue @@ -16,23 +16,23 @@ clearable size="sm" class="flex-1" - placeholder="筛选语言"/> + :placeholder="t('settings.language.filter')"/>
-

未找到匹配的语言

+

{{ t('settings.language.noMatch') }}

- 没有你需要的语言? + {{ t('settings.language.noLangQ') }}

@@ -59,7 +59,7 @@ @@ -136,38 +136,38 @@ - +
-
- +

- 确定要删除自定义语言 {{ languageToDelete }} 吗? + {{ t('settings.language.confirmDeletePre') }}{{ languageToDelete }}{{ t('settings.language.confirmDeletePost') }}

- - + +
@@ -194,12 +194,14 @@ import Select from "../../ui/Select.vue"; import Switch from '../../ui/Switch.vue' import EnvironmentManager from './EnvironmentManager.vue' import { useToast } from '../../plugins/toast' +import { useI18n } from 'vue-i18n' const emit = defineEmits<{ 'settings-changed': [config: PluginConfig] 'error': [message: string] }>() +const { t } = useI18n() const toast = useToast() const languageFilter = ref('') const showAddCustomLanguage = ref(false) @@ -257,7 +259,7 @@ const openIssues = async () => { await openUrl('https://github.com/devlive-community/codeforge/issues') } catch (error) { - toast.error('打开链接失败: ' + error) + toast.error(t('settings.language.openLinkFailed') + error) } } @@ -266,7 +268,7 @@ const selectIconFile = async () => { const selected = await openDialog({ multiple: false, filters: [{ - name: '图片文件', + name: t('settings.language.imageFileName'), extensions: ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp'] }] }) @@ -279,7 +281,7 @@ const selectIconFile = async () => { } } catch (error) { - toast.error('选择文件失败: ' + error) + toast.error(t('settings.language.selectFileFailed') + error) } } @@ -298,12 +300,12 @@ const deleteCustomLanguage = async () => { try { await invoke('remove_custom_plugin', { language }) - toast.success('自定义语言已删除') + toast.success(t('settings.language.customDeleted')) await reloadLanguages() await loadCustomLanguages() } catch (error: any) { - toast.error('删除失败: ' + error) + toast.error(t('settings.language.deleteFailed') + error) } } @@ -313,18 +315,18 @@ const loadCustomLanguages = async () => { customLanguages.value = plugins.map(p => p.language) } catch (error) { - console.error('加载自定义语言列表失败:', error) + console.error('load custom languages failed:', error) } } const addCustomLanguage = async () => { if (!newLanguage.value.language || !newLanguageName.value) { - toast.error('请填写语言标识和语言名称') + toast.error(t('settings.language.needIdName')) return } if (!newLanguage.value.extension) { - toast.error('请填写文件扩展名') + toast.error(t('settings.language.needExt')) return } @@ -344,7 +346,7 @@ const addCustomLanguage = async () => { } await invoke('add_custom_plugin', { config: newLanguage.value }) - toast.success('自定义语言添加成功') + toast.success(t('settings.language.addSuccess')) showAddCustomLanguage.value = false newLanguage.value = { @@ -367,7 +369,7 @@ const addCustomLanguage = async () => { await loadCustomLanguages() } catch (error: any) { - toast.error('添加失败: ' + error) + toast.error(t('settings.language.addFailed') + error) } } diff --git a/src/components/setting/Logs.vue b/src/components/setting/Logs.vue index 0da3e96..86249cf 100644 --- a/src/components/setting/Logs.vue +++ b/src/components/setting/Logs.vue @@ -4,21 +4,21 @@

- 日志设置 + {{ t('settings.logs.title') }}

-