update
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
# local/docker
|
||||
DEPLOYMENT_MODE=docker
|
||||
DEPLOYMENT_MODE=local
|
||||
|
||||
RDP_SERVICE_URL_DOCKER=ws://rdp:8081
|
||||
|
||||
RDP_SERVICE_URL_LOCAL=ws://localhost:8081
|
||||
VNC_SERVICE_URL_DOCKER=ws://vnc:8082
|
||||
VNC_SERVICE_URL_LOCAL=ws://localhost:8082
|
||||
|
||||
# Backend API Base URLs
|
||||
RDP_BACKEND_API_BASE_DOCKER=http://nexus-rdp:9090
|
||||
RDP_BACKEND_API_BASE_LOCAL=http://localhost:9090
|
||||
VNC_BACKEND_API_BASE_DOCKER=http://nexus-vnc:9091
|
||||
VNC_BACKEND_API_BASE_LOCAL=http://localhost:9091
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import fs from 'fs'; // fs is needed for early env loading if data/.env is checked
|
||||
|
||||
// --- 开始环境变量的早期加载 ---
|
||||
// 1. 加载根目录的 .env 文件 (定义部署模式等)
|
||||
// 注意: __dirname 在 dist/src 中,所以需要回退三级到项目根目录
|
||||
const projectRootEnvPath = path.resolve(__dirname, '../../../.env');
|
||||
const rootConfigResult = dotenv.config({ path: projectRootEnvPath });
|
||||
|
||||
if (rootConfigResult.error && (rootConfigResult.error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.warn(`[ENV Init Early] Warning: Could not load root .env file from ${projectRootEnvPath}. Error: ${rootConfigResult.error.message}`);
|
||||
} else if (!rootConfigResult.error) {
|
||||
console.log(`[ENV Init Early] Loaded environment variables from root .env file: ${projectRootEnvPath}`);
|
||||
} else {
|
||||
console.log(`[ENV Init Early] Root .env file not found at ${projectRootEnvPath}, proceeding without it.`);
|
||||
}
|
||||
|
||||
// 2. 加载 data/.env 文件 (定义密钥等)
|
||||
// 注意: 这个路径是相对于编译后的 dist/src/index.js
|
||||
const dataEnvPathGlobal = path.resolve(__dirname, '../data/.env'); // Renamed to avoid conflict if 'dataEnvPath' is used later
|
||||
const dataConfigResultGlobal = dotenv.config({ path: dataEnvPathGlobal }); // Renamed
|
||||
|
||||
if (dataConfigResultGlobal.error && (dataConfigResultGlobal.error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.warn(`[ENV Init Early] Warning: Could not load data .env file from ${dataEnvPathGlobal}. Error: ${dataConfigResultGlobal.error.message}`);
|
||||
} else if (!dataConfigResultGlobal.error) {
|
||||
console.log(`[ENV Init Early] Loaded environment variables from data .env file: ${dataEnvPathGlobal}`);
|
||||
}
|
||||
// --- 结束环境变量的早期加载 ---
|
||||
|
||||
import express = require('express');
|
||||
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// import fs from 'fs'; // Moved up
|
||||
// import path from 'path'; // Moved up
|
||||
import crypto from 'crypto';
|
||||
import dotenv from 'dotenv';
|
||||
// import dotenv from 'dotenv'; // Moved up
|
||||
import session from 'express-session';
|
||||
import sessionFileStore from 'session-file-store';
|
||||
import { getDbInstance } from './database/connection';
|
||||
@@ -54,38 +84,16 @@ process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
||||
|
||||
|
||||
const initializeEnvironment = async () => {
|
||||
// 1. 加载根目录的 .env 文件 (定义部署模式等)
|
||||
// 注意: __dirname 在 dist/src 中,所以需要回退三级到项目根目录
|
||||
const projectRootEnvPath = path.resolve(__dirname, '../../../.env');
|
||||
const rootConfigResult = dotenv.config({ path: projectRootEnvPath });
|
||||
// Use type assertion for error code checking
|
||||
if (rootConfigResult.error && (rootConfigResult.error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
// 只在文件存在但无法加载时发出警告
|
||||
console.warn(`[ENV Init] Warning: Could not load root .env file from ${projectRootEnvPath}. Error: ${rootConfigResult.error.message}`);
|
||||
} else if (!rootConfigResult.error) {
|
||||
console.log(`[ENV Init] Loaded environment variables from root .env file: ${projectRootEnvPath}`);
|
||||
} else {
|
||||
console.log(`[ENV Init] Root .env file not found at ${projectRootEnvPath}, proceeding without it (expected in non-local deployments where env vars are injected).`);
|
||||
}
|
||||
// Env files (root and data/.env) are now loaded at the very top of the file.
|
||||
// This function will now focus on generating keys if they are missing
|
||||
// and setting defaults for GUACD variables.
|
||||
|
||||
// 2. 加载 data/.env 文件 (定义密钥等)
|
||||
// 注意: 这个路径是相对于编译后的 dist/src/index.js
|
||||
const dataEnvPath = path.resolve(__dirname, '../data/.env');
|
||||
// Use the globally defined path for data .env
|
||||
const dataEnvPath = dataEnvPathGlobal; // Use the path defined at the top
|
||||
let keysGenerated = false;
|
||||
let keysToAppend = '';
|
||||
|
||||
// dotenv.config 默认不会覆盖已存在的 process.env 变量
|
||||
// 这意味着如果根 .env 和 data/.env 定义了相同的变量,先加载的(根 .env)的值会优先
|
||||
const dataConfigResult = dotenv.config({ path: dataEnvPath });
|
||||
// Use type assertion for error code checking
|
||||
if (dataConfigResult.error && (dataConfigResult.error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
// 只在文件存在但无法加载时发出警告,文件不存在是正常情况
|
||||
console.warn(`[ENV Init] Warning: Could not load data .env file from ${dataEnvPath}. Error: ${dataConfigResult.error.message}`);
|
||||
} else if (!dataConfigResult.error) {
|
||||
console.log(`[ENV Init] Loaded environment variables from data .env file: ${dataEnvPath}`);
|
||||
}
|
||||
|
||||
// 2. 检查 ENCRYPTION_KEY
|
||||
// 检查 ENCRYPTION_KEY (process.env should be populated by early loading)
|
||||
if (!process.env.ENCRYPTION_KEY) {
|
||||
console.log('[ENV Init] ENCRYPTION_KEY 未设置,正在生成...');
|
||||
const newEncryptionKey = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import axios from 'axios';
|
||||
import { ConnectionWithTags } from '../types/connection.types';
|
||||
|
||||
// RDP 后端服务的 Base URL,从环境变量读取,提供默认值
|
||||
const RDP_BACKEND_API_BASE = process.env.RDP_BACKEND_API_BASE || 'http://nexus-rdp:9090'; // 假设 RDP 服务名为 nexus-rdp
|
||||
// RDP 后端服务的 Base URL
|
||||
const RDP_BACKEND_API_BASE = process.env.DEPLOYMENT_MODE === 'local'
|
||||
? (process.env.RDP_BACKEND_API_BASE_LOCAL || 'http://localhost:9090')
|
||||
: (process.env.RDP_BACKEND_API_BASE_DOCKER || 'http://nexus-rdp:9090');
|
||||
|
||||
// VNC 后端服务的 Base URL,从环境变量读取,提供默认值
|
||||
const VNC_BACKEND_API_BASE = process.env.VNC_BACKEND_API_BASE || 'http://nexus-vnc:9091'; // 假设 VNC 服务名为 nexus-vnc,端口为 9091
|
||||
console.log(`[GuacamoleService] DEPLOYMENT_MODE: ${process.env.DEPLOYMENT_MODE}`);
|
||||
console.log(`[GuacamoleService] RDP_BACKEND_API_BASE_LOCAL: ${process.env.RDP_BACKEND_API_BASE_LOCAL}`);
|
||||
console.log(`[GuacamoleService] RDP_BACKEND_API_BASE_DOCKER: ${process.env.RDP_BACKEND_API_BASE_DOCKER}`);
|
||||
console.log(`[GuacamoleService] Using RDP Backend API Base: ${RDP_BACKEND_API_BASE}`);
|
||||
|
||||
|
||||
// VNC 后端服务的 Base URL
|
||||
const VNC_BACKEND_API_BASE = process.env.DEPLOYMENT_MODE === 'local'
|
||||
? (process.env.VNC_BACKEND_API_BASE_LOCAL || 'http://localhost:9091')
|
||||
: (process.env.VNC_BACKEND_API_BASE_DOCKER || 'http://nexus-vnc:9091');
|
||||
|
||||
console.log(`[GuacamoleService] VNC_BACKEND_API_BASE_LOCAL: ${process.env.VNC_BACKEND_API_BASE_LOCAL}`);
|
||||
console.log(`[GuacamoleService] VNC_BACKEND_API_BASE_DOCKER: ${process.env.VNC_BACKEND_API_BASE_DOCKER}`);
|
||||
console.log(`[GuacamoleService] Using VNC Backend API Base: ${VNC_BACKEND_API_BASE}`);
|
||||
|
||||
/**
|
||||
* 从 RDP 后端服务获取 Guacamole 令牌
|
||||
|
||||
@@ -20,6 +20,8 @@ import StyleCustomizer from './components/StyleCustomizer.vue';
|
||||
import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue';
|
||||
// +++ 导入 RDP 模态框组件 +++
|
||||
import RemoteDesktopModal from './components/RemoteDesktopModal.vue';
|
||||
// +++ 导入 VNC 模态框组件 +++
|
||||
import VncModal from './components/VncModal.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
@@ -33,7 +35,7 @@ const { showPopupFileEditorBoolean } = storeToRefs(settingsStore);
|
||||
const { isStyleCustomizerVisible } = storeToRefs(appearanceStore);
|
||||
const { isLayoutVisible, isHeaderVisible } = storeToRefs(layoutStore); // 添加 isHeaderVisible
|
||||
const { isConfiguratorVisible: isFocusSwitcherVisible } = storeToRefs(focusSwitcherStore);
|
||||
const { isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 状态 +++
|
||||
const { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 和 VNC 状态 +++
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind); // +++ Initialize Breakpoints +++
|
||||
const isMobile = breakpoints.smaller('md'); // +++ Define isMobile +++
|
||||
|
||||
@@ -319,6 +321,13 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => {
|
||||
@close="sessionStore.closeRdpModal()"
|
||||
/>
|
||||
|
||||
<!-- +++ 条件渲染 VNC 模态框 +++ -->
|
||||
<VncModal
|
||||
v-if="isVncModalOpen"
|
||||
:connection="vncConnectionInfo"
|
||||
@close="sessionStore.closeVncModal()"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -287,10 +287,7 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | '
|
||||
return {
|
||||
...baseProps,
|
||||
// Event forwarding
|
||||
onConnectRequest: (id: number) => {
|
||||
console.log(`[LayoutRenderer Sidebar] Forwarding 'connect-request' for ID: ${id}`);
|
||||
emit('connect-request', id);
|
||||
},
|
||||
onConnectRequest: (id: number) => emit('connect-request', id),
|
||||
onOpenNewSession: (id: number) => {
|
||||
console.log(`[LayoutRenderer Sidebar] Forwarding 'open-new-session' for ID: ${id}`);
|
||||
emit('open-new-session', id);
|
||||
|
||||
@@ -432,7 +432,6 @@ watchEffect(() => {
|
||||
desiredModalHeight.value = finalHeight;
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// 初始尺寸加载现在由 watchEffect 处理
|
||||
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed, watchEffect } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useSettingsStore } from '../stores/settings.store';
|
||||
import { useConnectionsStore } from '../stores/connections.store';
|
||||
// @ts-ignore - guacamole-common-js 缺少官方类型定义
|
||||
import Guacamole from 'guacamole-common-js';
|
||||
import type { ConnectionInfo } from '../stores/connections.store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionInfo | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
let saveWidthTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let saveHeightTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
const DEBOUNCE_DELAY = 500; // ms
|
||||
|
||||
const vncDisplayRef = ref<HTMLDivElement | null>(null);
|
||||
const vncContainerRef = ref<HTMLDivElement | null>(null);
|
||||
const guacClient = ref<any | null>(null);
|
||||
const connectionStatus = ref<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected');
|
||||
const statusMessage = ref('');
|
||||
const keyboard = ref<any | null>(null);
|
||||
const mouse = ref<any | null>(null);
|
||||
const desiredModalWidth = ref(1024);
|
||||
const desiredModalHeight = ref(768);
|
||||
const isKeyboardDisabledForInput = ref(false);
|
||||
|
||||
const MIN_MODAL_WIDTH = 800;
|
||||
const MIN_MODAL_HEIGHT = 600;
|
||||
|
||||
let vncWsBaseUrl: string;
|
||||
const VNC_WS_PORT_FROM_ENV = import.meta.env.VITE_VNC_WS_PORT || '8082';
|
||||
|
||||
if (window.location.hostname === 'localhost') {
|
||||
vncWsBaseUrl = `ws://localhost:${VNC_WS_PORT_FROM_ENV}`;
|
||||
console.log(`[VncModal] Using localhost VNC WebSocket base URL: ${vncWsBaseUrl}`);
|
||||
} else {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
vncWsBaseUrl = `${wsProtocol}//${window.location.hostname}:${VNC_WS_PORT_FROM_ENV}`;
|
||||
console.log(`[VncModal] Using production VNC WebSocket base URL: ${vncWsBaseUrl}`);
|
||||
}
|
||||
|
||||
const handleConnection = async () => {
|
||||
if (!props.connection || !vncDisplayRef.value) {
|
||||
statusMessage.value = t('remoteDesktopModal.errors.missingInfo');
|
||||
connectionStatus.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
while (vncDisplayRef.value.firstChild) {
|
||||
vncDisplayRef.value.removeChild(vncDisplayRef.value.firstChild);
|
||||
}
|
||||
disconnectGuacamole();
|
||||
|
||||
connectionStatus.value = 'connecting';
|
||||
statusMessage.value = t('remoteDesktopModal.status.fetchingToken');
|
||||
|
||||
try {
|
||||
const connectionsStore = useConnectionsStore();
|
||||
const token = await connectionsStore.getVncSessionToken(props.connection.id);
|
||||
if (!token) {
|
||||
throw new Error('VNC Token not found from store action');
|
||||
}
|
||||
statusMessage.value = t('remoteDesktopModal.status.connectingWs');
|
||||
const tunnelUrl = `${vncWsBaseUrl}?token=${encodeURIComponent(token)}`;
|
||||
console.log(`[VncModal] Connecting to VNC tunnel: ${tunnelUrl}`);
|
||||
|
||||
// @ts-ignore
|
||||
const tunnel = new Guacamole.WebSocketTunnel(tunnelUrl);
|
||||
|
||||
tunnel.onerror = (status: any) => {
|
||||
const errorMessage = status.message || 'Unknown tunnel error';
|
||||
const errorCode = status.code || 'N/A';
|
||||
statusMessage.value = `${t('remoteDesktopModal.errors.tunnelError')} (${errorCode}): ${errorMessage}`;
|
||||
connectionStatus.value = 'error';
|
||||
disconnectGuacamole();
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
guacClient.value = new Guacamole.Client(tunnel);
|
||||
guacClient.value.keepAliveFrequency = 3000;
|
||||
|
||||
vncDisplayRef.value.appendChild(guacClient.value.getDisplay().getElement());
|
||||
|
||||
guacClient.value.onstatechange = (state: number) => {
|
||||
let currentStatus = '';
|
||||
let i18nKeyPart = 'unknownState';
|
||||
|
||||
switch (state) {
|
||||
case 0: i18nKeyPart = 'idle'; currentStatus = 'disconnected'; break;
|
||||
case 1: i18nKeyPart = 'connectingVnc'; currentStatus = 'connecting'; break;
|
||||
case 2: i18nKeyPart = 'waiting'; currentStatus = 'connecting'; break;
|
||||
case 3:
|
||||
i18nKeyPart = 'connected';
|
||||
currentStatus = 'connected';
|
||||
setupInputListeners();
|
||||
nextTick(() => {
|
||||
const displayEl = guacClient.value?.getDisplay()?.getElement();
|
||||
if (displayEl && typeof displayEl.focus === 'function') {
|
||||
displayEl.focus();
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
nextTick(() => {
|
||||
if (vncDisplayRef.value && guacClient.value) {
|
||||
const canvases = vncDisplayRef.value.querySelectorAll('canvas');
|
||||
canvases.forEach((canvas) => { canvas.style.zIndex = '999'; });
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
break;
|
||||
case 4: i18nKeyPart = 'disconnecting'; currentStatus = 'disconnected'; break;
|
||||
case 5: i18nKeyPart = 'disconnected'; currentStatus = 'disconnected'; break;
|
||||
}
|
||||
statusMessage.value = t(`remoteDesktopModal.status.${i18nKeyPart}`, { state });
|
||||
if (currentStatus) connectionStatus.value = currentStatus as 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||
};
|
||||
|
||||
guacClient.value.onerror = (status: any) => {
|
||||
const errorMessage = status.message || 'Unknown client error';
|
||||
statusMessage.value = `${t('remoteDesktopModal.errors.clientError')}: ${errorMessage}`;
|
||||
connectionStatus.value = 'error';
|
||||
disconnectGuacamole();
|
||||
};
|
||||
|
||||
guacClient.value.connect('');
|
||||
|
||||
} catch (error: any) {
|
||||
statusMessage.value = `${t('remoteDesktopModal.errors.connectionFailed')}: ${error.response?.data?.message || error.message || String(error)}`;
|
||||
connectionStatus.value = 'error';
|
||||
disconnectGuacamole();
|
||||
}
|
||||
};
|
||||
|
||||
const setupInputListeners = () => {
|
||||
if (!guacClient.value || !vncDisplayRef.value) return;
|
||||
try {
|
||||
const displayEl = guacClient.value.getDisplay().getElement() as HTMLElement;
|
||||
displayEl.tabIndex = 0;
|
||||
|
||||
const handleVncDisplayClick = () => {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement && (activeElement.id === 'modal-width' || activeElement.id === 'modal-height')) {
|
||||
activeElement.blur();
|
||||
}
|
||||
};
|
||||
displayEl.addEventListener('click', handleVncDisplayClick);
|
||||
|
||||
const handleMouseEnter = () => { if (displayEl) displayEl.style.cursor = 'none'; };
|
||||
const handleMouseLeave = () => { if (displayEl) displayEl.style.cursor = 'default'; };
|
||||
displayEl.addEventListener('mouseenter', handleMouseEnter);
|
||||
displayEl.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
// @ts-ignore
|
||||
mouse.value = new Guacamole.Mouse(displayEl);
|
||||
const display = guacClient.value.getDisplay();
|
||||
display.showCursor(true);
|
||||
|
||||
const cursorLayer = display.getCursorLayer();
|
||||
if (cursorLayer) {
|
||||
const cursorElement = cursorLayer.getElement();
|
||||
if (cursorElement) {
|
||||
cursorElement.style.zIndex = '1000';
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
mouse.value.onmousedown = mouse.value.onmouseup = mouse.value.onmousemove = (mouseState: any) => {
|
||||
if (guacClient.value) {
|
||||
guacClient.value.sendMouseState(mouseState);
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
keyboard.value = new Guacamole.Keyboard(displayEl);
|
||||
|
||||
keyboard.value.onkeydown = (keysym: number) => {
|
||||
if (guacClient.value && !isKeyboardDisabledForInput.value) {
|
||||
guacClient.value.sendKeyEvent(1, keysym);
|
||||
}
|
||||
};
|
||||
keyboard.value.onkeyup = (keysym: number) => {
|
||||
if (guacClient.value && !isKeyboardDisabledForInput.value) {
|
||||
guacClient.value.sendKeyEvent(0, keysym);
|
||||
}
|
||||
};
|
||||
|
||||
} catch (inputError) {
|
||||
console.error("Error setting up VNC input listeners:", inputError);
|
||||
statusMessage.value = t('remoteDesktopModal.errors.inputError');
|
||||
}
|
||||
};
|
||||
|
||||
const removeInputListeners = () => {
|
||||
if (guacClient.value) {
|
||||
try {
|
||||
const displayEl = guacClient.value.getDisplay()?.getElement();
|
||||
if (displayEl) {
|
||||
displayEl.style.cursor = 'default';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Could not reset cursor on VNC display element:", e);
|
||||
}
|
||||
}
|
||||
if (keyboard.value) {
|
||||
keyboard.value.onkeydown = null;
|
||||
keyboard.value.onkeyup = null;
|
||||
keyboard.value = null;
|
||||
}
|
||||
if (mouse.value) {
|
||||
mouse.value.onmousedown = null;
|
||||
mouse.value.onmouseup = null;
|
||||
mouse.value.onmousemove = null;
|
||||
mouse.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const disableVncKeyboard = () => {
|
||||
isKeyboardDisabledForInput.value = true;
|
||||
};
|
||||
|
||||
const enableVncKeyboard = () => {
|
||||
isKeyboardDisabledForInput.value = false;
|
||||
nextTick(() => {
|
||||
const displayEl = guacClient.value?.getDisplay()?.getElement();
|
||||
if (displayEl && typeof displayEl.focus === 'function') {
|
||||
displayEl.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const disconnectGuacamole = () => {
|
||||
removeInputListeners();
|
||||
isKeyboardDisabledForInput.value = false;
|
||||
if (guacClient.value) {
|
||||
guacClient.value.disconnect();
|
||||
guacClient.value = null;
|
||||
}
|
||||
if (vncDisplayRef.value) {
|
||||
while (vncDisplayRef.value.firstChild) {
|
||||
vncDisplayRef.value.removeChild(vncDisplayRef.value.firstChild);
|
||||
}
|
||||
}
|
||||
if (connectionStatus.value !== 'error') {
|
||||
connectionStatus.value = 'disconnected';
|
||||
statusMessage.value = t('remoteDesktopModal.status.disconnected');
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
disconnectGuacamole();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
watch(desiredModalWidth, (newWidth, oldWidth) => {
|
||||
if (newWidth === oldWidth) return;
|
||||
const validatedWidth = Math.max(MIN_MODAL_WIDTH, Number(newWidth) || MIN_MODAL_WIDTH);
|
||||
if (saveWidthTimeout) clearTimeout(saveWidthTimeout);
|
||||
saveWidthTimeout = setTimeout(() => {
|
||||
if (String(validatedWidth) !== settingsStore.settings.vncModalWidth) {
|
||||
settingsStore.updateSetting('vncModalWidth', String(validatedWidth));
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
|
||||
watch(desiredModalHeight, (newHeight, oldHeight) => {
|
||||
if (newHeight === oldHeight) return;
|
||||
const validatedHeight = Math.max(MIN_MODAL_HEIGHT, Number(newHeight) || MIN_MODAL_HEIGHT);
|
||||
if (saveHeightTimeout) clearTimeout(saveHeightTimeout);
|
||||
saveHeightTimeout = setTimeout(() => {
|
||||
if (String(validatedHeight) !== settingsStore.settings.vncModalHeight) {
|
||||
settingsStore.updateSetting('vncModalHeight', String(validatedHeight));
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const storeWidth = settingsStore.settings.vncModalWidth;
|
||||
const storeHeight = settingsStore.settings.vncModalHeight;
|
||||
console.log(`[VncModal] From store - Width: ${storeWidth}, Height: ${storeHeight}`);
|
||||
|
||||
const initialWidth = storeWidth ? parseInt(storeWidth, 10) : desiredModalWidth.value;
|
||||
const initialHeight = storeHeight ? parseInt(storeHeight, 10) : desiredModalHeight.value;
|
||||
|
||||
const finalWidth = Math.max(MIN_MODAL_WIDTH, isNaN(initialWidth) ? MIN_MODAL_WIDTH : initialWidth);
|
||||
const finalHeight = Math.max(MIN_MODAL_HEIGHT, isNaN(initialHeight) ? MIN_MODAL_HEIGHT : initialHeight);
|
||||
console.log(`[VncModal] Applied - Width: ${finalWidth}, Height: ${finalHeight}`);
|
||||
desiredModalWidth.value = finalWidth;
|
||||
desiredModalHeight.value = finalHeight;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.connection) {
|
||||
nextTick(async () => {
|
||||
await handleConnection();
|
||||
});
|
||||
} else {
|
||||
statusMessage.value = t('remoteDesktopModal.errors.noConnection');
|
||||
connectionStatus.value = 'error';
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectGuacamole();
|
||||
});
|
||||
|
||||
watch(() => props.connection, (newConnection, oldConnection) => {
|
||||
if (newConnection && newConnection.id !== oldConnection?.id) {
|
||||
nextTick(async () => {
|
||||
await handleConnection();
|
||||
});
|
||||
} else if (!newConnection) {
|
||||
disconnectGuacamole();
|
||||
statusMessage.value = t('remoteDesktopModal.errors.noConnection');
|
||||
connectionStatus.value = 'error';
|
||||
}
|
||||
});
|
||||
|
||||
const computedModalStyle = computed(() => {
|
||||
const actualWidth = Math.max(MIN_MODAL_WIDTH, desiredModalWidth.value);
|
||||
const actualHeight = Math.max(MIN_MODAL_HEIGHT, desiredModalHeight.value);
|
||||
return {
|
||||
width: `${actualWidth}px`,
|
||||
height: `${actualHeight}px`,
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-overlay p-4">
|
||||
<div
|
||||
:style="computedModalStyle"
|
||||
class="bg-background text-foreground rounded-lg shadow-xl flex flex-col overflow-hidden border border-border"
|
||||
>
|
||||
<div class="flex items-center justify-between p-3 border-b border-border flex-shrink-0">
|
||||
<h3 class="text-base font-semibold truncate">
|
||||
<i class="fas fa-desktop mr-2 text-text-secondary"></i>
|
||||
{{ t('vncModal.title') }} - {{ props.connection?.name || props.connection?.host || t('remoteDesktopModal.titlePlaceholder') }}
|
||||
</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs px-2 py-0.5 rounded"
|
||||
:class="{
|
||||
'bg-yellow-200 text-yellow-800': connectionStatus === 'connecting',
|
||||
'bg-green-200 text-green-800': connectionStatus === 'connected',
|
||||
'bg-red-200 text-red-800': connectionStatus === 'error',
|
||||
'bg-gray-200 text-gray-800': connectionStatus === 'disconnected'
|
||||
}">
|
||||
{{ t('remoteDesktopModal.status.' + connectionStatus) }}
|
||||
</span>
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
||||
:title="t('common.close')"
|
||||
>
|
||||
<i class="fas fa-times fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="vncContainerRef" class="relative bg-black overflow-hidden flex-1">
|
||||
<div ref="vncDisplayRef" class="vnc-display-container w-full h-full">
|
||||
</div>
|
||||
<div v-if="connectionStatus === 'connecting' || connectionStatus === 'error'"
|
||||
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-75 text-white p-4 z-10">
|
||||
<div class="text-center">
|
||||
<i v-if="connectionStatus === 'connecting'" class="fas fa-spinner fa-spin fa-2x mb-3"></i>
|
||||
<i v-else class="fas fa-exclamation-triangle fa-2x mb-3 text-red-400"></i>
|
||||
<p class="text-sm">{{ statusMessage }}</p>
|
||||
<button v-if="connectionStatus === 'error'"
|
||||
@click="() => handleConnection()"
|
||||
class="mt-4 px-3 py-1 bg-primary text-white rounded text-xs hover:bg-primary-dark">
|
||||
{{ t('common.retry') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-2 border-t border-border flex-shrink-0 text-xs text-text-secondary bg-header flex items-center justify-end">
|
||||
<div class="flex items-center space-x-2 flex-wrap gap-y-1">
|
||||
<label for="modal-width" class="text-xs ml-2">{{ t('common.width') }}:</label>
|
||||
<input
|
||||
id="modal-width"
|
||||
type="number"
|
||||
v-model.number="desiredModalWidth"
|
||||
step="10"
|
||||
class="w-16 px-1 py-0.5 text-xs border border-border rounded bg-input text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
@focus="disableVncKeyboard"
|
||||
@blur="enableVncKeyboard"
|
||||
/>
|
||||
<label for="modal-height" class="text-xs">{{ t('common.height') }}:</label>
|
||||
<input
|
||||
id="modal-height"
|
||||
type="number"
|
||||
v-model.number="desiredModalHeight"
|
||||
step="10"
|
||||
class="w-16 px-1 py-0.5 text-xs border border-border rounded bg-input text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
@focus="disableVncKeyboard"
|
||||
@blur="enableVncKeyboard"
|
||||
/>
|
||||
<button
|
||||
@click="handleConnection"
|
||||
:disabled="connectionStatus === 'connecting'"
|
||||
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 disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out"
|
||||
:title="t('remoteDesktopModal.reconnectTooltip')"
|
||||
>
|
||||
{{ t('common.reconnect') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.vnc-display-container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vnc-display-container :deep(div) {
|
||||
}
|
||||
|
||||
.vnc-display-container :deep(canvas) {
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
@@ -306,15 +306,8 @@ const handleConnect = (connectionId: number, event?: MouseEvent | KeyboardEvent)
|
||||
|
||||
closeContextMenu(); // 关闭右键菜单
|
||||
|
||||
if (connection.type === 'RDP') {
|
||||
console.log(`[WkspConnList] RDP connection clicked (ID: ${connectionId}). Calling sessionStore.openRdpModal.`);
|
||||
// --- 修改:调用 Store Action ---
|
||||
sessionStore.openRdpModal(connection);
|
||||
} else {
|
||||
console.log(`[WkspConnList] Non-RDP connection clicked (ID: ${connectionId}, Type: ${connection.type}). Emitting connect-request.`);
|
||||
// 对于非 RDP 连接,保持原有逻辑,发出事件给父组件处理
|
||||
emit('connect-request', connectionId);
|
||||
}
|
||||
// 统一发出 connect-request 事件,让 sessionStore.handleConnectRequest 处理模态框和会话
|
||||
emit('connect-request', connectionId);
|
||||
};
|
||||
|
||||
// --- 移除 closeRdpModal 方法 ---
|
||||
|
||||
@@ -291,7 +291,7 @@ export const useConnectionsStore = defineStore('connections', {
|
||||
// this.error = null;
|
||||
try {
|
||||
// 调用后端 API GET /connections/:id/vnc-session
|
||||
const response = await apiClient.get<{ token: string }>(`/connections/${connectionId}/vnc-session`);
|
||||
const response = await apiClient.post<{ token: string }>(`/connections/${connectionId}/vnc-session`);
|
||||
return response.data.token;
|
||||
} catch (err: any) {
|
||||
console.error(`获取 VNC 会话令牌失败 (连接 ID: ${connectionId}):`, err);
|
||||
|
||||
@@ -134,6 +134,10 @@ export const useSessionStore = defineStore('session', () => {
|
||||
const isRdpModalOpen = ref(false);
|
||||
const rdpConnectionInfo = ref<ConnectionInfo | null>(null);
|
||||
|
||||
// --- VNC Modal State ---
|
||||
const isVncModalOpen = ref(false);
|
||||
const vncConnectionInfo = ref<ConnectionInfo | null>(null);
|
||||
|
||||
// --- Getters ---
|
||||
const sessionTabs = computed(() => {
|
||||
return Array.from(sessions.value.values()).map(session => ({
|
||||
@@ -409,11 +413,14 @@ export const useSessionStore = defineStore('session', () => {
|
||||
* - 否则,打开一个新的会话标签页并导航到 Workspace。
|
||||
*/
|
||||
const handleConnectRequest = (connection: ConnectionInfo) => {
|
||||
console.log(`[SessionStore] handleConnectRequest called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`);
|
||||
// console.log(`[SessionStore] handleConnectRequest called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`); // 保留原始日志或移除
|
||||
|
||||
if (connection.type === 'RDP') {
|
||||
// RDP: 直接打开模态框
|
||||
// RDP: 直接打开 RDP 模态框
|
||||
openRdpModal(connection);
|
||||
} else if (connection.type === 'VNC') {
|
||||
// VNC: 直接打开 VNC 模态框
|
||||
openVncModal(connection);
|
||||
} else {
|
||||
// 非 RDP (e.g., SSH): 处理会话和导航
|
||||
const connIdStr = String(connection.id);
|
||||
@@ -785,17 +792,30 @@ export const useSessionStore = defineStore('session', () => {
|
||||
|
||||
// --- RDP Modal Actions ---
|
||||
const openRdpModal = (connection: ConnectionInfo) => {
|
||||
console.log(`[SessionStore] Opening RDP modal for connection: ${connection.name} (ID: ${connection.id})`);
|
||||
// console.log(`[SessionStore] Opening RDP modal for connection: ${connection.name} (ID: ${connection.id})`); // 保留原始日志或移除
|
||||
rdpConnectionInfo.value = connection;
|
||||
isRdpModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeRdpModal = () => {
|
||||
console.log('[SessionStore] Closing RDP modal.');
|
||||
// console.log('[SessionStore] Closing RDP modal.'); // 保留原始日志或移除
|
||||
isRdpModalOpen.value = false;
|
||||
rdpConnectionInfo.value = null; // 清除连接信息
|
||||
};
|
||||
|
||||
// --- VNC Modal Actions ---
|
||||
const openVncModal = (connection: ConnectionInfo) => {
|
||||
// console.log(`[SessionStore] Opening VNC modal for connection: ${connection.name} (ID: ${connection.id})`); // 保留原始日志或移除
|
||||
vncConnectionInfo.value = connection;
|
||||
isVncModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeVncModal = () => {
|
||||
// console.log('[SessionStore] Closing VNC modal.'); // 保留原始日志或移除
|
||||
isVncModalOpen.value = false;
|
||||
vncConnectionInfo.value = null; // 清除连接信息
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新指定会话的命令输入框内容
|
||||
*/
|
||||
@@ -815,6 +835,8 @@ export const useSessionStore = defineStore('session', () => {
|
||||
activeSessionId,
|
||||
isRdpModalOpen, // 导出 RDP 模态框状态
|
||||
rdpConnectionInfo, // 导出 RDP 连接信息
|
||||
isVncModalOpen, // 导出 VNC 模态框状态
|
||||
vncConnectionInfo, // 导出 VNC 连接信息
|
||||
// Getters
|
||||
sessionTabs,
|
||||
sessionTabsWithStatus, // 导出新的 getter
|
||||
@@ -841,6 +863,8 @@ export const useSessionStore = defineStore('session', () => {
|
||||
// --- RDP Modal Actions ---
|
||||
openRdpModal, // 导出打开 RDP 模态框 Action
|
||||
closeRdpModal, // 导出关闭 RDP 模态框 Action
|
||||
openVncModal, // 导出打开 VNC 模态框 Action
|
||||
closeVncModal, // 导出关闭 VNC 模态框 Action
|
||||
// --- 命令输入框 Action ---
|
||||
updateSessionCommandInput, // 导出更新命令输入 Action
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import TerminalTabBar from '../components/TerminalTabBar.vue';
|
||||
import LayoutRenderer from '../components/LayoutRenderer.vue';
|
||||
import LayoutConfigurator from '../components/LayoutConfigurator.vue';
|
||||
import RemoteDesktopModal from '../components/RemoteDesktopModal.vue';
|
||||
import VncModal from '../components/VncModal.vue'; // +++ 引入 VncModal 组件 +++
|
||||
import Terminal from '../components/Terminal.vue'; // +++ 引入 Terminal 组件 +++
|
||||
import CommandInputBar from '../components/CommandInputBar.vue'; // +++ 引入 CommandInputBar 组件 +++
|
||||
import VirtualKeyboard from '../components/VirtualKeyboard.vue'; // +++ 引入 VirtualKeyboard 组件 +++
|
||||
@@ -33,7 +34,7 @@ const breakpoints = useBreakpoints(breakpointsTailwind); // +++ 初始化 Breakp
|
||||
const isMobile = breakpoints.smaller('md'); // +++ 定义 isMobile (小于 md 断点) +++
|
||||
|
||||
// --- 从 Store 获取响应式状态和 Getters ---
|
||||
const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 状态
|
||||
const { sessionTabsWithStatus, activeSessionId, activeSession, isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); // 使用 storeToRefs 获取 RDP 和 VNC 状态
|
||||
const { shareFileEditorTabsBoolean, layoutLockedBoolean } = storeToRefs(settingsStore); // +++ Add layoutLockedBoolean +++
|
||||
const { orderedTabs: globalEditorTabs, activeTabId: globalActiveEditorTabId } = storeToRefs(fileEditorStore);
|
||||
const { layoutTree } = storeToRefs(layoutStore); // 只获取布局树
|
||||
@@ -448,14 +449,13 @@ const handleCloseEditorTab = (tabId: string) => {
|
||||
|
||||
// --- 连接列表操作处理 (用于 WorkspaceConnectionList) ---
|
||||
const handleConnectRequest = (id: number) => {
|
||||
console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`);
|
||||
// +++ 修复:传递 ConnectionInfo 而不是 ID +++
|
||||
const connectionInfo = connectionsStore.connections.find(c => c.id === id);
|
||||
if (connectionInfo) {
|
||||
sessionStore.handleConnectRequest(connectionInfo);
|
||||
} else {
|
||||
console.error(`[WorkspaceView] handleConnectRequest: 未找到 ID 为 ${id} 的连接信息。`);
|
||||
}
|
||||
const connectionInfo = connectionsStore.connections.find(c => c.id === id);
|
||||
// console.log(`[WorkspaceView] Received 'connect-request' event for ID: ${id}`); // 保留原始日志或移除
|
||||
if (connectionInfo) {
|
||||
sessionStore.handleConnectRequest(connectionInfo);
|
||||
} else {
|
||||
console.error(`[WorkspaceView] handleConnectRequest: Connection info not found for ID ${id}.`); // 保留错误日志
|
||||
}
|
||||
};
|
||||
const handleOpenNewSession = (id: number) => {
|
||||
console.log(`[WorkspaceView] Received 'open-new-session' event for ID: ${id}`);
|
||||
@@ -650,11 +650,8 @@ const toggleVirtualKeyboard = () => {
|
||||
@close="handleCloseLayoutConfigurator"
|
||||
/>
|
||||
|
||||
<RemoteDesktopModal
|
||||
v-if="isRdpModalOpen"
|
||||
:connection="rdpConnectionInfo"
|
||||
@close="sessionStore.closeRdpModal()"
|
||||
/>
|
||||
<!-- RDP Modal is now rendered in App.vue -->
|
||||
<!-- VNC Modal is now rendered in App.vue -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user