feat: 适配移动端

只保留最基本的ssh功能
Related to #10
This commit is contained in:
Baobhan Sith
2025-05-04 00:21:13 +08:00
parent 9a93bb8aa6
commit f81e647497
8 changed files with 544 additions and 93 deletions
+46 -1
View File
@@ -1772,6 +1772,12 @@
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.18.1", "version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -2026,6 +2032,44 @@
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vueuse/core": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.1.0.tgz",
"integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.1.0",
"@vueuse/shared": "13.1.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.1.0.tgz",
"integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.1.0.tgz",
"integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@xmldom/xmldom": { "node_modules/@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -8848,12 +8892,13 @@
}, },
"packages/frontend": { "packages/frontend": {
"name": "@nexus-terminal/frontend", "name": "@nexus-terminal/frontend",
"version": "0.2.4", "version": "0.2.5",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@vscode/iconv-lite-umd": "^0.7.0", "@vscode/iconv-lite-umd": "^0.7.0",
"@vueuse/core": "^13.1.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0", "@xterm/addon-search": "^0.15.0",
"axios": "^1.8.4", "axios": "^1.8.4",
+1
View File
@@ -13,6 +13,7 @@
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@vscode/iconv-lite-umd": "^0.7.0", "@vscode/iconv-lite-umd": "^0.7.0",
"@vueuse/core": "^13.1.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0", "@xterm/addon-search": "^0.15.0",
"axios": "^1.8.4", "axios": "^1.8.4",
+6 -6
View File
@@ -260,12 +260,12 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => {
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<!-- 项目 Logo --> <!-- 项目 Logo -->
<img src="./assets/logo.png" alt="Project Logo" class="h-10 w-auto"> <!-- 移除右侧外边距使其更靠左 --> <img src="./assets/logo.png" alt="Project Logo" class="h-10 w-auto"> <!-- 移除右侧外边距使其更靠左 -->
<RouterLink to="/" class="px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.dashboard') }}</RouterLink> <!-- 恢复仪表盘链接 --> <RouterLink to="/" class="inline-flex px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.dashboard') }}</RouterLink> <!-- 恢复仪表盘链接, 始终可见 -->
<RouterLink to="/workspace" class="px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.terminal') }}</RouterLink> <RouterLink to="/workspace" class="inline-flex px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.terminal') }}</RouterLink> <!-- 保持可见 -->
<RouterLink to="/proxies" class="px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.proxies') }}</RouterLink> <RouterLink to="/proxies" class="hidden md:inline-flex px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.proxies') }}</RouterLink> <!-- 移动端隐藏 -->
<RouterLink to="/notifications" class="px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.notifications') }}</RouterLink> <RouterLink to="/notifications" class="hidden md:inline-flex px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.notifications') }}</RouterLink> <!-- 移动端隐藏 -->
<RouterLink to="/audit-logs" class="px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.auditLogs') }}</RouterLink> <RouterLink to="/audit-logs" class="hidden md:inline-flex px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.auditLogs') }}</RouterLink> <!-- 移动端隐藏 -->
<RouterLink to="/settings" class="px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.settings') }}</RouterLink> <RouterLink to="/settings" class="inline-flex px-3 py-2 rounded-md text-sm font-medium text-secondary hover:text-link-hover hover:bg-nav-active-bg hover:no-underline transition duration-150 ease-in-out whitespace-nowrap" active-class="text-link-active bg-nav-active-bg">{{ t('nav.settings') }}</RouterLink> <!-- 保持可见 -->
</div> </div>
<!-- Right navigation links with Tailwind classes using theme variables --> <!-- Right navigation links with Tailwind classes using theme variables -->
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
@@ -7,6 +7,7 @@ import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
import { useSettingsStore } from '../stores/settings.store'; import { useSettingsStore } from '../stores/settings.store';
import { useQuickCommandsStore } from '../stores/quickCommands.store'; 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 +++
const emit = defineEmits(['send-command', 'search', 'find-next', 'find-previous', 'close-search', 'clear-terminal']); // 添加 clear-terminal 事件 const emit = defineEmits(['send-command', 'search', 'find-next', 'find-previous', 'close-search', 'clear-terminal']); // 添加 clear-terminal 事件
const { t } = useI18n(); const { t } = useI18n();
@@ -31,11 +32,14 @@ const { updateSessionCommandInput } = sessionStore;
// Props definition is now empty as search results are no longer handled here // Props definition is now empty as search results are no longer handled here
const props = defineProps<{ const props = defineProps<{
// No props defined here currently // No props defined here currently
// +++ Add isMobile prop +++
isMobile?: boolean;
}>(); }>();
// --- 移除本地 commandInput ref --- // --- 移除本地 commandInput ref ---
// const commandInput = ref(''); // const commandInput = ref('');
const isSearching = ref(false); const isSearching = ref(false);
const searchTerm = ref(''); const searchTerm = ref('');
const showQuickCommands = ref(false); // +++ Add state for modal visibility +++
// *** 移除本地的搜索结果 ref *** // *** 移除本地的搜索结果 ref ***
// const searchResultCount = ref(0); // const searchResultCount = ref(0);
// const currentSearchResultIndex = ref(0); // const currentSearchResultIndex = ref(0);
@@ -244,11 +248,27 @@ onBeforeUnmount(() => {
unregisterTerminalSearchFocus(); unregisterTerminalSearchFocus();
} }
}); });
// +++ Functions to control the quick commands modal +++
const openQuickCommandsModal = () => {
showQuickCommands.value = true;
};
const closeQuickCommandsModal = () => {
showQuickCommands.value = false;
};
// +++ Handler for command execution from the modal +++
const handleQuickCommandExecute = (command: string) => {
console.log(`[CommandInputBar] Executing quick command: ${command}`);
emit('send-command', command); // Emit the command to the parent
closeQuickCommandsModal(); // Close the modal after selection
};
</script> </script>
<template> <template>
<div class="flex items-center px-2 py-1.5 bg-background gap-2"> <!-- Removed border-t and border-border/50 --> <div class="flex items-center px-2 py-1.5 bg-background gap-2">
<div class="flex-grow flex items-center bg-transparent relative gap-2"> <!-- Adjusted gap --> <div class="flex-grow flex items-center bg-transparent relative gap-2">
<!-- Clear Terminal Button --> <!-- Clear Terminal Button -->
<button <button
@click="emit('clear-terminal')" @click="emit('clear-terminal')"
@@ -257,34 +277,46 @@ onBeforeUnmount(() => {
> >
<i class="fas fa-eraser text-base"></i> <i class="fas fa-eraser text-base"></i>
</button> </button>
<!-- Focus Switcher Config Button --> <!-- +++ Quick Commands Button (Mobile only) +++ -->
<button
v-if="props.isMobile"
@click="openQuickCommandsModal"
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"
:title="t('quickCommands.title', '快捷指令')"
>
<i class="fas fa-bolt text-base"></i>
</button>
<!-- Focus Switcher Config Button (Hide on mobile) -->
<button <button
v-if="!props.isMobile"
@click="focusSwitcherStore.toggleConfigurator(true)" @click="focusSwitcherStore.toggleConfigurator(true)"
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" 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"
:title="t('commandInputBar.configureFocusSwitch', '配置焦点切换')" :title="t('commandInputBar.configureFocusSwitch', '配置焦点切换')"
> >
<i class="fas fa-keyboard text-base"></i> <!-- Removed text-primary --> <i class="fas fa-keyboard text-base"></i> <!-- Removed text-primary -->
</button> </button>
<!-- Command Input --> <!-- Command Input (Hide on mobile when searching) -->
<input <input
v-if="!props.isMobile || !isSearching"
type="text" type="text"
v-model="currentSessionCommandInput" v-model="currentSessionCommandInput"
:placeholder="t('commandInputBar.placeholder')" :placeholder="t('commandInputBar.placeholder')"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-300 ease-in-out" class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-300 ease-in-out"
:class="{ 'basis-3/4': isSearching, 'basis-full': !isSearching }" :class="{ 'basis-3/4': !props.isMobile && isSearching, 'basis-full': !isSearching }"
ref="commandInputRef" ref="commandInputRef"
data-focus-id="commandInput" data-focus-id="commandInput"
@keydown="handleCommandInputKeydown" @keydown="handleCommandInputKeydown"
@blur="handleCommandInputBlur" @blur="handleCommandInputBlur"
/> />
<!-- Search Input (Conditional rendering with v-show for transition) --> <!-- Search Input (Show when searching, adjust width on mobile) -->
<input <input
v-show="isSearching" v-if="isSearching"
type="text" type="text"
v-model="searchTerm" v-model="searchTerm"
:placeholder="t('commandInputBar.searchPlaceholder')" :placeholder="t('commandInputBar.searchPlaceholder')"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-300 ease-in-out basis-1/4" class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-300 ease-in-out"
:class="{ 'basis-1/4': !props.isMobile, 'basis-full': props.isMobile }"
data-focus-id="terminalSearch" data-focus-id="terminalSearch"
@keydown.enter.prevent="findNext" @keydown.enter.prevent="findNext"
@keydown.shift.enter.prevent="findPrevious" @keydown.shift.enter.prevent="findPrevious"
@@ -304,7 +336,8 @@ onBeforeUnmount(() => {
<i v-else class="fas fa-times text-base"></i> <i v-else class="fas fa-times text-base"></i>
</button> </button>
<template v-if="isSearching"> <!-- Search navigation buttons (Hide on mobile when searching) -->
<template v-if="isSearching && !props.isMobile"> <!-- +++ Add !props.isMobile condition +++ -->
<button <button
@click="findPrevious" @click="findPrevious"
class="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" class="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"
@@ -320,10 +353,17 @@ onBeforeUnmount(() => {
<i class="fas fa-arrow-down text-base"></i> <i class="fas fa-arrow-down text-base"></i>
</button> </button>
</template> </template>
<!-- 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>
</div> </div>
<!-- +++ Quick Commands Modal Instance +++ -->
<QuickCommandsModal
:is-visible="showQuickCommands"
@close="closeQuickCommandsModal"
@execute-command="handleQuickCommandExecute"
/>
</template> </template>
<style scoped> <style scoped>
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { defineProps, defineEmits, watch } from 'vue';
import QuickCommandsView from '../views/QuickCommandsView.vue'; // 导入视图
const props = defineProps<{
isVisible: boolean;
}>();
const emit = defineEmits<{
(e: 'close'): void;
(e: 'execute-command', command: string): void;
}>();
const closeModal = () => {
emit('close');
};
// 处理从 QuickCommandsView 传来的事件
const handleCommandExecute = (command: string) => {
emit('execute-command', command);
closeModal(); // 选择指令后自动关闭
};
// Optional: Add keyboard listener to close on Esc key
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeModal();
}
};
watch(() => props.isVisible, (newValue) => {
if (newValue) {
document.addEventListener('keydown', handleKeydown);
} else {
document.removeEventListener('keydown', handleKeydown);
}
});
// Clean up listener on unmount (though v-if usually handles this)
import { onUnmounted } from 'vue';
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
});
</script>
<template>
<div v-if="isVisible" class="fixed inset-0 bg-overlay flex justify-center items-center z-50 p-4" @click.self="closeModal">
<div class="bg-background text-foreground p-4 rounded-lg shadow-xl border border-border w-full max-w-lg max-h-[85vh] flex flex-col relative">
<!-- Close Button -->
<button class="absolute top-2 right-2 p-1 text-text-secondary hover:text-foreground z-10" @click="closeModal" title="关闭">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Title -->
<h3 class="text-lg font-semibold text-center mb-3 flex-shrink-0">快捷指令</h3>
<!-- Quick Commands View Embedded -->
<div class="flex-grow overflow-hidden border border-border rounded">
<QuickCommandsView @execute-command="handleCommandExecute" />
</div>
</div>
</div>
</template>
<style scoped>
/* Add any specific modal styles if needed */
.bg-overlay {
background-color: rgba(0, 0, 0, 0.6);
}
</style>
@@ -29,6 +29,11 @@ const props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
// +++ 添加 isMobile prop +++
isMobile: {
type: Boolean,
default: false,
},
}); });
// 定义事件 (使用对象语法修复类型) // 定义事件 (使用对象语法修复类型)
@@ -259,7 +264,10 @@ const toggleButtonTitle = computed(() => {
</script> </script>
<template> <template>
<div class="flex bg-header border border-border rounded-t-md mx-2 mt-2 overflow-hidden h-10"> <!-- +++ 使用 :class 绑定来条件化样式 +++ -->
<div :class="['flex bg-header border border-border overflow-hidden h-10',
{ 'rounded-t-md mx-2 mt-2': !isMobile } // 只在非移动端应用这些类
]">
<div class="flex items-center overflow-x-auto flex-shrink min-w-0"> <div class="flex items-center overflow-x-auto flex-shrink min-w-0">
<ul class="flex list-none p-0 m-0 h-full flex-shrink-0"> <ul class="flex list-none p-0 m-0 h-full flex-shrink-0">
<li <li
@@ -302,7 +310,8 @@ const toggleButtonTitle = computed(() => {
> >
<i :class="[eyeIconClass, 'text-sm']"></i> <i :class="[eyeIconClass, 'text-sm']"></i>
</button> </button>
<button class="flex items-center justify-center px-3 h-full border-l border-border text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150" <!-- +++ 使用 v-if 隐藏移动端的布局按钮 +++ -->
<button v-if="!isMobile" class="flex items-center justify-center px-3 h-full border-l border-border text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150"
@click="openLayoutConfigurator" :title="t('layout.configure', '配置布局')"> @click="openLayoutConfigurator" :title="t('layout.configure', '配置布局')">
<i class="fas fa-th-large text-sm"></i> <i class="fas fa-th-large text-sm"></i>
</button> </button>
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { ref, defineEmits } from 'vue'; // +++ Import ref +++
const emit = defineEmits<{
(e: 'send-key', keySequence: string): void;
}>();
// +++ Add state for modifier keys +++
const isCtrlActive = ref(false);
const isAltActive = ref(false);
// +++ Function to toggle modifier state +++
const toggleModifier = (modifier: 'ctrl' | 'alt') => {
if (modifier === 'ctrl') {
isCtrlActive.value = !isCtrlActive.value;
isAltActive.value = false; // Ctrl and Alt are mutually exclusive
} else if (modifier === 'alt') {
isAltActive.value = !isAltActive.value;
isCtrlActive.value = false; // Ctrl and Alt are mutually exclusive
}
};
// +++ Modified sendKey function +++
const sendKey = (keyDef: KeyDefinition) => {
// Handle modifier key clicks
if (keyDef.type === 'modifier') {
toggleModifier(keyDef.label.toLowerCase() as 'ctrl' | 'alt');
return; // Just toggle state, don't emit anything
}
// Determine the sequence to send
let sequence = keyDef.sequence ?? keyDef.label; // Default to label if no sequence (e.g., for 'A')
if (isCtrlActive.value) {
// Handle Ctrl combinations (example: convert A-Z to control characters 1-26)
if (keyDef.type === 'char' && keyDef.label.length === 1 && keyDef.label >= 'A' && keyDef.label <= 'Z') {
sequence = String.fromCharCode(keyDef.label.charCodeAt(0) - 'A'.charCodeAt(0) + 1);
} else if (keyDef.label === 'Ctrl+C') { // Keep predefined Ctrl+C
sequence = '\x03';
}
// Add more Ctrl combinations here if needed
console.log(`[VirtualKeyboard] Sending Ctrl + ${keyDef.label} as ${JSON.stringify(sequence)}`);
} else if (isAltActive.value) {
// Handle Alt combinations (typically prefix with ESC)
sequence = '\x1b' + sequence;
console.log(`[VirtualKeyboard] Sending Alt + ${keyDef.label} as ${JSON.stringify(sequence)}`);
} else {
// Send the standard sequence
console.log(`[VirtualKeyboard] Sending key: ${JSON.stringify(sequence)}`);
}
// Emit the final sequence
emit('send-key', sequence);
// Reset modifier state after sending a combined key
if (isCtrlActive.value || isAltActive.value) {
isCtrlActive.value = false;
isAltActive.value = false;
}
};
// +++ Define key structure +++
interface KeyDefinition {
label: string;
sequence?: string; // Sequence if different from label
type: 'modifier' | 'control' | 'char' | 'navigation' | 'special'; // Key type
}
// +++ Updated key layout definition +++
const keys: KeyDefinition[] = [
// Row 1: Modifiers and special controls
{ label: 'Ctrl', type: 'modifier' },
{ label: 'Alt', type: 'modifier' },
{ label: 'Tab', sequence: '\t', type: 'control' },
{ label: 'Esc', sequence: '\x1b', type: 'control' },
// Row 2: Navigation and common symbols
{ label: '↑', sequence: '\x1b[A', type: 'navigation' },
{ label: '↓', sequence: '\x1b[B', type: 'navigation' },
{ label: '←', sequence: '\x1b[D', type: 'navigation' },
{ label: '→', sequence: '\x1b[C', type: 'navigation' },
{ label: 'Home', sequence: '\x1b[1~', type: 'navigation' }, // +++ Home +++
{ label: 'End', sequence: '\x1b[4~', type: 'navigation' }, // +++ End +++
{ label: 'PgUp', sequence: '\x1b[5~', type: 'navigation' }, // +++ PageUp +++
{ label: 'PgDn', sequence: '\x1b[6~', type: 'navigation' }, // +++ PageDown +++
// Row 3: Example character keys for combinations
{ label: 'A', type: 'char' },
{ label: 'B', type: 'char' },
{ label: 'C', type: 'char' },
{ label: 'D', type: 'char' },
{ label: 'F', type: 'char' },
// Add more letters, numbers, or symbols as needed
];
</script>
<template>
<!-- +++ Updated template loop and bindings +++ -->
<div class="virtual-keyboard-bar flex flex-wrap items-center justify-center gap-1 p-1 bg-background border-t border-border">
<button
v-for="keyDef in keys"
:key="keyDef.label"
@click="sendKey(keyDef)"
class="px-3 py-1.5 rounded border border-border bg-input text-foreground text-xs hover:bg-border focus:outline-none focus:ring-1 focus:ring-primary transition-colors duration-150"
:class="{
'bg-primary text-primary-foreground hover:bg-primary/90': // Style for active modifiers
(keyDef.label === 'Ctrl' && isCtrlActive) ||
(keyDef.label === 'Alt' && isAltActive)
}"
:title="keyDef.label"
>
{{ keyDef.label }}
</button>
</div>
</template>
<style scoped>
.virtual-keyboard-bar {
/* Base styles */
flex-wrap: wrap; /* Allow wrapping */
}
button {
min-width: 40px; /* Ensure tappable area */
text-align: center;
}
/* Optional: Add specific styles for modifier keys */
/*
button[title="Ctrl"], button[title="Alt"] {
font-weight: bold;
}
*/
</style>
+228 -75
View File
@@ -2,19 +2,23 @@
import { onMounted, onBeforeUnmount, computed, ref } from 'vue'; import { onMounted, onBeforeUnmount, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useLayoutStore } from '../stores/layout.store'; import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'; // +++ 引入 useBreakpoints +++
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store'; import { useLayoutStore } from '../stores/layout.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 RemoteDesktopModal from '../components/RemoteDesktopModal.vue';
import Terminal from '../components/Terminal.vue'; // +++ 引入 Terminal 组件 +++
import CommandInputBar from '../components/CommandInputBar.vue'; // +++ 引入 CommandInputBar 组件 +++
import VirtualKeyboard from '../components/VirtualKeyboard.vue'; // +++ 引入 VirtualKeyboard 组件 +++
import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store'; import { useSessionStore, type SessionTabInfoWithStatus, type SshTerminalInstance } from '../stores/session.store';
import { useSettingsStore } from '../stores/settings.store'; import { useSettingsStore } from '../stores/settings.store';
import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store'; import { useFileEditorStore, type FileTab } from '../stores/fileEditor.store';
import { useCommandHistoryStore } from '../stores/commandHistory.store'; import { useCommandHistoryStore } from '../stores/commandHistory.store';
import type { Terminal } from 'xterm'; import type { Terminal as XtermTerminal } from 'xterm'; // --- 重命名避免冲突 ---
import type { ISearchOptions } from '@xterm/addon-search'; import type { ISearchOptions } from '@xterm/addon-search';
// --- Setup --- // --- Setup ---
const { t } = useI18n(); const { t } = useI18n();
@@ -25,6 +29,8 @@ const layoutStore = useLayoutStore();
const commandHistoryStore = useCommandHistoryStore(); const commandHistoryStore = useCommandHistoryStore();
const connectionsStore = useConnectionsStore(); const connectionsStore = useConnectionsStore();
const { isHeaderVisible } = storeToRefs(layoutStore); const { isHeaderVisible } = storeToRefs(layoutStore);
const breakpoints = useBreakpoints(breakpointsTailwind); // +++ 初始化 Breakpoints +++
const isMobile = breakpoints.smaller('md'); // +++ 定义 isMobile (小于 md 断点) +++
// --- 从 Store 获取响应式状态和 Getters --- // --- 从 Store 获取响应式状态和 Getters ---
const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 状态 const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 状态
@@ -59,6 +65,7 @@ const showLayoutConfigurator = ref(false); // 控制布局配置器可见性
// --- 搜索状态 --- // --- 搜索状态 ---
const currentSearchTerm = ref(''); // 当前搜索的关键词 const currentSearchTerm = ref(''); // 当前搜索的关键词
const mobileTerminalRef = ref<InstanceType<typeof Terminal> | null>(null); // +++ 添加 mobileTerminalRef +++
// --- 新增:处理全局键盘事件 --- // --- 新增:处理全局键盘事件 ---
const handleGlobalKeyDown = (event: KeyboardEvent) => { const handleGlobalKeyDown = (event: KeyboardEvent) => {
@@ -224,7 +231,7 @@ onBeforeUnmount(() => {
// 处理终端就绪 (用于 Terminal) // 处理终端就绪 (用于 Terminal)
// 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-ready', payload) // 注意:LayoutRenderer 内部的 Terminal 组件需要 emit('terminal-ready', payload)
// *** 修正:更新 payload 类型以包含 searchAddon *** // *** 修正:更新 payload 类型以包含 searchAddon ***
const handleTerminalReady = (payload: { sessionId: string; terminal: Terminal; searchAddon: any | null }) => { // 使用 any 避免导入 SearchAddon 类型 const handleTerminalReady = (payload: { sessionId: string; terminal: XtermTerminal; searchAddon: any | null }) => { // --- 使用重命名的 XtermTerminal ---
console.log(`[工作区视图 ${payload.sessionId}] 收到 terminal-ready 事件。Payload:`, payload); // *** 添加 Payload 日志 *** console.log(`[工作区视图 ${payload.sessionId}] 收到 terminal-ready 事件。Payload:`, payload); // *** 添加 Payload 日志 ***
// *** 检查 payload 中 searchAddon 是否存在 *** // *** 检查 payload 中 searchAddon 是否存在 ***
if (payload && payload.searchAddon) { if (payload && payload.searchAddon) {
@@ -237,7 +244,7 @@ onBeforeUnmount(() => {
}; };
// --- 搜索事件处理 --- // --- 搜索事件处理 ---
const handleSearch = (term: string) => { const handleSearch = (term: string) => { // +++ 修改 +++
currentSearchTerm.value = term; currentSearchTerm.value = term;
if (!term) { if (!term) {
// 如果搜索词为空,清除搜索 // 如果搜索词为空,清除搜索
@@ -246,52 +253,90 @@ const handleSearch = (term: string) => {
} }
console.log(`[WorkspaceView] Received search event: "${term}"`); console.log(`[WorkspaceView] Received search event: "${term}"`);
// 默认向前搜索 // 默认向前搜索
handleFindNext(); // 触发 findNext
handleFindNext(); // 保持调用 findNext,内部会处理 isMobile
}; };
const handleFindNext = () => { const handleFindNext = () => { // +++ 修改 +++
const manager = activeSession.value?.terminalManager; if (isMobile.value) {
if (manager && currentSearchTerm.value) { if (mobileTerminalRef.value && currentSearchTerm.value) {
console.log(`[WorkspaceView] Calling findNext for term: "${currentSearchTerm.value}"`); console.log(`[WorkspaceView Mobile] Calling findNext for term: "${currentSearchTerm.value}"`);
const found = manager.searchNext(currentSearchTerm.value, { incremental: true }); const found = mobileTerminalRef.value.findNext(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView] findNext returned: ${found}`); // 打印返回值 console.log(`[WorkspaceView Mobile] findNext returned: ${found}`);
if (!found) { if (!found) {
console.log(`[WorkspaceView] findNext: No more results for "${currentSearchTerm.value}"`); console.log(`[WorkspaceView Mobile] findNext: No more results for "${currentSearchTerm.value}"`);
// 可以添加 UI 提示,例如短暂高亮搜索框 }
} else {
console.warn(`[WorkspaceView Mobile] Cannot findNext, no mobile terminal ref or search term.`);
} }
} else { } else {
console.warn(`[WorkspaceView] Cannot findNext, no active session manager or search term.`); // --- 桌面端逻辑 ---
const manager = activeSession.value?.terminalManager;
if (manager && currentSearchTerm.value) {
console.log(`[WorkspaceView Desktop] Calling findNext for term: "${currentSearchTerm.value}"`);
const found = manager.searchNext(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView Desktop] findNext returned: ${found}`);
if (!found) {
console.log(`[WorkspaceView Desktop] findNext: No more results for "${currentSearchTerm.value}"`);
}
} else {
console.warn(`[WorkspaceView Desktop] Cannot findNext, no active session manager or search term.`);
}
} }
}; };
const handleFindPrevious = () => { const handleFindPrevious = () => { // +++ 修改 +++
const manager = activeSession.value?.terminalManager; if (isMobile.value) {
if (manager && currentSearchTerm.value) { if (mobileTerminalRef.value && currentSearchTerm.value) {
console.log(`[WorkspaceView] Calling findPrevious for term: "${currentSearchTerm.value}"`); console.log(`[WorkspaceView Mobile] Calling findPrevious for term: "${currentSearchTerm.value}"`);
const found = manager.searchPrevious(currentSearchTerm.value, { incremental: true }); const found = mobileTerminalRef.value.findPrevious(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView] findPrevious returned: ${found}`); // 打印返回值 console.log(`[WorkspaceView Mobile] findPrevious returned: ${found}`);
if (!found) { if (!found) {
console.log(`[WorkspaceView] findPrevious: No previous results for "${currentSearchTerm.value}"`); console.log(`[WorkspaceView Mobile] findPrevious: No previous results for "${currentSearchTerm.value}"`);
// 可以添加 UI 提示 }
} else {
console.warn(`[WorkspaceView Mobile] Cannot findPrevious, no mobile terminal ref or search term.`);
} }
} else { } else {
console.warn(`[WorkspaceView] Cannot findPrevious, no active session manager or search term.`); // --- 桌面端逻辑 ---
const manager = activeSession.value?.terminalManager;
if (manager && currentSearchTerm.value) {
console.log(`[WorkspaceView Desktop] Calling findPrevious for term: "${currentSearchTerm.value}"`);
const found = manager.searchPrevious(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView Desktop] findPrevious returned: ${found}`);
if (!found) {
console.log(`[WorkspaceView Desktop] findPrevious: No previous results for "${currentSearchTerm.value}"`);
}
} else {
console.warn(`[WorkspaceView Desktop] Cannot findPrevious, no active session manager or search term.`);
}
} }
}; };
const handleCloseSearch = () => { const handleCloseSearch = () => { // +++ 修改 +++
console.log(`[WorkspaceView] Received close-search event.`); console.log(`[WorkspaceView] Received close-search event.`);
currentSearchTerm.value = ''; // 清空搜索词 currentSearchTerm.value = ''; // 清空搜索词
const manager = activeSession.value?.terminalManager; if (isMobile.value) {
if (manager) { if (mobileTerminalRef.value) {
manager.clearTerminalSearch(); mobileTerminalRef.value.clearSearch();
console.log(`[WorkspaceView Mobile] Search cleared.`);
} else {
console.warn(`[WorkspaceView Mobile] Cannot clear search, no mobile terminal ref.`);
}
} else { } else {
console.warn(`[WorkspaceView] Cannot clear search, no active session manager.`); // --- 桌面端逻辑 ---
const manager = activeSession.value?.terminalManager;
if (manager) {
manager.clearTerminalSearch();
console.log(`[WorkspaceView Desktop] Search cleared.`);
} else {
console.warn(`[WorkspaceView Desktop] Cannot clear search, no active session manager.`);
}
} }
}; };
// +++ 新增:处理清空终端事件 +++ // +++ 新增:处理清空终端事件 +++
const handleClearTerminal = () => { const handleClearTerminal = () => { // +++ 修改 +++
const currentSession = activeSession.value; const currentSession = activeSession.value;
if (!currentSession) { if (!currentSession) {
console.warn('[WorkspaceView] Cannot clear terminal, no active session.'); console.warn('[WorkspaceView] Cannot clear terminal, no active session.');
@@ -299,11 +344,21 @@ const handleClearTerminal = () => {
} }
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined); const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
// 调用 Terminal.vue 组件暴露的 clear 方法 // 调用 Terminal.vue 组件暴露的 clear 方法
if (terminalManager && terminalManager.terminalInstance?.value && typeof terminalManager.terminalInstance.value.clear === 'function') { if (isMobile.value) {
console.log(`[WorkspaceView] Clearing terminal for active session ${currentSession.sessionId}`); if (mobileTerminalRef.value) {
terminalManager.terminalInstance.value.clear(); mobileTerminalRef.value.clear();
console.log(`[WorkspaceView Mobile] Terminal cleared.`);
} else {
console.warn(`[WorkspaceView Mobile] Cannot clear terminal, no mobile terminal ref.`);
}
} else { } else {
console.warn(`[WorkspaceView] Cannot clear terminal for session ${currentSession.sessionId}, terminal manager, instance, or clear method not available.`); // --- 桌面端逻辑 ---
if (terminalManager && terminalManager.terminalInstance?.value && typeof terminalManager.terminalInstance.value.clear === 'function') {
console.log(`[WorkspaceView Desktop] Clearing terminal for active session ${currentSession.sessionId}`);
terminalManager.terminalInstance.value.clear();
} else {
console.warn(`[WorkspaceView Desktop] Cannot clear terminal for session ${currentSession.sessionId}, terminal manager, instance, or clear method not available.`);
}
} }
}; };
@@ -402,6 +457,24 @@ const handleCloseEditorTab = (tabId: string) => {
sessionStore.handleOpenNewSession(id); sessionStore.handleOpenNewSession(id);
}; };
// +++ 新增:处理虚拟键盘按键事件 +++
const handleVirtualKeyPress = (keySequence: string) => {
const currentSession = activeSession.value;
if (!currentSession) {
console.warn('[WorkspaceView] Cannot send virtual key, no active session.');
return;
}
// 在移动端模式下,我们假设 terminalManager 总是存在的(如果会话活动)
// 并且直接发送数据,因为虚拟键盘通常用于发送控制字符或特殊序列
const terminalManager = currentSession.terminalManager as (SshTerminalInstance | undefined);
if (terminalManager && typeof terminalManager.sendData === 'function') {
console.log(`[WorkspaceView Mobile] Sending virtual key sequence: ${JSON.stringify(keySequence)}`);
terminalManager.sendData(keySequence);
} else {
console.warn(`[WorkspaceView Mobile] Cannot send virtual key for session ${currentSession.sessionId}, terminal manager or sendData method not available.`);
}
};
// RDP 事件处理方法已被移除 // RDP 事件处理方法已被移除
// --- 标签页关闭操作处理 --- // --- 标签页关闭操作处理 ---
@@ -459,12 +532,13 @@ const handleCloseEditorTab = (tabId: string) => {
</script> </script>
<template> <template>
<!-- *** 确保动态 class 绑定存在 *** --> <!-- *** 动态 class 绑定添加 is-mobile *** -->
<div :class="['workspace-view', { 'with-header': isHeaderVisible }]"> <div :class="['workspace-view', { 'with-header': isHeaderVisible, 'is-mobile': isMobile }]">
<!-- TerminalTabBar 始终渲染 --> <!-- TerminalTabBar 始终渲染, 传递 isMobile 状态 -->
<TerminalTabBar <TerminalTabBar
:sessions="sessionTabsWithStatus" :sessions="sessionTabsWithStatus"
:active-session-id="activeSessionId" :active-session-id="activeSessionId"
:is-mobile="isMobile"
@activate-session="sessionStore.activateSession" @activate-session="sessionStore.activateSession"
@close-session="sessionStore.closeSession" @close-session="sessionStore.closeSession"
@open-layout-configurator="handleOpenLayoutConfigurator" @open-layout-configurator="handleOpenLayoutConfigurator"
@@ -475,44 +549,80 @@ const handleCloseEditorTab = (tabId: string) => {
@close-sessions-to-left="handleCloseSessionsToLeft" @close-sessions-to-left="handleCloseSessionsToLeft"
/> />
<!-- 移除 :class 绑定 --> <!-- --- 桌面端布局 --- -->
<div class="main-content-area"> <template v-if="!isMobile">
<LayoutRenderer <div class="main-content-area">
v-if="layoutTree" <LayoutRenderer
:is-root-renderer="true" v-if="layoutTree"
:layout-node="layoutTree" :is-root-renderer="true"
:active-session-id="activeSessionId" :layout-node="layoutTree"
class="layout-renderer-wrapper" :active-session-id="activeSessionId"
:editor-tabs="editorTabs" class="layout-renderer-wrapper"
:active-editor-tab-id="activeEditorTabId" :editor-tabs="editorTabs"
:active-editor-tab-id="activeEditorTabId"
@send-command="handleSendCommand"
@terminal-input="handleTerminalInput"
@terminal-resize="handleTerminalResize"
@terminal-ready="handleTerminalReady"
@close-editor-tab="handleCloseEditorTab"
@activate-editor-tab="handleActivateEditorTab"
@update-editor-content="handleUpdateEditorContent"
@save-editor-tab="handleSaveEditorTab"
@connect-request="handleConnectRequest"
@open-new-session="handleOpenNewSession"
@request-add-connection="handleRequestAddConnection"
@request-edit-connection="handleRequestEditConnection"
@search="handleSearch"
@find-next="handleFindNext"
@find-previous="handleFindPrevious"
@close-search="handleCloseSearch"
@clear-terminal="handleClearTerminal"
@change-encoding="handleChangeEncoding"
@close-other-tabs="handleCloseOtherEditorTabs"
@close-tabs-to-right="handleCloseEditorTabsToRight"
@close-tabs-to-left="handleCloseEditorTabsToLeft"
></LayoutRenderer>
<div v-else class="pane-placeholder">
{{ t('layout.loading', '加载布局中...') }}
</div>
</div>
</template>
<!-- --- 移动端布局 --- -->
<template v-else>
<div class="mobile-content-area">
<Terminal
v-if="activeSessionId"
ref="mobileTerminalRef"
:session-id="activeSessionId"
:is-active="true"
class="mobile-terminal"
@data="(data) => handleTerminalInput({ sessionId: activeSessionId!, data })"
@resize="(dims) => handleTerminalResize({ sessionId: activeSessionId!, dims })"
@ready="(payload) => handleTerminalReady({ ...payload, sessionId: activeSessionId! })"
/>
<div v-else class="pane-placeholder">
{{ t('workspace.noActiveSession', '没有活动的会话') }}
</div>
</div>
<CommandInputBar
class="mobile-command-bar"
:is-mobile="isMobile"
@send-command="handleSendCommand" @send-command="handleSendCommand"
@terminal-input="handleTerminalInput"
@terminal-resize="handleTerminalResize"
@terminal-ready="handleTerminalReady"
@close-editor-tab="handleCloseEditorTab"
@activate-editor-tab="handleActivateEditorTab"
@update-editor-content="handleUpdateEditorContent"
@save-editor-tab="handleSaveEditorTab"
@connect-request="handleConnectRequest"
@open-new-session="handleOpenNewSession"
@request-add-connection="handleRequestAddConnection"
@request-edit-connection="handleRequestEditConnection"
@search="handleSearch" @search="handleSearch"
@find-next="handleFindNext" @find-next="handleFindNext"
@find-previous="handleFindPrevious" @find-previous="handleFindPrevious"
@close-search="handleCloseSearch" @close-search="handleCloseSearch"
@clear-terminal="handleClearTerminal" @clear-terminal="handleClearTerminal"
@change-encoding="handleChangeEncoding" />
@close-other-tabs="handleCloseOtherEditorTabs" <!-- +++ 添加虚拟键盘监听事件 +++ -->
@close-tabs-to-right="handleCloseEditorTabsToRight" <VirtualKeyboard
@close-tabs-to-left="handleCloseEditorTabsToLeft" class="mobile-virtual-keyboard"
></LayoutRenderer> <!-- 修正使用单独的结束标签 --> @send-key="handleVirtualKeyPress"
<div v-else class="pane-placeholder"> <!-- 确保 v-else 紧随 v-if --> />
{{ t('layout.loading', '加载布局中...') }} </template>
</div>
</div>
<!-- Modals should be outside the main content flow --> <!-- Modals 保持不变应在布局之外 -->
<AddConnectionFormComponent <AddConnectionFormComponent
v-if="showAddEditForm" v-if="showAddEditForm"
:connection-to-edit="connectionToEdit" :connection-to-edit="connectionToEdit"
@@ -526,13 +636,12 @@ const handleCloseEditorTab = (tabId: string) => {
@close="handleCloseLayoutConfigurator" @close="handleCloseLayoutConfigurator"
/> />
<!-- RDP Modal (使用 Store 状态控制) -->
<RemoteDesktopModal <RemoteDesktopModal
v-if="isRdpModalOpen" v-if="isRdpModalOpen"
:connection="rdpConnectionInfo" :connection="rdpConnectionInfo"
@close="sessionStore.closeRdpModal()" @close="sessionStore.closeRdpModal()"
/> />
</div> <!-- End of root element --> </div>
</template> </template>
<style scoped> <style scoped>
@@ -582,6 +691,50 @@ const handleCloseEditorTab = (tabId: string) => {
padding: var(--base-padding); /* Use base padding variable */ padding: var(--base-padding); /* Use base padding variable */
} }
/* 移除旧的、不再需要的特定面板样式,因为渲染由 LayoutRenderer 处理 */
/* --- Mobile Layout Styles --- */
.workspace-view.is-mobile {
/* Ensure flex column layout */
display: flex; /* Uncommented */
flex-direction: column; /* Uncommented */
/* Height is already handled by .workspace-view and .with-header */
}
.workspace-view.is-mobile .main-content-area {
/* Hide the desktop content area in mobile view */
display: none;
}
.mobile-content-area {
display: flex; /* Use flex for the terminal container */
flex-direction: column; /* Stack elements vertically if needed */
flex-grow: 1; /* Allow this area to take up remaining space */
overflow: hidden; /* Prevent overflow */
position: relative; /* Needed for potential absolute positioning inside */
/* Remove desktop margins/borders */
margin: 0;
border: none;
border-radius: 0;
}
.mobile-terminal {
flex-grow: 1; /* Terminal takes all available space in mobile-content-area */
width: 100%;
overflow: hidden;
}
.mobile-command-bar {
flex-shrink: 0; /* Prevent command bar from shrinking */
/* Add specific styles if needed, e.g., border-top */
border-top: 1px solid var(--border-color, #ccc);
}
.mobile-virtual-keyboard {
flex-shrink: 0; /* 防止虚拟键盘缩小 */
/* 可以添加更多样式,例如背景色、边框等 */
}
/* Ensure modals are still displayed correctly (they are outside the main flow) */
</style> </style>