Merge pull request #30 from Heavrnl/feature/sftpModal
Feature/sftp modal
This commit is contained in:
@@ -41,6 +41,7 @@ export const settingsController = {
|
|||||||
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
||||||
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
||||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
||||||
|
'showPopupFileManager', // +++ 添加弹窗文件管理器设置键 +++
|
||||||
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
||||||
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
||||||
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useQuickCommandsStore } from '../stores/quickCommands.store';
|
|||||||
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
||||||
import QuickCommandsModal from './QuickCommandsModal.vue'; // +++ Import the modal component +++
|
import QuickCommandsModal from './QuickCommandsModal.vue'; // +++ Import the modal component +++
|
||||||
import SuspendedSshSessionsModal from './SuspendedSshSessionsModal.vue'; // +++ Import the new modal +++
|
import SuspendedSshSessionsModal from './SuspendedSshSessionsModal.vue'; // +++ Import the new modal +++
|
||||||
|
import { useFileEditorStore } from '../stores/fileEditor.store'; // +++ Import File Editor Store +++
|
||||||
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
||||||
|
|
||||||
// Disable attribute inheritance as this component has multiple root nodes (div + modal)
|
// Disable attribute inheritance as this component has multiple root nodes (div + modal)
|
||||||
@@ -23,9 +24,10 @@ const settingsStore = useSettingsStore();
|
|||||||
const quickCommandsStore = useQuickCommandsStore();
|
const quickCommandsStore = useQuickCommandsStore();
|
||||||
const commandHistoryStore = useCommandHistoryStore();
|
const commandHistoryStore = useCommandHistoryStore();
|
||||||
const sessionStore = useSessionStore(); // +++ 初始化 Session Store +++
|
const sessionStore = useSessionStore(); // +++ 初始化 Session Store +++
|
||||||
|
const fileEditorStore = useFileEditorStore(); // +++ Initialize File Editor Store +++
|
||||||
|
|
||||||
// Get reactive setting from store
|
// Get reactive setting from store
|
||||||
const { commandInputSyncTarget } = storeToRefs(settingsStore);
|
const { commandInputSyncTarget, showPopupFileManagerBoolean, showPopupFileEditorBoolean } = storeToRefs(settingsStore); // +++ Import showPopupFileEditorBoolean +++
|
||||||
// Get reactive state and actions from quick commands store
|
// Get reactive state and actions from quick commands store
|
||||||
const { selectedIndex: quickCommandsSelectedIndex, flatVisibleCommands: quickCommandsFiltered } = storeToRefs(quickCommandsStore);
|
const { selectedIndex: quickCommandsSelectedIndex, flatVisibleCommands: quickCommandsFiltered } = storeToRefs(quickCommandsStore);
|
||||||
const { resetSelection: resetQuickCommandsSelection } = quickCommandsStore;
|
const { resetSelection: resetQuickCommandsSelection } = quickCommandsStore;
|
||||||
@@ -298,6 +300,28 @@ const closeSuspendedSshSessionsModal = () => {
|
|||||||
showSuspendedSshSessionsModal.value = false;
|
showSuspendedSshSessionsModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// +++ Function to request opening the file manager modal via event bus +++
|
||||||
|
const openFileManagerModal = () => {
|
||||||
|
if (activeSessionId.value) {
|
||||||
|
console.log(`[CommandInputBar] Emitting fileManager:openModalRequest for session: ${activeSessionId.value}`);
|
||||||
|
emitWorkspaceEvent('fileManager:openModalRequest', { sessionId: activeSessionId.value });
|
||||||
|
} else {
|
||||||
|
console.warn('[CommandInputBar] Cannot open file manager modal: No active session ID.');
|
||||||
|
// Optionally, show a notification to the user
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// +++ Function to request opening the file editor modal +++
|
||||||
|
const openFileEditorModal = () => {
|
||||||
|
if (activeSessionId.value) {
|
||||||
|
console.log(`[CommandInputBar] Triggering popup editor for session: ${activeSessionId.value}`);
|
||||||
|
fileEditorStore.triggerPopup('', activeSessionId.value); // Call store action directly
|
||||||
|
} else {
|
||||||
|
console.warn('[CommandInputBar] Cannot open file editor modal: No active session ID.');
|
||||||
|
// Optionally, show a notification to the user
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// +++ Handler for command execution from the modal +++
|
// +++ Handler for command execution from the modal +++
|
||||||
const handleQuickCommandExecute = (command: string) => {
|
const handleQuickCommandExecute = (command: string) => {
|
||||||
console.log(`[CommandInputBar] Executing quick command: ${command}`);
|
console.log(`[CommandInputBar] Executing quick command: ${command}`);
|
||||||
@@ -418,6 +442,22 @@ const handleQuickCommandExecute = (command: string) => {
|
|||||||
<i class="fas fa-arrow-down text-base"></i>
|
<i class="fas fa-arrow-down text-base"></i>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- File Manager Button -->
|
||||||
|
<button
|
||||||
|
v-if="showPopupFileManagerBoolean"
|
||||||
|
@click="openFileManagerModal"
|
||||||
|
class="flex-shrink-0 flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder text-base"></i>
|
||||||
|
</button>
|
||||||
|
<!-- File Editor Button -->
|
||||||
|
<button
|
||||||
|
v-if="showPopupFileEditorBoolean"
|
||||||
|
@click="openFileEditorModal"
|
||||||
|
class="flex-shrink-0 flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit text-base"></i>
|
||||||
|
</button>
|
||||||
<!-- Note: On mobile, when searching, only the close button (inside toggleSearch button logic) will be effectively visible in this control group -->
|
<!-- Note: On mobile, when searching, only the close button (inside toggleSearch button logic) will be effectively visible in this control group -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,6 +474,7 @@ const handleQuickCommandExecute = (command: string) => {
|
|||||||
:is-visible="showSuspendedSshSessionsModal"
|
:is-visible="showSuspendedSshSessionsModal"
|
||||||
@close="closeSuspendedSshSessionsModal"
|
@close="closeSuspendedSshSessionsModal"
|
||||||
/>
|
/>
|
||||||
|
<!-- File Manager Modal is now handled by a listener for 'fileManager:openModalRequest' event -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -148,14 +148,12 @@ const handleContextMenuPaste = async (event: MouseEvent) => {
|
|||||||
const addContextMenuListener = () => {
|
const addContextMenuListener = () => {
|
||||||
if (terminalRef.value) {
|
if (terminalRef.value) {
|
||||||
terminalRef.value.addEventListener('contextmenu', handleContextMenuPaste);
|
terminalRef.value.addEventListener('contextmenu', handleContextMenuPaste);
|
||||||
console.log(`[Terminal ${props.sessionId}] Right-click paste listener added.`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeContextMenuListener = () => {
|
const removeContextMenuListener = () => {
|
||||||
if (terminalRef.value) {
|
if (terminalRef.value) {
|
||||||
terminalRef.value.removeEventListener('contextmenu', handleContextMenuPaste);
|
terminalRef.value.removeEventListener('contextmenu', handleContextMenuPaste);
|
||||||
console.log(`[Terminal ${props.sessionId}] Right-click paste listener removed.`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// --- 右键粘贴功能结束 ---
|
// --- 右键粘贴功能结束 ---
|
||||||
@@ -245,7 +243,6 @@ onMounted(() => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Re-check if still active and terminal exists
|
// Re-check if still active and terminal exists
|
||||||
if (props.isActive && terminal && terminalRef.value && terminalRef.value.offsetHeight > 0) {
|
if (props.isActive && terminal && terminalRef.value && terminalRef.value.offsetHeight > 0) {
|
||||||
console.log(`[Terminal ${props.sessionId}] Executing delayed fit and resize.`);
|
|
||||||
fitAndEmitResizeNow(terminal);
|
fitAndEmitResizeNow(terminal);
|
||||||
// Also ensure focus when becoming active
|
// Also ensure focus when becoming active
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
|
|||||||
@@ -21,6 +21,26 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-border/50">
|
<hr class="border-border/50">
|
||||||
|
<!-- Popup File Manager -->
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<h3 class="text-base font-semibold text-foreground mb-3">{{ t('settings.popupFileManager.title') }}</h3>
|
||||||
|
<form @submit.prevent="handleUpdateShowPopupFileManager" class="space-y-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="showPopupFileManager" v-model="showPopupFileManagerLocal"
|
||||||
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
||||||
|
<label for="showPopupFileManager" class="text-sm text-foreground cursor-pointer select-none">{{ t('settings.popupFileManager.enableLabel') }}</label>
|
||||||
|
</div>
|
||||||
|
<small class="block mt-1 text-xs text-text-secondary">{{ t('settings.popupFileManager.description') }}</small>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button type="submit"
|
||||||
|
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition duration-150 ease-in-out text-sm font-medium">
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<p v-if="showPopupFileManagerMessage" :class="['text-sm', showPopupFileManagerSuccess ? 'text-success' : 'text-error']">{{ showPopupFileManagerMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<hr class="border-border/50">
|
||||||
<!-- Share Tabs -->
|
<!-- Share Tabs -->
|
||||||
<div class="settings-section-content">
|
<div class="settings-section-content">
|
||||||
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.shareEditorTabs.title') }}</h3>
|
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.shareEditorTabs.title') }}</h3>
|
||||||
@@ -268,6 +288,12 @@ const {
|
|||||||
terminalEnableRightClickPasteMessage, // NEW
|
terminalEnableRightClickPasteMessage, // NEW
|
||||||
terminalEnableRightClickPasteSuccess, // NEW
|
terminalEnableRightClickPasteSuccess, // NEW
|
||||||
handleUpdateTerminalRightClickPasteSetting, // NEW
|
handleUpdateTerminalRightClickPasteSetting, // NEW
|
||||||
|
// Popup File Manager
|
||||||
|
showPopupFileManagerLocal,
|
||||||
|
// showPopupFileManagerLoading, // Not used
|
||||||
|
showPopupFileManagerMessage,
|
||||||
|
showPopupFileManagerSuccess,
|
||||||
|
handleUpdateShowPopupFileManager,
|
||||||
} = useWorkspaceSettings();
|
} = useWorkspaceSettings();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ export function useSystemSettings() {
|
|||||||
selectedLanguage.value = newVal;
|
selectedLanguage.value = newVal;
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Language
|
// Language
|
||||||
selectedLanguage,
|
selectedLanguage,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function useWorkspaceSettings() {
|
|||||||
terminalScrollbackLimitNumber,
|
terminalScrollbackLimitNumber,
|
||||||
fileManagerShowDeleteConfirmationBoolean,
|
fileManagerShowDeleteConfirmationBoolean,
|
||||||
terminalEnableRightClickPasteBoolean, // NEW
|
terminalEnableRightClickPasteBoolean, // NEW
|
||||||
|
showPopupFileManagerBoolean, // +++ Import the new getter +++
|
||||||
} = storeToRefs(settingsStore);
|
} = storeToRefs(settingsStore);
|
||||||
|
|
||||||
// --- Popup Editor ---
|
// --- Popup Editor ---
|
||||||
@@ -261,6 +262,30 @@ export function useWorkspaceSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Popup File Manager ---
|
||||||
|
const showPopupFileManagerLocal = ref(true);
|
||||||
|
const showPopupFileManagerLoading = ref(false);
|
||||||
|
const showPopupFileManagerMessage = ref('');
|
||||||
|
const showPopupFileManagerSuccess = ref(false);
|
||||||
|
|
||||||
|
const handleUpdateShowPopupFileManager = async () => {
|
||||||
|
showPopupFileManagerLoading.value = true;
|
||||||
|
showPopupFileManagerMessage.value = '';
|
||||||
|
showPopupFileManagerSuccess.value = false;
|
||||||
|
try {
|
||||||
|
const valueToSave = showPopupFileManagerLocal.value ? 'true' : 'false';
|
||||||
|
await settingsStore.updateSetting('showPopupFileManager', valueToSave);
|
||||||
|
showPopupFileManagerMessage.value = t('settings.popupFileManager.success.saved');
|
||||||
|
showPopupFileManagerSuccess.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新弹窗文件管理器设置失败:', error);
|
||||||
|
showPopupFileManagerMessage.value = error.message || t('settings.popupFileManager.error.saveFailed');
|
||||||
|
showPopupFileManagerSuccess.value = false;
|
||||||
|
} finally {
|
||||||
|
showPopupFileManagerLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Watchers to sync local state with store state
|
// Watchers to sync local state with store state
|
||||||
watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true });
|
watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true });
|
||||||
watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true });
|
watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true });
|
||||||
@@ -272,6 +297,7 @@ export function useWorkspaceSettings() {
|
|||||||
watch(terminalScrollbackLimitNumber, (newValue) => { terminalScrollbackLimitLocal.value = newValue; }, { immediate: true });
|
watch(terminalScrollbackLimitNumber, (newValue) => { terminalScrollbackLimitLocal.value = newValue; }, { immediate: true });
|
||||||
watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true });
|
watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true });
|
||||||
watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true }); // NEW
|
watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true }); // NEW
|
||||||
|
watch(showPopupFileManagerBoolean, (newValue) => { showPopupFileManagerLocal.value = newValue; }, { immediate: true }); // +++ Watch for popup file manager +++
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -334,5 +360,12 @@ export function useWorkspaceSettings() {
|
|||||||
terminalEnableRightClickPasteMessage, // NEW
|
terminalEnableRightClickPasteMessage, // NEW
|
||||||
terminalEnableRightClickPasteSuccess, // NEW
|
terminalEnableRightClickPasteSuccess, // NEW
|
||||||
handleUpdateTerminalRightClickPasteSetting, // NEW
|
handleUpdateTerminalRightClickPasteSetting, // NEW
|
||||||
|
|
||||||
|
// Popup File Manager
|
||||||
|
showPopupFileManagerLocal,
|
||||||
|
showPopupFileManagerLoading,
|
||||||
|
showPopupFileManagerMessage,
|
||||||
|
showPopupFileManagerSuccess,
|
||||||
|
handleUpdateShowPopupFileManager,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,6 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
|
|||||||
console.log(`[SSH ${sessionId}] handleTerminalResize called with:`, dimensions);
|
console.log(`[SSH ${sessionId}] handleTerminalResize called with:`, dimensions);
|
||||||
// 只有在连接状态下才发送 resize 命令给后端
|
// 只有在连接状态下才发送 resize 命令给后端
|
||||||
if (isConnected.value) {
|
if (isConnected.value) {
|
||||||
console.log(`[SSH ${sessionId}] Sending ssh:resize to backend:`, dimensions);
|
|
||||||
sendMessage({ type: 'ssh:resize', sessionId, payload: dimensions });
|
sendMessage({ type: 'ssh:resize', sessionId, payload: dimensions });
|
||||||
} else {
|
} else {
|
||||||
console.log(`[SSH ${sessionId}] WebSocket not connected, skipping ssh:resize.`);
|
console.log(`[SSH ${sessionId}] WebSocket not connected, skipping ssh:resize.`);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export type WorkspaceEventPayloads = {
|
|||||||
// UI Interaction Events
|
// UI Interaction Events
|
||||||
'ui:openLayoutConfigurator': void;
|
'ui:openLayoutConfigurator': void;
|
||||||
// 'ui:toggleVirtualKeyboard': void; // 如果决定迁移 CommandInputBar 的这个事件
|
// 'ui:toggleVirtualKeyboard': void; // 如果决定迁移 CommandInputBar 的这个事件
|
||||||
|
'fileManager:openModalRequest': { sessionId: string }; // 请求打开文件管理器模态框
|
||||||
|
|
||||||
// Suspended SSH Session Events
|
// Suspended SSH Session Events
|
||||||
'suspendedSession:actionCompleted': void; // Emitted when a resume/remove action is completed
|
'suspendedSession:actionCompleted': void; // Emitted when a resume/remove action is completed
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
"appName": "Nexus Terminal",
|
"appName": "Nexus Terminal",
|
||||||
"projectName": "Nexus Terminal",
|
"projectName": "Nexus Terminal",
|
||||||
"slogan":"Stir the stars, command the terminal.",
|
"slogan":"Stir the stars, command the terminal.",
|
||||||
@@ -301,7 +302,11 @@
|
|||||||
"reconnectingMsg": "Attempting to reconnect..."
|
"reconnectingMsg": "Attempting to reconnect..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fileEditor": {
|
||||||
|
"title": "File Editor"
|
||||||
|
},
|
||||||
"fileManager": {
|
"fileManager": {
|
||||||
|
"modalTitle": "File Manager",
|
||||||
"currentPath": "Current Path",
|
"currentPath": "Current Path",
|
||||||
"loading": "Loading directory...",
|
"loading": "Loading directory...",
|
||||||
"emptyDirectory": "Directory is empty",
|
"emptyDirectory": "Directory is empty",
|
||||||
@@ -503,6 +508,17 @@
|
|||||||
"deleteFailed": "Failed to delete tag \"{name}\": {error}"
|
"deleteFailed": "Failed to delete tag \"{name}\": {error}"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"popupFileManager": {
|
||||||
|
"title": "Popup File Manager",
|
||||||
|
"enableLabel": "Enable Popup File Manager",
|
||||||
|
"description": "When enabled, the file manager button will be displayed in the command input bar, allowing you to open the popup file manager.",
|
||||||
|
"success": {
|
||||||
|
"saved": "Popup File Manager settings saved successfully."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "Failed to save Popup File Manager settings."
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"category": {
|
"category": {
|
||||||
"security": "Security Settings",
|
"security": "Security Settings",
|
||||||
@@ -890,7 +906,17 @@
|
|||||||
"noReleases": "No releases found",
|
"noReleases": "No releases found",
|
||||||
"rateLimit": "GitHub API rate limit exceeded, please try again later"
|
"rateLimit": "GitHub API rate limit exceeded, please try again later"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"tabs": {
|
||||||
|
"security": "Security",
|
||||||
|
"ipControl": "IP Control",
|
||||||
|
"workspace": "Workspace",
|
||||||
|
"system": "System",
|
||||||
|
"dataManagement": "Data Management",
|
||||||
|
"appearance": "Appearance",
|
||||||
|
"about": "About"
|
||||||
|
},
|
||||||
|
"loading": "Loading..."
|
||||||
},
|
},
|
||||||
"notificationController": {
|
"notificationController": {
|
||||||
"errorFetchSettings": "Failed to fetch notification settings",
|
"errorFetchSettings": "Failed to fetch notification settings",
|
||||||
@@ -1101,6 +1127,9 @@
|
|||||||
"dockerManager": "Docker Manager",
|
"dockerManager": "Docker Manager",
|
||||||
"suspendedSshSessions": "Suspended Sessions Management"
|
"suspendedSshSessions": "Suspended Sessions Management"
|
||||||
},
|
},
|
||||||
|
"panes": {
|
||||||
|
"suspendedSshSessions": "Suspended Session Manager"
|
||||||
|
},
|
||||||
"noActiveSession": {
|
"noActiveSession": {
|
||||||
"title": "No Active Session",
|
"title": "No Active Session",
|
||||||
"message": "Please connect to a session first",
|
"message": "Please connect to a session first",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
"appName": "星枢ターミナル",
|
"appName": "星枢ターミナル",
|
||||||
"auditLog": {
|
"auditLog": {
|
||||||
"actions": {
|
"actions": {
|
||||||
@@ -311,7 +312,11 @@
|
|||||||
},
|
},
|
||||||
"waitingForSsh": "SSH接続を待機中..."
|
"waitingForSsh": "SSH接続を待機中..."
|
||||||
},
|
},
|
||||||
|
"fileEditor": {
|
||||||
|
"title": "ファイルエディタ"
|
||||||
|
},
|
||||||
"fileManager": {
|
"fileManager": {
|
||||||
|
"modalTitle": "ファイルマネージャー",
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"cdToTerminal": "ターミナルのディレクトリを現在のパスに変更",
|
"cdToTerminal": "ターミナルのディレクトリを現在のパスに変更",
|
||||||
@@ -508,7 +513,11 @@
|
|||||||
"fileManager": "ファイルマネージャー",
|
"fileManager": "ファイルマネージャー",
|
||||||
"quickCommands": "クイックコマンド",
|
"quickCommands": "クイックコマンド",
|
||||||
"statusMonitor": "ステータスモニター",
|
"statusMonitor": "ステータスモニター",
|
||||||
"terminal": "ターミナル"
|
"terminal": "ターミナル",
|
||||||
|
"suspendedSshSessions": "中断されたセッション管理"
|
||||||
|
},
|
||||||
|
"panes": {
|
||||||
|
"suspendedSshSessions": "中断されたセッション管理者"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layoutConfigurator": {
|
"layoutConfigurator": {
|
||||||
@@ -690,6 +699,17 @@
|
|||||||
"title": "VNCセッション"
|
"title": "VNCセッション"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"popupFileManager": {
|
||||||
|
"title": "ポップアップファイルマネージャー",
|
||||||
|
"enableLabel": "ポップアップファイルマネージャーを有効にする",
|
||||||
|
"description": "有効にすると、コマンド入力バーにファイルマネージャーボタンが表示され、ポップアップファイルマネージャーを開くことができます。",
|
||||||
|
"success": {
|
||||||
|
"saved": "ポップアップファイルマネージャーの設定が保存されました。"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "ポップアップファイルマネージャーの設定の保存に失敗しました。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"customizeButton": "外観をカスタマイズ",
|
"customizeButton": "外観をカスタマイズ",
|
||||||
"description": "アプリケーションのビジュアルテーマと背景をカスタマイズします。",
|
"description": "アプリケーションのビジュアルテーマと背景をカスタマイズします。",
|
||||||
@@ -1080,6 +1100,16 @@
|
|||||||
"decryptKeyInfo": "解凍パスワードは、data/.env ファイル内の ENCRYPTION_KEY です。このファイルを安全に保管してください。",
|
"decryptKeyInfo": "解凍パスワードは、data/.env ファイル内の ENCRYPTION_KEY です。このファイルを安全に保管してください。",
|
||||||
"buttonText": "エクスポートを開始"
|
"buttonText": "エクスポートを開始"
|
||||||
},
|
},
|
||||||
|
"tabs": {
|
||||||
|
"security": "セキュリティ",
|
||||||
|
"ipControl": "IP制御",
|
||||||
|
"workspace": "ワークスペース",
|
||||||
|
"system": "システム",
|
||||||
|
"dataManagement": "データ管理",
|
||||||
|
"appearance": "外観",
|
||||||
|
"about": "バージョン情報"
|
||||||
|
},
|
||||||
|
"loading": "読み込み中...",
|
||||||
"setup": {
|
"setup": {
|
||||||
"confirmPassword": "パスワードを再入力",
|
"confirmPassword": "パスワードを再入力",
|
||||||
"confirmPasswordPlaceholder": "パスワードを再入力して確認",
|
"confirmPasswordPlaceholder": "パスワードを再入力して確認",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
"appName": "星枢终端",
|
"appName": "星枢终端",
|
||||||
"projectName": "星枢终端",
|
"projectName": "星枢终端",
|
||||||
"slogan": "星垂平野阔,枢动万端通",
|
"slogan": "星垂平野阔,枢动万端通",
|
||||||
@@ -300,7 +301,11 @@
|
|||||||
"reconnectingMsg": "正在尝试重新连接..."
|
"reconnectingMsg": "正在尝试重新连接..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fileEditor": {
|
||||||
|
"title": "文件编辑器"
|
||||||
|
},
|
||||||
"fileManager": {
|
"fileManager": {
|
||||||
|
"modalTitle": "文件管理器",
|
||||||
"currentPath": "当前路径",
|
"currentPath": "当前路径",
|
||||||
"loading": "正在加载目录...",
|
"loading": "正在加载目录...",
|
||||||
"emptyDirectory": "目录为空",
|
"emptyDirectory": "目录为空",
|
||||||
@@ -502,6 +507,17 @@
|
|||||||
"deleteFailed": "标签 \"{name}\" 删除失败: {error}"
|
"deleteFailed": "标签 \"{name}\" 删除失败: {error}"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"popupFileManager": {
|
||||||
|
"title": "弹窗文件管理器",
|
||||||
|
"enableLabel": "启用弹窗文件管理器",
|
||||||
|
"description": "启用后,命令输入栏将显示文件管理器按钮,点击可打开弹窗式文件管理器。",
|
||||||
|
"success": {
|
||||||
|
"saved": "弹窗文件管理器设置已保存。"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"saveFailed": "保存弹窗文件管理器设置失败。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
"category": {
|
"category": {
|
||||||
"security": "安全设置",
|
"security": "安全设置",
|
||||||
@@ -890,8 +906,18 @@
|
|||||||
"title": "导出连接数据",
|
"title": "导出连接数据",
|
||||||
"decryptKeyInfo": "解压密码是您 data/.env 文件中的 ENCRYPTION_KEY。请妥善保管此文件。",
|
"decryptKeyInfo": "解压密码是您 data/.env 文件中的 ENCRYPTION_KEY。请妥善保管此文件。",
|
||||||
"buttonText": "开始导出"
|
"buttonText": "开始导出"
|
||||||
}
|
},
|
||||||
},
|
"tabs": {
|
||||||
|
"security": "安全",
|
||||||
|
"ipControl": "IP 管控",
|
||||||
|
"workspace": "工作区",
|
||||||
|
"system": "系统",
|
||||||
|
"dataManagement": "数据管理",
|
||||||
|
"appearance": "外观",
|
||||||
|
"about": "关于"
|
||||||
|
},
|
||||||
|
"loading": "加载中..."
|
||||||
|
},
|
||||||
|
|
||||||
"notificationController": {
|
"notificationController": {
|
||||||
"errorFetchSettings": "获取通知设置失败",
|
"errorFetchSettings": "获取通知设置失败",
|
||||||
@@ -1104,6 +1130,9 @@
|
|||||||
"dockerManager": "Docker 管理器",
|
"dockerManager": "Docker 管理器",
|
||||||
"suspendedSshSessions": "挂起会话管理"
|
"suspendedSshSessions": "挂起会话管理"
|
||||||
},
|
},
|
||||||
|
"panes": {
|
||||||
|
"suspendedSshSessions": "挂起会话管理器"
|
||||||
|
},
|
||||||
"noActiveSession": {
|
"noActiveSession": {
|
||||||
"title": "无活动会话",
|
"title": "无活动会话",
|
||||||
"message": "请先连接一个会话",
|
"message": "请先连接一个会话",
|
||||||
|
|||||||
@@ -101,13 +101,6 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
]);
|
]);
|
||||||
appearanceSettings.value = settingsResponse.data;
|
appearanceSettings.value = settingsResponse.data;
|
||||||
allTerminalThemes.value = themesResponse.data; // 更新 allTerminalThemes
|
allTerminalThemes.value = themesResponse.data; // 更新 allTerminalThemes
|
||||||
console.log('[AppearanceStore LOG] 外观设置已加载 (原始数据):', JSON.stringify(settingsResponse.data)); // 添加原始数据日志
|
|
||||||
console.log(`[AppearanceStore LOG] 从后端加载的 terminalBackgroundEnabled 原始值: ${settingsResponse.data.terminalBackgroundEnabled}`); // 专门记录该值
|
|
||||||
console.log('[AppearanceStore] 所有终端主题列表已加载:', allTerminalThemes.value);
|
|
||||||
|
|
||||||
// --- 后端返回的 activeTerminalThemeId 已经是 number | null ---
|
|
||||||
// 前端不再需要设置默认主题 ID 的逻辑,后端初始化时会保证它不为 NULL
|
|
||||||
// 如果后端返回 null (理论上不应发生,除非初始化失败),则 currentTerminalTheme 计算属性会回退到 defaultXtermTheme
|
|
||||||
|
|
||||||
// 应用加载的 UI 主题
|
// 应用加载的 UI 主题
|
||||||
applyUiTheme(currentUiTheme.value);
|
applyUiTheme(currentUiTheme.value);
|
||||||
@@ -486,7 +479,6 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
for (const [key, value] of Object.entries(theme)) {
|
for (const [key, value] of Object.entries(theme)) {
|
||||||
root.style.setProperty(key, value);
|
root.style.setProperty(key, value);
|
||||||
}
|
}
|
||||||
console.log('[AppearanceStore] UI 主题已应用:', theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => {
|
|||||||
try {
|
try {
|
||||||
const cachedData = localStorage.getItem(cacheKey);
|
const cachedData = localStorage.getItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
console.log('[CmdHistoryStore] Loading history from cache.');
|
|
||||||
historyList.value = JSON.parse(cachedData); // 缓存中已是降序
|
historyList.value = JSON.parse(cachedData); // 缓存中已是降序
|
||||||
isLoading.value = false; // 先显示缓存
|
isLoading.value = false; // 先显示缓存
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
|||||||
try {
|
try {
|
||||||
const cachedData = localStorage.getItem(cacheKey);
|
const cachedData = localStorage.getItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
console.log('[QuickCmdTagStore] Loading quick command tags from cache.');
|
|
||||||
tags.value = JSON.parse(cachedData);
|
tags.value = JSON.parse(cachedData);
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -41,7 +40,6 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
|||||||
// 2. 后台获取最新数据
|
// 2. 后台获取最新数据
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
console.log('[QuickCmdTagStore] Fetching latest quick command tags from server...');
|
|
||||||
// 使用新的 API 端点
|
// 使用新的 API 端点
|
||||||
const response = await apiClient.get<QuickCommandTag[]>('/quick-command-tags');
|
const response = await apiClient.get<QuickCommandTag[]>('/quick-command-tags');
|
||||||
const freshData = response.data;
|
const freshData = response.data;
|
||||||
@@ -50,11 +48,9 @@ export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
|||||||
// 3. 对比并更新
|
// 3. 对比并更新
|
||||||
const currentDataString = JSON.stringify(tags.value);
|
const currentDataString = JSON.stringify(tags.value);
|
||||||
if (currentDataString !== freshDataString) {
|
if (currentDataString !== freshDataString) {
|
||||||
console.log('[QuickCmdTagStore] Tags data changed, updating state and cache.');
|
|
||||||
tags.value = freshData;
|
tags.value = freshData;
|
||||||
localStorage.setItem(cacheKey, freshDataString);
|
localStorage.setItem(cacheKey, freshDataString);
|
||||||
} else {
|
} else {
|
||||||
console.log('[QuickCmdTagStore] Tags data is up-to-date.');
|
|
||||||
}
|
}
|
||||||
error.value = null;
|
error.value = null;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
try {
|
try {
|
||||||
const cachedData = localStorage.getItem(cacheKey);
|
const cachedData = localStorage.getItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
console.log(`[QuickCmdStore] Loading commands from cache.`);
|
|
||||||
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds)
|
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds)
|
||||||
const parsedData = JSON.parse(cachedData) as QuickCommandFE[];
|
const parsedData = JSON.parse(cachedData) as QuickCommandFE[];
|
||||||
// 基本验证,确保 tagIds 是数组
|
// 基本验证,确保 tagIds 是数组
|
||||||
@@ -291,7 +290,6 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
|||||||
quickCommandsList.value = freshData;
|
quickCommandsList.value = freshData;
|
||||||
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
||||||
} else {
|
} else {
|
||||||
console.log('[QuickCmdStore] Commands data is up-to-date.');
|
|
||||||
}
|
}
|
||||||
error.value = null;
|
error.value = null;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export const requestStartSshSuspend = (sessionId: string): void => {
|
|||||||
// join('\n') 会在行间添加换行符,如果最后一行是空字符串,末尾不会有多余的 \n
|
// join('\n') 会在行间添加换行符,如果最后一行是空字符串,末尾不会有多余的 \n
|
||||||
// 如果最后一行非空,则自然以该行结束。
|
// 如果最后一行非空,则自然以该行结束。
|
||||||
|
|
||||||
console.log(`[${t('term.sshSuspend')}] 已获取会话 ${sessionId} 的初始屏幕缓冲区内容 (处理后),长度: ${initialBuffer.length}, 最后非空行索引: ${lastNonEmptyLineIndex}`);
|
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[${t('term.sshSuspend')}] 未能获取会话 ${sessionId} 的终端实例以提取初始缓冲区。`);
|
console.warn(`[${t('term.sshSuspend')}] 未能获取会话 ${sessionId} 的终端实例以提取初始缓冲区。`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface SettingsState {
|
|||||||
maxLoginAttempts?: string;
|
maxLoginAttempts?: string;
|
||||||
loginBanDuration?: string;
|
loginBanDuration?: string;
|
||||||
showPopupFileEditor?: string; // 'true' or 'false'
|
showPopupFileEditor?: string; // 'true' or 'false'
|
||||||
|
showPopupFileManager?: string; // 'true' or 'false' - NEW: 弹窗文件管理器
|
||||||
shareFileEditorTabs?: string; // 'true' or 'false'
|
shareFileEditorTabs?: string; // 'true' or 'false'
|
||||||
ipWhitelistEnabled?: string; // 添加 IP 白名单启用状态 'true' or 'false'
|
ipWhitelistEnabled?: string; // 添加 IP 白名单启用状态 'true' or 'false'
|
||||||
autoCopyOnSelect?: string; // 'true' or 'false' - 终端选中自动复制
|
autoCopyOnSelect?: string; // 'true' or 'false' - 终端选中自动复制
|
||||||
@@ -114,6 +115,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
if (settings.value.showPopupFileEditor === undefined) {
|
if (settings.value.showPopupFileEditor === undefined) {
|
||||||
settings.value.showPopupFileEditor = 'true';
|
settings.value.showPopupFileEditor = 'true';
|
||||||
}
|
}
|
||||||
|
// +++ 添加 showPopupFileManager 默认值 (改为 false) +++
|
||||||
|
if (settings.value.showPopupFileManager === undefined) {
|
||||||
|
settings.value.showPopupFileManager = 'false'; // 默认禁用弹窗文件管理器
|
||||||
|
}
|
||||||
if (settings.value.shareFileEditorTabs === undefined) {
|
if (settings.value.shareFileEditorTabs === undefined) {
|
||||||
settings.value.shareFileEditorTabs = 'true';
|
settings.value.shareFileEditorTabs = 'true';
|
||||||
}
|
}
|
||||||
@@ -203,7 +208,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
try {
|
try {
|
||||||
if (settings.value.fileManagerColWidths) {
|
if (settings.value.fileManagerColWidths) {
|
||||||
loadedFmWidths = JSON.parse(settings.value.fileManagerColWidths);
|
loadedFmWidths = JSON.parse(settings.value.fileManagerColWidths);
|
||||||
console.log(`[SettingsStore] Successfully parsed fileManagerColWidths JSON: ${JSON.stringify(loadedFmWidths)}`);
|
|
||||||
if (typeof loadedFmWidths !== 'object' || loadedFmWidths === null) {
|
if (typeof loadedFmWidths !== 'object' || loadedFmWidths === null) {
|
||||||
console.warn('[SettingsStore] Invalid fileManagerColWidths format loaded, resetting.');
|
console.warn('[SettingsStore] Invalid fileManagerColWidths format loaded, resetting.');
|
||||||
loadedFmWidths = {};
|
loadedFmWidths = {};
|
||||||
@@ -371,7 +375,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// 移除外观相关的键检查
|
// 移除外观相关的键检查
|
||||||
const allowedKeys: Array<keyof SettingsState> = [
|
const allowedKeys: Array<keyof SettingsState> = [
|
||||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||||
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
|
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++
|
||||||
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
||||||
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
||||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
||||||
@@ -466,7 +470,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// 移除外观相关的键检查
|
// 移除外观相关的键检查
|
||||||
const allowedKeys: Array<keyof SettingsState> = [
|
const allowedKeys: Array<keyof SettingsState> = [
|
||||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||||
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
|
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++
|
||||||
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
||||||
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
||||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
||||||
@@ -664,6 +668,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
return settings.value.showPopupFileEditor !== 'false';
|
return settings.value.showPopupFileEditor !== 'false';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// +++ Getter for popup file manager setting, returning boolean +++
|
||||||
|
const showPopupFileManagerBoolean = computed(() => {
|
||||||
|
return settings.value.showPopupFileManager !== 'false'; // Default to true
|
||||||
|
});
|
||||||
|
|
||||||
// Getter for sharing setting, returning boolean
|
// Getter for sharing setting, returning boolean
|
||||||
const shareFileEditorTabsBoolean = computed(() => {
|
const shareFileEditorTabsBoolean = computed(() => {
|
||||||
return settings.value.shareFileEditorTabs !== 'false';
|
return settings.value.shareFileEditorTabs !== 'false';
|
||||||
@@ -791,6 +800,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
error,
|
error,
|
||||||
language,
|
language,
|
||||||
showPopupFileEditorBoolean,
|
showPopupFileEditorBoolean,
|
||||||
|
showPopupFileManagerBoolean, // +++ 暴露弹窗文件管理器 getter +++
|
||||||
shareFileEditorTabsBoolean,
|
shareFileEditorTabsBoolean,
|
||||||
ipWhitelistEnabled, // 暴露 IP 白名单启用状态
|
ipWhitelistEnabled, // 暴露 IP 白名单启用状态
|
||||||
ipBlacklistEnabledBoolean, // <-- NEW: 暴露 IP 黑名单启用状态 getter
|
ipBlacklistEnabledBoolean, // <-- NEW: 暴露 IP 黑名单启用状态 getter
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
try {
|
try {
|
||||||
const cachedData = localStorage.getItem(cacheKey);
|
const cachedData = localStorage.getItem(cacheKey);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
console.log('[TagsStore] Loading tags from cache.');
|
|
||||||
tags.value = JSON.parse(cachedData);
|
tags.value = JSON.parse(cachedData);
|
||||||
isLoading.value = false; // 先显示缓存
|
isLoading.value = false; // 先显示缓存
|
||||||
} else {
|
} else {
|
||||||
@@ -39,7 +38,6 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
// 2. 后台获取最新数据
|
// 2. 后台获取最新数据
|
||||||
isLoading.value = true; // 标记正在后台获取
|
isLoading.value = true; // 标记正在后台获取
|
||||||
try {
|
try {
|
||||||
console.log('[TagsStore] Fetching latest tags from server...');
|
|
||||||
const response = await apiClient.get<TagInfo[]>('/tags');
|
const response = await apiClient.get<TagInfo[]>('/tags');
|
||||||
const freshData = response.data;
|
const freshData = response.data;
|
||||||
const freshDataString = JSON.stringify(freshData);
|
const freshDataString = JSON.stringify(freshData);
|
||||||
@@ -47,7 +45,6 @@ export const useTagsStore = defineStore('tags', () => {
|
|||||||
// 3. 对比并更新
|
// 3. 对比并更新
|
||||||
const currentDataString = JSON.stringify(tags.value);
|
const currentDataString = JSON.stringify(tags.value);
|
||||||
if (currentDataString !== freshDataString) {
|
if (currentDataString !== freshDataString) {
|
||||||
console.log('[TagsStore] Tags data changed, updating state and cache.');
|
|
||||||
tags.value = freshData;
|
tags.value = freshData;
|
||||||
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,68 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-4 bg-background text-foreground min-h-screen"> <!-- Outer container -->
|
<div class="p-4 bg-background text-foreground min-h-screen"> <!-- Outer container -->
|
||||||
<div class="max-w-7xl mx-auto"> <!-- Inner container for max-width -->
|
<div class="max-w-7xl mx-auto"> <!-- Inner container for max-width -->
|
||||||
<h1 class="text-2xl font-semibold text-foreground mb-6 pb-3 border-b border-border"> <!-- Main Title -->
|
<!-- Tabs Navigation -->
|
||||||
{{ $t('settings.title') }}
|
<div class="mb-6 flex space-x-1 sticky top-0 bg-background z-10 py-2">
|
||||||
</h1>
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
:class="['px-4 py-2 text-sm font-medium rounded-md focus:outline-none transition-colors duration-150 ease-in-out',
|
||||||
|
activeTab === tab.key ? 'bg-primary text-white' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground']"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Error state (Show first if error exists) -->
|
<!-- Error state (Show first if error exists) -->
|
||||||
<div v-if="settingsError" class="p-4 mb-4 border-l-4 border-error bg-error/10 text-error rounded">
|
<div v-if="settingsError" class="p-4 mb-4 border-l-4 border-error bg-error/10 text-error rounded">
|
||||||
{{ settingsError }}
|
{{ settingsError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Sections Grid (Render grid structure always if no error) -->
|
<!-- Settings Content based on activeTab -->
|
||||||
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <!-- Changed to 2 columns on large screens -->
|
<div v-else class="space-y-6">
|
||||||
<!-- Removed global loading state, content will show/hide based on individual loading states -->
|
<!-- Security Tab Content -->
|
||||||
|
<div v-if="activeTab === 'security'">
|
||||||
<!-- Column 1: Security -->
|
|
||||||
<div class="space-y-6"> <!-- Removed col-span -->
|
|
||||||
<!-- Security Sections: Only show if settings data is loaded -->
|
|
||||||
<div v-if="settings" class="bg-background border border-border rounded-lg shadow-sm overflow-hidden">
|
<div v-if="settings" class="bg-background border border-border rounded-lg shadow-sm overflow-hidden">
|
||||||
<h2 class="text-lg font-semibold text-foreground px-6 py-4 border-b border-border bg-header/50">{{ $t('settings.category.security') }}</h2>
|
<h2 class="text-lg font-semibold text-foreground px-6 py-4 border-b border-border bg-header/50">{{ $t('settings.category.security') }}</h2>
|
||||||
<div class="p-6 space-y-6">
|
<div class="p-6 space-y-6">
|
||||||
<!-- Change Password -->
|
|
||||||
<ChangePasswordForm />
|
<ChangePasswordForm />
|
||||||
<hr class="border-border/50">
|
<hr class="border-border/50">
|
||||||
<!-- Passkey Management -->
|
|
||||||
<PasskeyManagement />
|
<PasskeyManagement />
|
||||||
<hr class="border-border/50">
|
<hr class="border-border/50">
|
||||||
<!-- 2FA -->
|
|
||||||
<TwoFactorAuthSettings />
|
<TwoFactorAuthSettings />
|
||||||
<hr class="border-border/50"> <!-- Separator -->
|
<hr class="border-border/50">
|
||||||
<!-- CAPTCHA Settings -->
|
|
||||||
<CaptchaSettingsForm />
|
<CaptchaSettingsForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="p-4 text-center text-muted-foreground">{{ $t('settings.loading', '加载中...') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- IP Whitelist Section: Only show if settings data is loaded -->
|
<!-- IP Control Tab Content -->
|
||||||
<div v-if="settings" class="bg-background border border-border rounded-lg shadow-sm overflow-hidden">
|
<div v-if="activeTab === 'ipControl'">
|
||||||
|
<div v-if="settings" class="bg-background border border-border rounded-lg shadow-sm overflow-hidden mb-6">
|
||||||
<h2 class="text-lg font-semibold text-foreground px-6 py-4 border-b border-border bg-header/50">{{ $t('settings.ipWhitelist.title') }}</h2>
|
<h2 class="text-lg font-semibold text-foreground px-6 py-4 border-b border-border bg-header/50">{{ $t('settings.ipWhitelist.title') }}</h2>
|
||||||
<div class="p-6 space-y-6">
|
<div class="p-6 space-y-6">
|
||||||
<IpWhitelistSettings />
|
<IpWhitelistSettings />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- IP Blacklist Section: Only show if settings data is loaded (as config depends on it) -->
|
|
||||||
<IpBlacklistSettings v-if="settings" />
|
<IpBlacklistSettings v-if="settings" />
|
||||||
|
<div v-else-if="!settings && activeTab === 'ipControl'" class="p-4 text-center text-muted-foreground">{{ $t('settings.loading', '加载中...') }}</div>
|
||||||
<!-- About Section -->
|
|
||||||
<AboutSection />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Column 2: Appearance, Workspace, System -->
|
<!-- Workspace Tab Content -->
|
||||||
<div class="space-y-6"> <!-- Removed col-span -->
|
<div v-if="activeTab === 'workspace'">
|
||||||
|
<WorkspaceSettingsSection v-if="settings" />
|
||||||
|
<div v-else class="p-4 text-center text-muted-foreground">{{ $t('settings.loading', '加载中...') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Workspace Section: Only show if settings data is loaded -->
|
<!-- System Tab Content -->
|
||||||
<WorkspaceSettingsSection />
|
<div v-if="activeTab === 'system'">
|
||||||
|
<SystemSettingsSection v-if="settings" />
|
||||||
|
<div v-else class="p-4 text-center text-muted-foreground">{{ $t('settings.loading', '加载中...') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- System Section: Only show if settings data is loaded -->
|
<!-- Data Management Tab Content -->
|
||||||
<SystemSettingsSection />
|
<div v-if="activeTab === 'dataManagement'">
|
||||||
<!-- Data Management Section (including Export) -->
|
<DataManagementSection v-if="settings" />
|
||||||
<DataManagementSection />
|
<div v-else class="p-4 text-center text-muted-foreground">{{ $t('settings.loading', '加载中...') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Appearance Section: Only show if settings data is loaded -->
|
<!-- Appearance Tab Content -->
|
||||||
<AppearanceSection />
|
<div v-if="activeTab === 'appearance'">
|
||||||
|
<AppearanceSection v-if="settings" />
|
||||||
|
<div v-else class="p-4 text-center text-muted-foreground">{{ $t('settings.loading', '加载中...') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- About Tab Content -->
|
||||||
|
<div v-if="activeTab === 'about'">
|
||||||
|
<AboutSection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +84,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'; // Simplified Vue imports
|
import { onMounted, ref } from 'vue'; // Simplified Vue imports
|
||||||
import { useAuthStore } from '../stores/auth.store';
|
import { useAuthStore } from '../stores/auth.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
|
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
|
||||||
@@ -93,6 +107,18 @@ const settingsStore = useSettingsStore();
|
|||||||
const appearanceStore = useAppearanceStore(); // 实例化外观 store
|
const appearanceStore = useAppearanceStore(); // 实例化外观 store
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// Define tabs for settings sections
|
||||||
|
const tabs = ref([
|
||||||
|
{ key: 'security', label: t('settings.tabs.security', '安全') },
|
||||||
|
{ key: 'ipControl', label: t('settings.tabs.ipControl', 'IP 管控') },
|
||||||
|
{ key: 'workspace', label: t('settings.tabs.workspace', '工作区') },
|
||||||
|
{ key: 'system', label: t('settings.tabs.system', '系统') },
|
||||||
|
{ key: 'dataManagement', label: t('settings.tabs.dataManagement', '数据管理') },
|
||||||
|
{ key: 'appearance', label: t('settings.tabs.appearance', '外观') },
|
||||||
|
{ key: 'about', label: t('settings.tabs.about', '关于') },
|
||||||
|
]);
|
||||||
|
const activeTab = ref(tabs.value[0].key);
|
||||||
|
|
||||||
// --- Reactive state from store ---
|
// --- Reactive state from store ---
|
||||||
// 使用 storeToRefs 获取响应式 getter,包括 language
|
// 使用 storeToRefs 获取响应式 getter,包括 language
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onBeforeUnmount, computed, ref } from 'vue';
|
import { onMounted, onBeforeUnmount, computed, ref, shallowRef, type PropType } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'; // +++ 引入 useBreakpoints +++
|
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core';
|
||||||
import { useLayoutStore } from '../stores/layout.store';
|
import { useLayoutStore } from '../stores/layout.store';
|
||||||
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
||||||
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
||||||
import TerminalTabBar from '../components/TerminalTabBar.vue';
|
import TerminalTabBar from '../components/TerminalTabBar.vue';
|
||||||
import LayoutRenderer from '../components/LayoutRenderer.vue';
|
import LayoutRenderer from '../components/LayoutRenderer.vue';
|
||||||
import LayoutConfigurator from '../components/LayoutConfigurator.vue';
|
import LayoutConfigurator from '../components/LayoutConfigurator.vue';
|
||||||
import RemoteDesktopModal from '../components/RemoteDesktopModal.vue';
|
import Terminal from '../components/Terminal.vue';
|
||||||
import VncModal from '../components/VncModal.vue'; // +++ 引入 VncModal 组件 +++
|
import CommandInputBar from '../components/CommandInputBar.vue';
|
||||||
import Terminal from '../components/Terminal.vue'; // +++ 引入 Terminal 组件 +++
|
import VirtualKeyboard from '../components/VirtualKeyboard.vue';
|
||||||
import CommandInputBar from '../components/CommandInputBar.vue'; // +++ 引入 CommandInputBar 组件 +++
|
import FileManager from '../components/FileManager.vue';
|
||||||
import VirtualKeyboard from '../components/VirtualKeyboard.vue'; // +++ 引入 VirtualKeyboard 组件 +++
|
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import type { SessionTabInfoWithStatus, SshTerminalInstance } from '../stores/session/types';
|
import type { SessionTabInfoWithStatus, SshTerminalInstance } from '../stores/session/types';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
useWorkspaceEventOff,
|
useWorkspaceEventOff,
|
||||||
type WorkspaceEventPayloads
|
type WorkspaceEventPayloads
|
||||||
} from '../composables/workspaceEvents';
|
} from '../composables/workspaceEvents';
|
||||||
|
import type { WebSocketDependencies } from '../composables/useSftpActions';
|
||||||
|
|
||||||
// --- Setup ---
|
// --- Setup ---
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -75,6 +75,16 @@ const currentSearchTerm = ref(''); // 当前搜索的关键词
|
|||||||
const mobileTerminalRef = ref<InstanceType<typeof Terminal> | null>(null);
|
const mobileTerminalRef = ref<InstanceType<typeof Terminal> | null>(null);
|
||||||
const isVirtualKeyboardVisible = ref(true); // +++ State for virtual keyboard visibility +++
|
const isVirtualKeyboardVisible = ref(true); // +++ State for virtual keyboard visibility +++
|
||||||
|
|
||||||
|
// --- 文件管理器模态框状态 ---
|
||||||
|
const showFileManagerModal = ref(false);
|
||||||
|
const fileManagerPropsMap = shallowRef<Map<string, {
|
||||||
|
sessionId: string;
|
||||||
|
instanceId: string;
|
||||||
|
dbConnectionId: string;
|
||||||
|
wsDeps: WebSocketDependencies;
|
||||||
|
}>>(new Map());
|
||||||
|
const currentFileManagerSessionId = ref<string | null>(null);
|
||||||
|
|
||||||
// --- 处理全局键盘事件 ---
|
// --- 处理全局键盘事件 ---
|
||||||
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
||||||
// 检查是否按下了 Alt 键以及上/下箭头键
|
// 检查是否按下了 Alt 键以及上/下箭头键
|
||||||
@@ -154,6 +164,7 @@ onMounted(() => {
|
|||||||
subscribeToWorkspaceEvents('session:closeToRight', (payload) => handleCloseSessionsToRight(payload.targetSessionId));
|
subscribeToWorkspaceEvents('session:closeToRight', (payload) => handleCloseSessionsToRight(payload.targetSessionId));
|
||||||
subscribeToWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
|
subscribeToWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
|
||||||
subscribeToWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
|
subscribeToWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
|
||||||
|
subscribeToWorkspaceEvents('fileManager:openModalRequest', handleFileManagerOpenRequest); // +++ 订阅文件管理器打开请求 +++
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -196,6 +207,7 @@ onBeforeUnmount(() => {
|
|||||||
unsubscribeFromWorkspaceEvents('session:closeToRight', (payload) => handleCloseSessionsToRight(payload.targetSessionId));
|
unsubscribeFromWorkspaceEvents('session:closeToRight', (payload) => handleCloseSessionsToRight(payload.targetSessionId));
|
||||||
unsubscribeFromWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
|
unsubscribeFromWorkspaceEvents('session:closeToLeft', (payload) => handleCloseSessionsToLeft(payload.targetSessionId));
|
||||||
unsubscribeFromWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
|
unsubscribeFromWorkspaceEvents('ui:openLayoutConfigurator', handleOpenLayoutConfigurator);
|
||||||
|
unsubscribeFromWorkspaceEvents('fileManager:openModalRequest', handleFileManagerOpenRequest); // +++ 取消订阅文件管理器打开请求 +++
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscribeToWorkspaceEvents = useWorkspaceEventSubscriber(); // +++ 定义订阅和取消订阅函数 +++
|
const subscribeToWorkspaceEvents = useWorkspaceEventSubscriber(); // +++ 定义订阅和取消订阅函数 +++
|
||||||
@@ -313,7 +325,6 @@ const unsubscribeFromWorkspaceEvents = useWorkspaceEventOff();
|
|||||||
// 处理终端大小调整 (用于 Terminal)
|
// 处理终端大小调整 (用于 Terminal)
|
||||||
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-resize', sessionId, dims)
|
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-resize', sessionId, dims)
|
||||||
const handleTerminalResize = (payload: { sessionId: string; dims: { cols: number; rows: number } }) => {
|
const handleTerminalResize = (payload: { sessionId: string; dims: { cols: number; rows: number } }) => {
|
||||||
console.log(`[工作区视图 ${payload.sessionId}] 收到 resize 事件:`, payload.dims);
|
|
||||||
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalResize(payload.dims);
|
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalResize(payload.dims);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -634,6 +645,66 @@ const toggleVirtualKeyboard = () => {
|
|||||||
tabsToClose.forEach(id => handleCloseEditorTab(id));
|
tabsToClose.forEach(id => handleCloseEditorTab(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 文件管理器模态框处理 ---
|
||||||
|
const handleFileManagerOpenRequest = (payload: { sessionId: string }) => {
|
||||||
|
const { sessionId } = payload;
|
||||||
|
const session = sessionStore.sessions.get(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
console.error(`[WorkspaceView] Cannot open file manager: Session ${sessionId} not found.`);
|
||||||
|
// TODO: Show error notification
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 获取 dbConnectionId
|
||||||
|
const dbConnectionId = session.connectionId;
|
||||||
|
if (!dbConnectionId) {
|
||||||
|
console.error(`[WorkspaceView] Cannot open file manager: Missing dbConnectionId for session ${sessionId}.`);
|
||||||
|
// TODO: Show error notification
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取 wsDeps (从 session.wsManager 获取)
|
||||||
|
if (!session.wsManager) {
|
||||||
|
console.error(`[WorkspaceView] Cannot open file manager: wsManager not found for session ${sessionId}.`);
|
||||||
|
// TODO: Show error notification
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wsDeps: WebSocketDependencies = {
|
||||||
|
sendMessage: session.wsManager.sendMessage,
|
||||||
|
onMessage: session.wsManager.onMessage,
|
||||||
|
isConnected: session.wsManager.isConnected,
|
||||||
|
isSftpReady: session.wsManager.isSftpReady,
|
||||||
|
};
|
||||||
|
console.log(`[WorkspaceView] Retrieved wsDeps from session.wsManager for session ${sessionId}.`);
|
||||||
|
|
||||||
|
if (!wsDeps) {
|
||||||
|
// 如果 wsDeps 仍然为 null,则无法继续
|
||||||
|
console.error(`[WorkspaceView] Cannot open file manager: wsDeps are null after attempting retrieval for session ${sessionId}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 生成或获取 instanceId
|
||||||
|
const currentProps = fileManagerPropsMap.value.get(sessionId);
|
||||||
|
const instanceId = currentProps ? currentProps.instanceId : `fm-modal-${sessionId}`;
|
||||||
|
|
||||||
|
// 4. 设置 props 并显示模态框
|
||||||
|
const newProps = {
|
||||||
|
sessionId,
|
||||||
|
instanceId,
|
||||||
|
dbConnectionId: String(dbConnectionId), // 确保是 string
|
||||||
|
wsDeps,
|
||||||
|
};
|
||||||
|
fileManagerPropsMap.value.set(sessionId, newProps);
|
||||||
|
currentFileManagerSessionId.value = sessionId;
|
||||||
|
showFileManagerModal.value = true;
|
||||||
|
console.log(`[WorkspaceView] Opening FileManager modal with props for session ${sessionId}:`, newProps);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFileManagerModal = () => {
|
||||||
|
showFileManagerModal.value = false;
|
||||||
|
console.log('[WorkspaceView] FileManager modal hidden (kept alive).');
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -726,6 +797,32 @@ const toggleVirtualKeyboard = () => {
|
|||||||
|
|
||||||
<!-- RDP Modal is now rendered in App.vue -->
|
<!-- RDP Modal is now rendered in App.vue -->
|
||||||
<!-- VNC Modal is now rendered in App.vue -->
|
<!-- VNC Modal is now rendered in App.vue -->
|
||||||
|
|
||||||
|
<!-- FileManager Modal Container -->
|
||||||
|
<div v-show="showFileManagerModal && currentFileManagerSessionId && fileManagerPropsMap.get(currentFileManagerSessionId)" class="fixed inset-0 flex items-center justify-center z-50 p-4" :style="{ backgroundColor: 'var(--overlay-bg-color)' }" @click.self="closeFileManagerModal">
|
||||||
|
<div class="bg-background rounded-lg shadow-xl w-full max-w-4xl h-[85vh] flex flex-col overflow-hidden border border-border">
|
||||||
|
<div class="flex justify-between items-center p-3 border-b border-border flex-shrink-0 bg-header">
|
||||||
|
<h2 class="text-lg font-semibold text-foreground">{{ t('fileManager.modalTitle', '文件管理器') }} ({{ currentFileManagerSessionId ? (sessionStore.sessions.get(currentFileManagerSessionId)?.connectionName || currentFileManagerSessionId) : '未知会话' }})</h2>
|
||||||
|
<button @click="closeFileManagerModal" class="text-text-secondary hover:text-foreground transition-colors">
|
||||||
|
<i class="fas fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden">
|
||||||
|
<template v-for="props in fileManagerPropsMap.values()" :key="props.sessionId">
|
||||||
|
<div v-show="props.sessionId === currentFileManagerSessionId" class="h-full">
|
||||||
|
<FileManager
|
||||||
|
:session-id="props.sessionId"
|
||||||
|
:instance-id="props.instanceId"
|
||||||
|
:db-connection-id="props.dbConnectionId"
|
||||||
|
:ws-deps="props.wsDeps"
|
||||||
|
class="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user