This commit is contained in:
Baobhan Sith
2025-04-18 15:07:54 +08:00
parent 6609680bc1
commit 06723fc145
10 changed files with 216 additions and 124 deletions
+70 -86
View File
@@ -153,150 +153,134 @@ CREATE TABLE IF NOT EXISTS quick_commands (
// --- 结束新增表结构定义 ---
export const runMigrations = async (db: Database): Promise<void> => {
try {
// 创建 settings 表 (如果不存在)
await new Promise<void>((resolve, reject) => {
export const runMigrations = (db: Database): Promise<void> => {
// 使用 Promise 包装 serialize,以便在所有操作完成后解析
return new Promise<void>((resolveOuter, rejectOuter) => {
db.serialize(() => {
// 定义一个统一的错误处理函数
const handleError = (operation: string, err: Error | null) => {
if (err) {
const errorMsg = `${operation} 时出错: ${err.message}`;
console.error(errorMsg);
// 停止序列并拒绝外部 Promise
rejectOuter(new Error(errorMsg));
return true; // 表示有错误发生
}
return false; // 表示没有错误
};
// 创建 settings 表
db.run(createSettingsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 settings 表时出错: ${err.message}`));
if (handleError('创建 settings 表', err)) return;
console.log('Settings 表已检查/创建。');
resolve();
});
});
// 插入默认的 IP 白名单设置
await new Promise<void>((resolve, reject) => {
// 插入默认的 IP 白名单设置 (使用 INSERT OR IGNORE)
db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('ipWhitelistEnabled', 'false')", (err: Error | null) => {
if (err) return reject(new Error(`插入默认 ipWhitelistEnabled 设置时出错: ${err.message}`));
console.log('默认 ipWhitelistEnabled 设置已插入。');
resolve();
// 对于 INSERT OR IGNORE,即使发生 UNIQUE constraint 错误,err 也可能为 null 或特定错误
// 但 SQLITE_BUSY 是需要处理的
if (err && (err as any).code !== 'SQLITE_CONSTRAINT') { // 忽略约束错误,处理其他错误
if (handleError('插入默认 ipWhitelistEnabled 设置', err)) return;
} else if (err && (err as any).code === 'SQLITE_CONSTRAINT') {
console.log('默认 ipWhitelistEnabled 设置已存在,跳过插入。');
} else {
console.log('默认 ipWhitelistEnabled 设置已插入或已存在。');
}
});
});
await new Promise<void>((resolve, reject) => {
db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('ipWhitelist', '')", (err: Error | null) => {
if (err) return reject(new Error(`插入默认 ipWhitelist 设置时出错: ${err.message}`));
console.log('默认 ipWhitelist 设置已插入。');
resolve();
if (err && (err as any).code !== 'SQLITE_CONSTRAINT') {
if (handleError('插入默认 ipWhitelist 设置', err)) return;
} else if (err && (err as any).code === 'SQLITE_CONSTRAINT') {
console.log('默认 ipWhitelist 设置已存在,跳过插入。');
} else {
console.log('默认 ipWhitelist 设置已插入或已存在。');
}
});
});
// 创建 audit_logs 表 (如果不存在)
await new Promise<void>((resolve, reject) => {
// 创建 audit_logs 表
db.run(createAuditLogsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 audit_logs 表时出错: ${err.message}`));
if (handleError('创建 audit_logs 表', err)) return;
console.log('Audit_Logs 表已检查/创建。');
resolve();
});
});
// 创建 api_keys 表 (如果不存在)
await new Promise<void>((resolve, reject) => {
// 创建 api_keys 表
db.run(createApiKeysTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 api_keys 表时出错: ${err.message}`));
if (handleError('创建 api_keys 表', err)) return;
console.log('Api_Keys 表已检查/创建。');
resolve();
});
});
// 创建 passkeys 表 (如果不存在)
await new Promise<void>((resolve, reject) => {
// 创建 passkeys 表
db.run(createPasskeysTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 passkeys 表时出错: ${err.message}`));
if (handleError('创建 passkeys 表', err)) return;
console.log('Passkeys 表已检查/创建。');
resolve();
});
});
// 创建 notification_settings 表 (如果不存在)
await new Promise<void>((resolve, reject) => {
// 创建 notification_settings 表
db.run(createNotificationSettingsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 notification_settings 表时出错: ${err.message}`));
if (handleError('创建 notification_settings 表', err)) return;
console.log('Notification_Settings 表已检查/创建。');
resolve();
});
});
// --- 新增表创建逻辑 ---
// --- 新增表创建逻辑 ---
// 创建 users 表
await new Promise<void>((resolve, reject) => {
// 创建 users 表
db.run(createUsersTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 users 表时出错: ${err.message}`));
if (handleError('创建 users 表', err)) return;
console.log('Users 表已检查/创建。');
resolve();
});
});
// 创建 proxies 表
await new Promise<void>((resolve, reject) => {
// 创建 proxies 表
db.run(createProxiesTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 proxies 表时出错: ${err.message}`));
if (handleError('创建 proxies 表', err)) return;
console.log('Proxies 表已检查/创建。');
resolve();
});
});
// 创建 connections 表 (依赖 proxies)
await new Promise<void>((resolve, reject) => {
// 创建 connections 表
db.run(createConnectionsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 connections 表时出错: ${err.message}`));
if (handleError('创建 connections 表', err)) return;
console.log('Connections 表已检查/创建。');
resolve();
});
});
// 创建 tags 表
await new Promise<void>((resolve, reject) => {
// 创建 tags 表
db.run(createTagsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 tags 表时出错: ${err.message}`));
if (handleError('创建 tags 表', err)) return;
console.log('Tags 表已检查/创建。');
resolve();
});
});
// 创建 connection_tags 表 (依赖 connections, tags)
await new Promise<void>((resolve, reject) => {
// 创建 connection_tags 表
db.run(createConnectionTagsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 connection_tags 表时出错: ${err.message}`));
if (handleError('创建 connection_tags 表', err)) return;
console.log('Connection_Tags 表已检查/创建。');
resolve();
});
});
// 创建 ip_blacklist 表
await new Promise<void>((resolve, reject) => {
// 创建 ip_blacklist 表
db.run(createIpBlacklistTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 ip_blacklist 表时出错: ${err.message}`));
if (handleError('创建 ip_blacklist 表', err)) return;
console.log('Ip_Blacklist 表已检查/创建。');
resolve();
});
});
// 创建 command_history 表
await new Promise<void>((resolve, reject) => {
// 创建 command_history 表
db.run(createCommandHistoryTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 command_history 表时出错: ${err.message}`));
if (handleError('创建 command_history 表', err)) return;
console.log('Command_History 表已检查/创建。');
resolve();
});
});
// 创建 quick_commands 表
await new Promise<void>((resolve, reject) => {
// 创建 quick_commands 表 - 这是最后一个操作
db.run(createQuickCommandsTableSQL, (err: Error | null) => {
if (err) return reject(new Error(`创建 quick_commands 表时出错: ${err.message}`));
if (handleError('创建 quick_commands 表', err)) {
// 如果最后一个操作失败,serialize 会停止,rejectOuter 已被调用
return;
}
console.log('Quick_Commands 表已检查/创建。');
resolve();
// 所有操作成功完成
console.log('所有数据库迁移已成功完成。');
resolveOuter(); // 解析外部 Promise
});
});
// --- 结束新增表创建逻辑 ---
// --- 结束新增表创建逻辑 ---
console.log('所有数据库迁移已完成。');
} catch (error) {
console.error('数据库迁移过程中出错:', error);
throw error;
}
}); // 结束 db.serialize
}); // 结束 new Promise
};
@@ -92,7 +92,6 @@ watch(searchTerm, (newValue) => {
<!-- 搜索控制按钮 -->
<div class="search-controls">
<button @click="toggleSearch" class="icon-button" :title="isSearching ? t('commandInputBar.closeSearch') : t('commandInputBar.openSearch')">
<!-- 使用 Font Awesome 图标 -->
<i v-if="!isSearching" class="fas fa-search"></i>
<i v-else class="fas fa-times"></i>
</button>
@@ -104,11 +103,11 @@ watch(searchTerm, (newValue) => {
<button @click="findNext" class="icon-button" :title="t('commandInputBar.findNext')">
<i class="fas fa-arrow-down"></i>
</button>
<!-- 搜索结果显示已移除 -->
</template>
</div>
</div>
<!-- Removed hidden span -->
</div>
</template>
@@ -178,14 +178,14 @@ const handleDelete = async (conn: ConnectionInfo) => {
</tr>
</thead>
<tbody>
<!-- 遍历分组内的连接 -->
<tr v-for="conn in groupConnections" :key="conn.id">
<td>{{ conn.name }}</td>
<td>{{ conn.host }}</td>
<td>{{ conn.port }}</td>
<td>{{ conn.username }}</td>
<td>{{ conn.auth_method }}</td>
<td> <!-- 显示标签 -->
<td>
<span v-if="getConnectionTagNames(conn).length > 0" class="tag-list">
<span v-for="tagName in getConnectionTagNames(conn)" :key="tagName" class="tag-item">
{{ tagName }}
@@ -217,7 +217,7 @@ const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string
<!-- 如果是容器节点 -->
<template v-if="layoutNode.type === 'container' && layoutNode.children && layoutNode.children.length > 0">
<splitpanes
:horizontal="layoutNode.direction === 'vertical'"
:horizontal="layoutNode.direction === 'vertical'"
class="default-theme"
style="height: 100%; width: 100%;"
@resized="handlePaneResize"
@@ -48,29 +48,29 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
searchAddon.value = addon; // *** 存储 searchAddon 实例 ***
// *** 监听搜索结果变化 ***
if (searchAddon.value) {
// *** 移除错误的类型注解,让 TS 推断 ***
searchAddon.value.onDidChangeResults((results) => {
// *** 添加更详细的日志 ***
console.log(`[会话 ${sessionId}][SearchAddon] onDidChangeResults 事件触发! results:`, JSON.stringify(results)); // 使用 JSON.stringify 查看完整结构
if (results && typeof results.resultIndex === 'number' && typeof results.resultCount === 'number') {
// 确认 results 包含预期的数字属性
searchResultCount.value = results.resultCount;
currentSearchResultIndex.value = results.resultIndex; // xterm 的索引是从 0 开始的
console.log(`[会话 ${sessionId}][SearchAddon] 状态已更新: index=${currentSearchResultIndex.value}, count=${searchResultCount.value}`);
} else {
// 没有结果、搜索被清除或 results 结构不符合预期
console.log(`[会话 ${sessionId}][SearchAddon] 清除搜索状态或结果无效。 results:`, JSON.stringify(results)); // 使用 JSON.stringify 查看完整结构
searchResultCount.value = 0;
currentSearchResultIndex.value = -1;
// console.log(`[会话 ${sessionId}][SearchAddon] 搜索结果清除或无匹配。`); // 这行日志有点重复,可以注释掉
}
});
// *** 添加确认日志 ***
console.log(`[会话 ${sessionId}][SearchAddon] onDidChangeResults 监听器已附加。`);
} else {
console.warn(`[会话 ${sessionId}][SearchAddon] 无法附加 onDidChangeResults 监听器,searchAddon 实例为空。`);
}
// if (searchAddon.value) {
// // *** 移除错误的类型注解,让 TS 推断 ***
// searchAddon.value.onDidChangeResults((results) => {
// // *** 添加更详细的日志 ***
// console.log(`[会话 ${sessionId}][SearchAddon] onDidChangeResults 事件触发! results:`, JSON.stringify(results)); // 使用 JSON.stringify 查看完整结构
// if (results && typeof results.resultIndex === 'number' && typeof results.resultCount === 'number') {
// // 确认 results 包含预期的数字属性
// searchResultCount.value = results.resultCount;
// currentSearchResultIndex.value = results.resultIndex; // xterm 的索引是从 0 开始的
// console.log(`[会话 ${sessionId}][SearchAddon] 状态已更新: index=${currentSearchResultIndex.value}, count=${searchResultCount.value}`);
// } else {
// // 没有结果、搜索被清除或 results 结构不符合预期
// console.log(`[会话 ${sessionId}][SearchAddon] 清除搜索状态或结果无效。 results:`, JSON.stringify(results)); // 使用 JSON.stringify 查看完整结构
// searchResultCount.value = 0;
// currentSearchResultIndex.value = -1;
// // console.log(`[会话 ${sessionId}][SearchAddon] 搜索结果清除或无匹配。`); // 这行日志有点重复,可以注释掉
// }
// });
// // *** 添加确认日志 ***
// console.log(`[会话 ${sessionId}][SearchAddon] onDidChangeResults 监听器已附加。`);
// } else {
// console.warn(`[会话 ${sessionId}][SearchAddon] 无法附加 onDidChangeResults 监听器,searchAddon 实例为空。`);
// }
// --- 添加日志:检查缓冲区处理 ---
console.log(`[会话 ${sessionId}][SSH前端] handleTerminalReady: 准备处理缓冲区,缓冲区长度: ${terminalOutputBuffer.value.length}`);
+4 -3
View File
@@ -1,4 +1,4 @@
import { createI18n } from 'vue-i18n';
import { createI18n, Composer } from 'vue-i18n';
// 导入语言文件
import enMessages from './locales/en.json';
@@ -41,8 +41,9 @@ const i18n = createI18n<[MessageSchema], 'en' | 'zh'>({
* @param lang 要设置的语言代码 ('en', 'zh', etc.)
*/
export const setLocale = (lang: 'en' | 'zh') => {
if (i18n.global.availableLocales.includes(lang)) {
i18n.global.locale = lang; // 直接赋值
const globalComposer = i18n.global as unknown as Composer; // 强制类型断言
if (globalComposer.availableLocales.includes(lang)) {
globalComposer.locale.value = lang; // 访问 .value 属性
try {
localStorage.setItem(localStorageKey, lang); // 持久化到 localStorage
console.log(`[i18n] Locale set to "${lang}" and saved to localStorage.`); // 添加日志
+10 -4
View File
@@ -92,7 +92,8 @@
"loggingIn": "Logging in...",
"error": "Login failed. Please check your username and password.",
"twoFactorPrompt": "Enter your two-factor authentication code:",
"verifyButton": "Verify"
"verifyButton": "Verify",
"rememberMe": "Remember Me (7 days)"
},
"connections": {
"title": "Connection Management",
@@ -243,7 +244,9 @@
"noPassword": "Connection config is missing password.",
"shellError": "Failed to open shell: {message}",
"alreadyConnected": "An active SSH connection already exists.",
"unknown": "Unknown status"
"unknown": "Unknown status",
"wsClosedWillRetry": "WebSocket connection closed, will attempt reconnect {attempt} in {seconds} seconds...",
"reconnecting": "Attempting to reconnect..."
},
"terminal": {
"infoPrefix": "[INFO]",
@@ -269,6 +272,7 @@
"newFolder": "New Folder",
"rename": "Rename",
"changePermissions": "Change Permissions",
"newFile": "New File",
"delete": "Delete",
"deleteMultiple": "Delete {count} items",
"download": "Download",
@@ -306,7 +310,8 @@
"readFileFailed": "Failed to read file",
"fileDecodeError": "File decoding failed (likely not UTF-8)",
"saveFailed": "Failed to save file",
"saveTimeout": "Save timed out"
"saveTimeout": "Save timed out",
"fileExists": "File \"{name}\" already exists."
},
"prompts": {
"enterFolderName": "Enter the name for the new folder:",
@@ -315,7 +320,8 @@
"confirmDeleteFolder": "Are you sure you want to delete the directory \"{name}\" and all its contents? This cannot be undone.",
"confirmDeleteFile": "Are you sure you want to delete the file \"{name}\"? This cannot be undone.",
"enterNewName": "Enter the new name for \"{oldName}\":",
"enterNewPermissions": "Enter new permissions for \"{name}\" (octal, e.g., 755):"
"enterNewPermissions": "Enter new permissions for \"{name}\" (octal, e.g., 755):",
"enterFileName": "Enter the name for the new file:"
},
"editingFile": "Editing",
"loadingFile": "Loading file...",
+1
View File
@@ -330,6 +330,7 @@
"saveError": "保存出错",
"editPathTooltip": "点击路径进行编辑",
"noActiveSession": "无活动会话",
"loadDirectoryFailed": "加载目录失败",
"noOpenFile": "未打开文件",
"selectFileToEdit": "请从文件管理器中选择文件以开始编辑。",
"searchPlaceholder": "搜索文件..."
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTagsStore, TagInfo } from '../stores/tags.store';
const { t } = useI18n();
const tagsStore = useTagsStore();
const showAddTagForm = ref(false);
const tagToEdit = ref<TagInfo | null>(null);
// 组件挂载时获取标签列表
onMounted(() => {
tagsStore.fetchTags();
});
// 打开添加表单
const openAddForm = () => {
tagToEdit.value = null; // 确保不是编辑模式
showAddTagForm.value = true;
};
// 打开编辑表单
const openEditForm = (tag: TagInfo) => {
tagToEdit.value = tag;
showAddTagForm.value = true;
};
// 关闭表单
const closeForm = () => {
showAddTagForm.value = false;
tagToEdit.value = null;
};
// 处理标签添加/更新成功事件
const onTagSaved = () => {
closeForm();
// Store 内部会自动刷新列表,这里无需额外操作
};
</script>
<template>
<div class="tags-view">
<h2>{{ t('tags.title') }}</h2>
<div class="actions-bar">
<button @click="openAddForm">{{ t('tags.addTag') }}</button>
</div>
<div v-if="tagsStore.isLoading" class="loading-message">
{{ t('tags.loading') }}
</div>
<div v-else-if="tagsStore.error" class="error-message">
{{ t('tags.error', { error: tagsStore.error }) }}
</div>
<div v-else-if="tagsStore.tags.length === 0" class="no-data-message">
{{ t('tags.noTags') }}
</div>
<TagList v-else :tags="tagsStore.tags" @edit-tag="openEditForm" />
<!-- 添加/编辑标签表单 (模态框) -->
<AddTagForm
v-if="showAddTagForm"
:tag-to-edit="tagToEdit"
@close="closeForm"
@tag-saved="onTagSaved"
/>
</div>
</template>
<style scoped>
.tags-view {
padding: 1rem;
}
h2 {
margin-bottom: 1rem;
}
.actions-bar {
margin-bottom: 1rem;
}
.actions-bar button {
padding: 0.5rem 1rem;
cursor: pointer;
}
.loading-message,
.error-message,
.no-data-message {
margin-top: 1rem;
text-align: center;
color: #666;
}
.error-message {
color: red;
}
</style>
@@ -309,7 +309,7 @@ const handleCloseEditorTab = (tabId: string) => {
</script>
<template>
<div class="workspace-view"> <!-- Root element -->
<div class="workspace-view">
<TerminalTabBar
:sessions="sessionTabsWithStatus"
:active-session-id="activeSessionId"
@@ -327,7 +327,6 @@ const handleCloseEditorTab = (tabId: string) => {
class="layout-renderer-wrapper"
:editor-tabs="editorTabs"
:active-editor-tab-id="activeEditorTabId"
<!-- Removed terminalManager prop -->
@send-command="handleSendCommand"
@terminal-input="handleTerminalInput"
@terminal-resize="handleTerminalResize"