update
This commit is contained in:
@@ -5,24 +5,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch, defineExpose } from 'vue';
|
||||
import * as monaco from 'monaco-editor';
|
||||
// import { useAppearanceStore } from '../stores/appearance.store'; // <-- 移除 Store 导入
|
||||
// import { storeToRefs } from 'pinia'; // <-- 移除 storeToRefs 导入
|
||||
|
||||
// Props for the component (will be expanded later)
|
||||
const localFontSize = ref(14); // <-- 添加本地字体大小状态,默认 14
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { // Use modelValue for v-model support
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'plaintext', // Default language
|
||||
default: 'plaintext',
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'vs-dark', // Default theme (can be 'vs', 'vs-dark', 'hc-black')
|
||||
default: 'vs-dark',
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
@@ -30,20 +27,12 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
// Emits for v-model update and save request
|
||||
const emit = defineEmits(['update:modelValue', 'request-save']);
|
||||
|
||||
const editorContainer = ref<HTMLElement | null>(null);
|
||||
let editorInstance: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||
|
||||
// --- 移除 Appearance Store 相关代码 ---
|
||||
// const appearanceStore = useAppearanceStore();
|
||||
// const { currentEditorFontSize } = storeToRefs(appearanceStore);
|
||||
|
||||
// --- 移除防抖函数和相关调用 ---
|
||||
// let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
// const debounce = (func: Function, delay: number) => { ... };
|
||||
// const debouncedSetEditorFontSize = debounce((size: number) => { ... });
|
||||
|
||||
onMounted(() => {
|
||||
if (editorContainer.value) {
|
||||
@@ -51,16 +40,15 @@ onMounted(() => {
|
||||
value: props.modelValue,
|
||||
language: props.language,
|
||||
theme: props.theme,
|
||||
fontSize: localFontSize.value, // <-- 使用本地字体大小
|
||||
automaticLayout: true, // Auto resize editor on container resize
|
||||
fontSize: localFontSize.value,
|
||||
automaticLayout: true,
|
||||
readOnly: props.readOnly,
|
||||
// Add more options as needed
|
||||
minimap: { enabled: true },
|
||||
lineNumbers: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
// Listen for content changes and emit update event for v-model
|
||||
|
||||
editorInstance.onDidChangeModelContent(() => {
|
||||
if (editorInstance) {
|
||||
const currentValue = editorInstance.getValue();
|
||||
@@ -70,24 +58,24 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Add Ctrl+S / Cmd+S keybinding for saving
|
||||
// Ctrl+S / Cmd+S
|
||||
editorInstance.addAction({
|
||||
id: 'save-file',
|
||||
label: 'Save File',
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
||||
],
|
||||
precondition: undefined, // Fix: Use undefined instead of null
|
||||
keybindingContext: undefined, // Fix: Use undefined instead of null
|
||||
contextMenuGroupId: 'navigation', // Optional: where to show in context menu
|
||||
contextMenuOrder: 1.5, // Optional: order in context menu
|
||||
precondition: undefined,
|
||||
keybindingContext: undefined,
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
run: () => {
|
||||
console.log('[MonacoEditor] Save action triggered (Ctrl+S / Cmd+S)');
|
||||
emit('request-save');
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for content changes and emit update event for v-model
|
||||
|
||||
editorInstance.onDidChangeModelContent(() => {
|
||||
if (editorInstance) {
|
||||
const currentValue = editorInstance.getValue();
|
||||
@@ -97,17 +85,17 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Add Ctrl+S / Cmd+S keybinding for saving
|
||||
//Ctrl+S / Cmd+S
|
||||
editorInstance.addAction({
|
||||
id: 'save-file',
|
||||
label: 'Save File',
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
||||
],
|
||||
precondition: undefined, // Fix: Use undefined instead of null
|
||||
keybindingContext: undefined, // Fix: Use undefined instead of null
|
||||
contextMenuGroupId: 'navigation', // Optional: where to show in context menu
|
||||
contextMenuOrder: 1.5, // Optional: order in context menu
|
||||
precondition: undefined,
|
||||
keybindingContext: undefined,
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
run: () => {
|
||||
console.log('[MonacoEditor] Save action triggered (Ctrl+S / Cmd+S)');
|
||||
emit('request-save');
|
||||
@@ -122,13 +110,13 @@ onMounted(() => {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
|
||||
// Calculate new font size immediately based on local state
|
||||
|
||||
const currentSize = localFontSize.value; // 使用本地状态
|
||||
let newSize: number;
|
||||
if (event.deltaY < 0) {
|
||||
newSize = Math.min(currentSize + 1, 40); // Increase size, max 40
|
||||
newSize = Math.min(currentSize + 1, 40);
|
||||
} else {
|
||||
newSize = Math.max(currentSize - 1, 8); // Decrease size, min 8
|
||||
newSize = Math.max(currentSize - 1, 8);
|
||||
}
|
||||
|
||||
// Update visual font size and local state immediately
|
||||
@@ -141,11 +129,11 @@ onMounted(() => {
|
||||
// debouncedSetEditorFontSize(newSize);
|
||||
}
|
||||
}
|
||||
}, { passive: false }); // passive: false allows preventDefault
|
||||
}, { passive: false });
|
||||
} else {
|
||||
console.error('[MonacoEditor] editorDomNode is null, cannot add wheel listener.');
|
||||
}
|
||||
// --- End of wheel event listener ---
|
||||
|
||||
|
||||
// --- 移除鼠标滚轮缩放功能 ---
|
||||
// const editorDomNode = editorInstance?.getDomNode();
|
||||
@@ -164,28 +152,28 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Update editor content if modelValue prop changes from outside
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (editorInstance && editorInstance.getValue() !== newValue) {
|
||||
editorInstance.setValue(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
// Update language if prop changes
|
||||
|
||||
watch(() => props.language, (newLanguage) => {
|
||||
if (editorInstance && editorInstance.getModel()) {
|
||||
monaco.editor.setModelLanguage(editorInstance.getModel()!, newLanguage);
|
||||
}
|
||||
});
|
||||
|
||||
// Update theme if prop changes
|
||||
|
||||
watch(() => props.theme, (newTheme) => {
|
||||
if (editorInstance) {
|
||||
monaco.editor.setTheme(newTheme);
|
||||
}
|
||||
});
|
||||
|
||||
// Update readOnly status if prop changes
|
||||
|
||||
watch(() => props.readOnly, (newReadOnly) => {
|
||||
if (editorInstance) {
|
||||
editorInstance.updateOptions({ readOnly: newReadOnly });
|
||||
@@ -194,8 +182,6 @@ watch(() => props.readOnly, (newReadOnly) => {
|
||||
|
||||
|
||||
// --- 移除对全局字体大小的监听 ---
|
||||
// watch(currentEditorFontSize, (newSize) => { ... });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (editorInstance) {
|
||||
editorInstance.dispose();
|
||||
@@ -203,12 +189,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Expose a method to get the current value if needed (optional)
|
||||
// defineExpose({
|
||||
// getValue: () => editorInstance?.getValue()
|
||||
// });
|
||||
|
||||
// Expose the focus method
|
||||
defineExpose({
|
||||
focus: () => editorInstance?.focus()
|
||||
});
|
||||
@@ -218,8 +198,8 @@ defineExpose({
|
||||
<style scoped>
|
||||
.monaco-editor-container {
|
||||
width: 100%;
|
||||
height: 100%; /* Ensure the container has height */
|
||||
min-height: 300px; /* Example minimum height */
|
||||
text-align: left; /* Ensure editor content aligns left */
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed, watchEffect } from 'vue'; // Import watchEffect
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed, watchEffect } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useSettingsStore } from '../stores/settings.store'; // Import settings store
|
||||
// @ts-ignore - guacamole-common-js lacks official types
|
||||
import { useSettingsStore } from '../stores/settings.store';
|
||||
// @ts-ignore - guacamole-common-js 缺少官方类型定义
|
||||
import Guacamole from 'guacamole-common-js';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { ConnectionInfo } from '../stores/connections.store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore(); // Instantiate settings store
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const props = defineProps<{
|
||||
connection: ConnectionInfo | null;
|
||||
@@ -21,19 +21,14 @@ let saveHeightTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
const DEBOUNCE_DELAY = 500; // ms
|
||||
|
||||
const rdpDisplayRef = ref<HTMLDivElement | null>(null);
|
||||
const rdpContainerRef = ref<HTMLDivElement | null>(null); // Added ref for the container
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rdpContainerRef = 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 inputWidth = ref(1024); // Removed, size determined by container
|
||||
// const inputHeight = ref(768); // Removed, size determined by container
|
||||
// const modalStyle = ref({}); // Replaced by computedModalStyle
|
||||
// const rdpContainerStyle = ref<{ height?: string }>({}); // Removed, size determined by flex-1
|
||||
const desiredModalWidth = ref(1064); // User sets the desired TOTAL modal width (1024 + 40 padding)
|
||||
const desiredModalHeight = ref(858); // User sets the desired TOTAL modal height (768 + chrome)
|
||||
const desiredModalWidth = ref(1064);
|
||||
const desiredModalHeight = ref(858);
|
||||
|
||||
const MIN_MODAL_WIDTH = 1024;
|
||||
const MIN_MODAL_HEIGHT = 768;
|
||||
@@ -45,16 +40,16 @@ const LOCAL_BACKEND_URL = 'ws://localhost:3001'
|
||||
// Determine WebSocket URL based on hostname
|
||||
if (window.location.hostname === 'localhost') {
|
||||
backendBaseUrl = LOCAL_BACKEND_URL;
|
||||
console.log(`[RDP Modal] Using localhost WebSocket Base URL: ${backendBaseUrl}`);
|
||||
console.log(`[RDP 模态框] 使用 localhost WebSocket 基础 URL: ${backendBaseUrl}`);
|
||||
} else {
|
||||
// Fallback: Construct URL based on current window location for production/other environments
|
||||
// 备选方案: 根据当前 window.location 为生产环境或其他环境构建 URL
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHostAndPort = window.location.host;
|
||||
backendBaseUrl = `${wsProtocol}//${wsHostAndPort}`;
|
||||
console.log(`[RDP Modal] Using production WebSocket Base URL from window.location: ${backendBaseUrl}`);
|
||||
console.log(`[RDP 模态框] 使用生产环境 WebSocket 基础 URL (来自 window.location): ${backendBaseUrl}`);
|
||||
}
|
||||
|
||||
const connectRdp = async () => { // Removed useInputValues parameter
|
||||
const connectRdp = async () => {
|
||||
if (!props.connection || !rdpDisplayRef.value) {
|
||||
statusMessage.value = t('remoteDesktopModal.errors.missingInfo');
|
||||
connectionStatus.value = 'error';
|
||||
@@ -80,29 +75,29 @@ const connectRdp = async () => { // Removed useInputValues parameter
|
||||
}
|
||||
statusMessage.value = t('remoteDesktopModal.status.connectingWs');
|
||||
|
||||
// Get RDP container dimensions after DOM update
|
||||
// DOM 更新后获取 RDP 容器尺寸
|
||||
await nextTick();
|
||||
|
||||
let widthToSend = 800; // Default/fallback width
|
||||
let heightToSend = 600; // Default/fallback height
|
||||
let widthToSend = 800; // 默认/备用宽度
|
||||
let heightToSend = 600; // 默认/备用高度
|
||||
const dpiToSend = 96;
|
||||
|
||||
if (rdpContainerRef.value) {
|
||||
// Use clientWidth/clientHeight as they represent the inner dimensions available for content
|
||||
// 使用 clientWidth/clientHeight,因为它们代表可用于内容的内部尺寸
|
||||
widthToSend = rdpContainerRef.value.clientWidth;
|
||||
heightToSend = rdpContainerRef.value.clientHeight - 1; // Subtract 1 based on feedback
|
||||
// Ensure minimum dimensions, adjust if necessary based on backend requirements
|
||||
heightToSend = rdpContainerRef.value.clientHeight - 1; // 根据反馈减去 1
|
||||
// 确保最小尺寸,必要时根据后端要求进行调整
|
||||
widthToSend = Math.max(100, widthToSend);
|
||||
heightToSend = Math.max(100, heightToSend);
|
||||
console.log(`Calculated RDP dimensions: ${widthToSend}x${heightToSend}`);
|
||||
console.log(`计算出的 RDP 尺寸: ${widthToSend}x${heightToSend}`);
|
||||
} else {
|
||||
console.warn("RDP container ref not available to get dimensions. Using defaults.");
|
||||
// Consider setting an error state or notifying the user
|
||||
console.warn("RDP 容器引用不可用,无法获取尺寸。使用默认值。");
|
||||
// 考虑设置错误状态或通知用户
|
||||
}
|
||||
|
||||
// Construct URL for the backend proxy endpoint using the determined base URL
|
||||
// 使用确定的基础 URL 构建后端代理端点的 URL
|
||||
const tunnelUrl = `${backendBaseUrl}/rdp-proxy?token=${encodeURIComponent(token)}&width=${widthToSend}&height=${heightToSend}&dpi=${dpiToSend}`;
|
||||
console.log(`[RDP Modal] Connecting to tunnel: ${tunnelUrl}`); // Log the final URL
|
||||
console.log(`[RDP 模态框] 连接到隧道: ${tunnelUrl}`); // 记录最终 URL
|
||||
// @ts-ignore
|
||||
const tunnel = new Guacamole.WebSocketTunnel(tunnelUrl);
|
||||
|
||||
@@ -116,8 +111,8 @@ const connectRdp = async () => { // Removed useInputValues parameter
|
||||
|
||||
// @ts-ignore
|
||||
guacClient.value = new Guacamole.Client(tunnel);
|
||||
// Add this line to enable keep-alive (send NOP every 3 seconds)
|
||||
guacClient.value.keepAliveFrequency = 3000; // milliseconds
|
||||
// 添加此行以启用 keep-alive (每 3 秒发送 NOP)
|
||||
guacClient.value.keepAliveFrequency = 3000; // 毫秒
|
||||
|
||||
rdpDisplayRef.value.appendChild(guacClient.value.getDisplay().getElement());
|
||||
|
||||
@@ -171,7 +166,7 @@ const connectRdp = async () => { // Removed useInputValues parameter
|
||||
disconnectRdp();
|
||||
};
|
||||
|
||||
guacClient.value.connect(''); // Keep the '' change
|
||||
guacClient.value.connect(''); // 保留 '' 的更改
|
||||
|
||||
} catch (error: any) {
|
||||
statusMessage.value = `${t('remoteDesktopModal.errors.connectionFailed')}: ${error.response?.data?.message || error.message || String(error)}`;
|
||||
@@ -195,7 +190,7 @@ const setupInputListeners = () => {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
keyboard.value = new Guacamole.Keyboard(document); // Attach listener to document for better capture
|
||||
keyboard.value = new Guacamole.Keyboard(document); // 将监听器附加到 document 以便更好地捕获
|
||||
|
||||
keyboard.value.onkeydown = (keysym: number) => {
|
||||
if (guacClient.value) {
|
||||
@@ -228,10 +223,7 @@ const removeInputListeners = () => {
|
||||
};
|
||||
|
||||
|
||||
// Removed stopResizeObserver as ResizeObserver is no longer used
|
||||
|
||||
const disconnectRdp = () => {
|
||||
// stopResizeObserver(); // Removed
|
||||
removeInputListeners();
|
||||
if (guacClient.value) {
|
||||
guacClient.value.disconnect();
|
||||
@@ -249,38 +241,32 @@ const disconnectRdp = () => {
|
||||
};
|
||||
|
||||
|
||||
// Removed reconnectWithNewSize function
|
||||
|
||||
const closeModal = () => {
|
||||
disconnectRdp();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// Removed setupResizeObserver as ResizeObserver is no longer used
|
||||
|
||||
|
||||
// Removed loadDesiredModalSize function
|
||||
|
||||
// Watch local refs and save validated size to settings store
|
||||
// 监听本地 ref 并将验证后的尺寸保存到设置存储
|
||||
watch(desiredModalWidth, (newWidth, oldWidth) => {
|
||||
// 只有当值真正改变时才处理
|
||||
if (newWidth === oldWidth) {
|
||||
console.log(`[RDP Modal] Width watch triggered but value (${newWidth}) hasn't changed. Skipping save.`);
|
||||
console.log(`[RDP 模态框] 宽度监听触发,但值 (${newWidth}) 未改变。跳过保存。`);
|
||||
return;
|
||||
}
|
||||
console.log(`[RDP Modal] Watch triggered for desiredModalWidth: ${oldWidth} -> ${newWidth}`); // 添加日志
|
||||
// Validate new width before saving
|
||||
console.log(`[RDP 模态框] 监听 desiredModalWidth 触发: ${oldWidth} -> ${newWidth}`); // 添加日志
|
||||
// 保存前验证新宽度
|
||||
const validatedWidth = Math.max(MIN_MODAL_WIDTH, Number(newWidth) || MIN_MODAL_WIDTH);
|
||||
// Debounce saving the *validated* width
|
||||
// 防抖保存 *验证后* 的宽度
|
||||
if (saveWidthTimeout) clearTimeout(saveWidthTimeout);
|
||||
saveWidthTimeout = setTimeout(() => {
|
||||
// Only save the validated width, don't change the input value here
|
||||
console.log(`[RDP Modal] Debounced Save - Saving width: ${validatedWidth} (Input value: ${newWidth})`);
|
||||
// 只保存验证后的宽度,不要在此处更改输入值
|
||||
console.log(`[RDP 模态框] 防抖保存 - 保存宽度: ${validatedWidth} (输入值: ${newWidth})`);
|
||||
// 再次检查,确保在延迟期间值没有变回原来的 store 值
|
||||
if (String(validatedWidth) !== settingsStore.settings.rdpModalWidth) {
|
||||
settingsStore.updateSetting('rdpModalWidth', String(validatedWidth));
|
||||
} else {
|
||||
console.log(`[RDP Modal] Debounced Save - Width ${validatedWidth} matches store value. Skipping redundant save.`);
|
||||
console.log(`[RDP 模态框] 防抖保存 - 宽度 ${validatedWidth} 与存储值匹配。跳过冗余保存。`);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
@@ -288,52 +274,52 @@ watch(desiredModalWidth, (newWidth, oldWidth) => {
|
||||
watch(desiredModalHeight, (newHeight, oldHeight) => {
|
||||
// 只有当值真正改变时才处理
|
||||
if (newHeight === oldHeight) {
|
||||
console.log(`[RDP Modal] Height watch triggered but value (${newHeight}) hasn't changed. Skipping save.`);
|
||||
console.log(`[RDP 模态框] 高度监听触发,但值 (${newHeight}) 未改变。跳过保存。`);
|
||||
return;
|
||||
}
|
||||
console.log(`[RDP Modal] Watch triggered for desiredModalHeight: ${oldHeight} -> ${newHeight}`); // 添加日志
|
||||
// Validate new height before saving
|
||||
console.log(`[RDP 模态框] 监听 desiredModalHeight 触发: ${oldHeight} -> ${newHeight}`);
|
||||
// 保存前验证新高度
|
||||
const validatedHeight = Math.max(MIN_MODAL_HEIGHT, Number(newHeight) || MIN_MODAL_HEIGHT);
|
||||
// Debounce saving the *validated* height
|
||||
// 防抖保存 *验证后* 的高度
|
||||
if (saveHeightTimeout) clearTimeout(saveHeightTimeout);
|
||||
saveHeightTimeout = setTimeout(() => {
|
||||
// Only save the validated height, don't change the input value here
|
||||
console.log(`[RDP Modal] Debounced Save - Saving height: ${validatedHeight} (Input value: ${newHeight})`);
|
||||
// 只保存验证后的高度,不要在此处更改输入值
|
||||
console.log(`[RDP 模态框] 防抖保存 - 保存高度: ${validatedHeight} (输入值: ${newHeight})`);
|
||||
// 再次检查
|
||||
if (String(validatedHeight) !== settingsStore.settings.rdpModalHeight) {
|
||||
settingsStore.updateSetting('rdpModalHeight', String(validatedHeight));
|
||||
} else {
|
||||
console.log(`[RDP Modal] Debounced Save - Height ${validatedHeight} matches store value. Skipping redundant save.`);
|
||||
console.log(`[RDP 模态框] 防抖保存 - 高度 ${validatedHeight} 与存储值匹配。跳过冗余保存。`);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
|
||||
// Load initial size from settings store when component mounts or settings change
|
||||
// 组件挂载或设置更改时从设置存储加载初始尺寸
|
||||
watchEffect(() => {
|
||||
const storeWidth = settingsStore.settings.rdpModalWidth;
|
||||
const storeHeight = settingsStore.settings.rdpModalHeight;
|
||||
console.log(`[RDP Modal] Loading size from store - Width: ${storeWidth}, Height: ${storeHeight}`); // +++ Add log +++
|
||||
console.log(`[RDP 模态框] 从存储加载尺寸 - 宽度: ${storeWidth}, 高度: ${storeHeight}`);
|
||||
|
||||
// Use defaults from store if available, otherwise use component defaults
|
||||
const initialWidth = storeWidth ? parseInt(storeWidth, 10) : desiredModalWidth.value; // Use current ref value as fallback default
|
||||
const initialHeight = storeHeight ? parseInt(storeHeight, 10) : desiredModalHeight.value; // Use current ref value as fallback default
|
||||
// 如果存储中有默认值则使用,否则使用组件默认值
|
||||
const initialWidth = storeWidth ? parseInt(storeWidth, 10) : desiredModalWidth.value; // 使用当前 ref 值作为备用默认值
|
||||
const initialHeight = storeHeight ? parseInt(storeHeight, 10) : desiredModalHeight.value; // 使用当前 ref 值作为备用默认值
|
||||
|
||||
// Validate against minimums
|
||||
// 根据最小值进行验证
|
||||
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(`[RDP Modal] Applying validated size - Width: ${finalWidth}, Height: ${finalHeight}`); // +++ Add log +++
|
||||
console.log(`[RDP 模态框] 应用验证后的尺寸 - 宽度: ${finalWidth}, 高度: ${finalHeight}`);
|
||||
desiredModalWidth.value = finalWidth;
|
||||
desiredModalHeight.value = finalHeight;
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// Initial size loading is now handled by watchEffect
|
||||
// 初始尺寸加载现在由 watchEffect 处理
|
||||
|
||||
if (props.connection) {
|
||||
nextTick(async () => {
|
||||
await connectRdp(); // Connect using initial size
|
||||
// No need to setup observer anymore
|
||||
await connectRdp(); // 使用初始尺寸连接
|
||||
// 不再需要设置 observer
|
||||
});
|
||||
} else {
|
||||
statusMessage.value = t('remoteDesktopModal.errors.noConnection');
|
||||
@@ -342,14 +328,14 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectRdp(); // This already calls stopResizeObserver
|
||||
disconnectRdp(); // 这里已经调用了 removeInputListeners
|
||||
});
|
||||
|
||||
watch(() => props.connection, (newConnection, oldConnection) => {
|
||||
if (newConnection && newConnection.id !== oldConnection?.id) {
|
||||
nextTick(async () => {
|
||||
await connectRdp(); // Connect using initial size
|
||||
// No need to setup observer anymore
|
||||
await connectRdp(); // 使用初始尺寸连接
|
||||
// 不再需要设置 observer
|
||||
});
|
||||
} else if (!newConnection) {
|
||||
disconnectRdp();
|
||||
@@ -358,14 +344,10 @@ watch(() => props.connection, (newConnection, oldConnection) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Use the desired modal size directly for the style
|
||||
// 直接使用所需的模态框尺寸作为样式
|
||||
const computedModalStyle = computed(() => {
|
||||
// const extraWidth = 40; // Removed from here as well
|
||||
// const headerHeight = 45; // Defined in connectRdp
|
||||
// const footerHeight = 35; // Defined in connectRdp
|
||||
// const extraHeight = headerHeight + footerHeight + 10; // Defined in connectRdp
|
||||
|
||||
// Apply minimum constraints here for the actual modal style
|
||||
// 在此处为实际模态框样式应用最小约束
|
||||
const actualWidth = Math.max(MIN_MODAL_WIDTH, desiredModalWidth.value);
|
||||
const actualHeight = Math.max(MIN_MODAL_HEIGHT, desiredModalHeight.value);
|
||||
return {
|
||||
@@ -442,7 +424,7 @@ const computedModalStyle = computed(() => {
|
||||
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"
|
||||
/>
|
||||
<!-- Add Reconnect Button -->
|
||||
<!-- 添加重新连接按钮 -->
|
||||
<button
|
||||
@click="connectRdp"
|
||||
:disabled="connectionStatus === 'connecting'"
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
<template>
|
||||
<!-- Root element with padding, background, border, and text styles -->
|
||||
<!-- 根元素,包含内边距、背景、边框和文本样式 -->
|
||||
<div class="status-monitor p-4 bg-background text-foreground h-full overflow-y-auto text-sm">
|
||||
<!-- Title with margin, border, padding, font size, and color -->
|
||||
<!-- 标题,包含外边距、边框、内边距、字体大小和颜色 -->
|
||||
<h4 class="mt-0 mb-4 border-b border-border pb-2 text-base font-medium">
|
||||
{{ t('statusMonitor.title') }}
|
||||
</h4>
|
||||
|
||||
<!-- Error State -->
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="statusError" class="status-error flex flex-col items-center justify-center text-center text-red-500 mt-4 h-full">
|
||||
<i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
|
||||
<span>{{ t('statusMonitor.errorPrefix') }} {{ statusError }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<!-- 加载状态 -->
|
||||
<div v-else-if="!serverStatus" class="loading-status flex flex-col items-center justify-center text-center text-text-secondary mt-4 h-full">
|
||||
<i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
|
||||
<span>{{ t('statusMonitor.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status Grid -->
|
||||
<!-- 状态网格 -->
|
||||
<div v-else class="status-grid grid gap-3">
|
||||
<!-- CPU Model -->
|
||||
<!-- CPU 型号 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuModelLabel') }}</label>
|
||||
<span class="cpu-model-value truncate text-left" :title="displayCpuModel">{{ displayCpuModel }}</span>
|
||||
</div>
|
||||
|
||||
<!-- OS Name -->
|
||||
<!-- 操作系统名称 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.osLabel') }}</label>
|
||||
<span class="os-name-value truncate text-left" :title="displayOsName">{{ displayOsName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- CPU Usage -->
|
||||
<!-- CPU 使用率 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow"> <!-- Reduced height -->
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow"> <!-- 减小高度 -->
|
||||
<div class="progress-bar bg-blue-500 h-full transition-width duration-300 ease-in-out" :style="{ width: `${serverStatus.cpuPercent ?? 0}%` }"></div>
|
||||
</div>
|
||||
<span class="font-mono text-left text-xs w-12 text-right">{{ serverStatus.cpuPercent?.toFixed(1) ?? t('statusMonitor.notAvailable') }}%</span> <!-- Fixed width and right align -->
|
||||
<span class="font-mono text-left text-xs w-12 text-right">{{ serverStatus.cpuPercent?.toFixed(1) ?? t('statusMonitor.notAvailable') }}%</span> <!-- 固定宽度并右对齐 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Usage -->
|
||||
<!-- 内存使用率 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.memoryLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
@@ -54,12 +54,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Swap Usage -->
|
||||
<!-- swap -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.swapLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow">
|
||||
<!-- Conditional color for swap -->
|
||||
<!-- swap颜色 -->
|
||||
<div class="progress-bar h-full transition-width duration-300 ease-in-out"
|
||||
:class="serverStatus.swapPercent && serverStatus.swapPercent > 0 ? 'bg-yellow-500' : 'bg-gray-500'"
|
||||
:style="{ width: `${serverStatus.swapPercent ?? 0}%` }"></div>
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disk Usage -->
|
||||
<!-- 磁盘使用率 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.diskLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
@@ -79,16 +79,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Rate -->
|
||||
<!-- 网络速率 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3 mt-2">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.networkLabel') }} ({{ serverStatus.netInterface || '...' }}):</label>
|
||||
<div class="network-values flex items-center justify-start gap-4"> <!-- Reduced gap -->
|
||||
<div class="network-values flex items-center justify-start gap-4"> <!-- 减小间距 -->
|
||||
<span class="rate down inline-flex items-center gap-1 text-green-500 text-xs whitespace-nowrap">
|
||||
<i class="fas fa-arrow-down w-3 text-center"></i> <!-- Font Awesome icon -->
|
||||
<i class="fas fa-arrow-down w-3 text-center"></i> <!-- Font Awesome 图标 -->
|
||||
<span class="font-mono">{{ formatBytesPerSecond(serverStatus.netRxRate) }}</span>
|
||||
</span>
|
||||
<span class="rate up inline-flex items-center gap-1 text-orange-500 text-xs whitespace-nowrap">
|
||||
<i class="fas fa-arrow-up w-3 text-center"></i> <!-- Font Awesome icon -->
|
||||
<i class="fas fa-arrow-up w-3 text-center"></i> <!-- Font Awesome 图标 -->
|
||||
<span class="font-mono">{{ formatBytesPerSecond(serverStatus.netTxRate) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Interface remains the same
|
||||
|
||||
interface ServerStatus {
|
||||
cpuPercent?: number;
|
||||
memPercent?: number;
|
||||
@@ -116,8 +116,8 @@ interface ServerStatus {
|
||||
diskUsed?: number; // KB
|
||||
diskTotal?: number; // KB
|
||||
cpuModel?: string;
|
||||
netRxRate?: number; // Bytes per second
|
||||
netTxRate?: number; // Bytes per second
|
||||
netRxRate?: number; // 字节/秒
|
||||
netTxRate?: number; // 字节/秒
|
||||
netInterface?: string;
|
||||
osName?: string;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ const props = defineProps<{
|
||||
statusError?: string | null;
|
||||
}>();
|
||||
|
||||
// --- Caching logic remains the same ---
|
||||
// --- 缓存逻辑保持不变 ---
|
||||
const cachedCpuModel = ref<string | null>(null);
|
||||
const cachedOsName = ref<string | null>(null);
|
||||
|
||||
@@ -143,7 +143,7 @@ watch(() => props.serverStatus, (newData) => {
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// --- Computed properties remain the same ---
|
||||
// --- 计算属性保持不变 ---
|
||||
const displayCpuModel = computed(() => {
|
||||
return (props.serverStatus?.cpuModel ?? cachedCpuModel.value) || t('statusMonitor.notAvailable');
|
||||
});
|
||||
@@ -167,7 +167,7 @@ const formatKbToGb = (kb?: number): string => {
|
||||
return `${gb.toFixed(1)} ${t('statusMonitor.gigaBytes')}`;
|
||||
};
|
||||
|
||||
// Helper function to format MB to GB if needed
|
||||
// 辅助函数,用于在需要时将 MB 格式化为 GB
|
||||
const formatMemorySize = (mb?: number): string => {
|
||||
if (mb === undefined || mb === null || isNaN(mb)) return t('statusMonitor.notAvailable');
|
||||
if (mb < 1024) {
|
||||
@@ -182,14 +182,14 @@ const formatMemorySize = (mb?: number): string => {
|
||||
const memDisplay = computed(() => {
|
||||
const data = props.serverStatus;
|
||||
if (!data || data.memUsed === undefined || data.memTotal === undefined) return t('statusMonitor.notAvailable');
|
||||
const percent = data.memPercent !== undefined ? `(${(data.memPercent).toFixed(1)}%)` : ''; // Keep 1 decimal for percent
|
||||
const percent = data.memPercent !== undefined ? `(${(data.memPercent).toFixed(1)}%)` : ''; // 百分比保留 1 位小数
|
||||
return `${formatMemorySize(data.memUsed)} / ${formatMemorySize(data.memTotal)} ${percent}`;
|
||||
});
|
||||
|
||||
const diskDisplay = computed(() => {
|
||||
const data = props.serverStatus;
|
||||
if (!data || data.diskUsed === undefined || data.diskTotal === undefined) return t('statusMonitor.notAvailable');
|
||||
const percent = data.diskPercent !== undefined ? `(${(data.diskPercent).toFixed(1)}%)` : ''; // Keep 1 decimal for percent
|
||||
const percent = data.diskPercent !== undefined ? `(${(data.diskPercent).toFixed(1)}%)` : ''; // 百分比保留 1 位小数
|
||||
return `${formatKbToGb(data.diskUsed)} / ${formatKbToGb(data.diskTotal)} ${percent}`;
|
||||
});
|
||||
|
||||
@@ -199,15 +199,14 @@ const swapDisplay = computed(() => {
|
||||
const total = data?.swapTotal ?? 0;
|
||||
const percentVal = data?.swapPercent ?? 0;
|
||||
|
||||
// Only show details if swap total > 0
|
||||
// 仅当交换空间总量 > 0 时显示详细信息
|
||||
if (total === 0) {
|
||||
return t('statusMonitor.swapNotAvailable'); // Or a more specific message
|
||||
return t('statusMonitor.swapNotAvailable'); // 或更具体的消息
|
||||
}
|
||||
|
||||
const percent = `(${(percentVal).toFixed(1)}%)`; // Keep 1 decimal for percent
|
||||
const percent = `(${(percentVal).toFixed(1)}%)`; // 百分比保留 1 位小数
|
||||
return `${formatMemorySize(used)} / ${formatMemorySize(total)} ${percent}`;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<!-- No <style scoped> needed anymore -->
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, PropType, onMounted, watch } from 'vue'; // 导入 ref, computed, onMounted, watch
|
||||
import { useI18n } from 'vue-i18n'; // 导入 i18n
|
||||
import { useRoute } from 'vue-router'; // 导入 useRoute
|
||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue'; // 导入连接列表组件
|
||||
import { useSessionStore } from '../stores/session.store'; // 导入 session store
|
||||
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store'; // +++ 导入 connections store 和类型 +++
|
||||
import { useLayoutStore, type PaneName } from '../stores/layout.store'; // 导入布局 store 和类型
|
||||
import { ref, computed, PropType, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import WorkspaceConnectionListComponent from './WorkspaceConnectionList.vue';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
import { useConnectionsStore, type ConnectionInfo } from '../stores/connections.store';
|
||||
import { useLayoutStore, type PaneName } from '../stores/layout.store';
|
||||
// 导入会话状态类型
|
||||
import type { SessionTabInfoWithStatus } from '../stores/session.store'; // 导入更新后的类型
|
||||
// *** 假设 layoutStore 会有这些状态和方法 ***
|
||||
// import { useLayoutStore } from '../stores/layout.store';
|
||||
import type { SessionTabInfoWithStatus } from '../stores/session.store';
|
||||
|
||||
|
||||
|
||||
// --- Setup ---
|
||||
const { t } = useI18n(); // 初始化 i18n
|
||||
const layoutStore = useLayoutStore(); // 初始化布局 store
|
||||
const connectionsStore = useConnectionsStore(); // +++ 获取 connections store 实例 +++
|
||||
const connectionsStore = useConnectionsStore();
|
||||
const { isHeaderVisible } = storeToRefs(layoutStore); // 从 layout store 获取主导航栏可见状态
|
||||
const route = useRoute(); // 获取路由实例
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
sessions: {
|
||||
type: Array as PropType<SessionTabInfoWithStatus[]>, // 使用更新后的类型
|
||||
type: Array as PropType<SessionTabInfoWithStatus[]>,
|
||||
required: true,
|
||||
},
|
||||
activeSessionId: {
|
||||
type: String as PropType<string | null>, // 类型已包含 null
|
||||
required: false, // 改为非必需,允许初始为 null
|
||||
default: null, // 提供默认值 null
|
||||
type: String as PropType<string | null>,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -37,10 +36,8 @@ const emit = defineEmits([
|
||||
'activate-session',
|
||||
'close-session',
|
||||
'open-layout-configurator',
|
||||
'request-add-connection-from-popup', // 声明从弹窗发出的添加请求事件
|
||||
'request-edit-connection-from-popup' // 新增:声明从弹窗发出的编辑请求事件
|
||||
// --- 移除 RDP 事件 ---
|
||||
// 'request-rdp-modal-from-popup'
|
||||
'request-add-connection-from-popup',
|
||||
'request-edit-connection-from-popup'
|
||||
]);
|
||||
|
||||
const activateSession = (sessionId: string) => {
|
||||
@@ -151,7 +148,7 @@ const toggleButtonTitle = computed(() => {
|
||||
// 调整 i18n key 和默认文本
|
||||
return isHeaderVisible.value ? t('header.hide', '隐藏顶部导航') : t('header.show', '显示顶部导航');
|
||||
});
|
||||
// --- End Header Visibility Logic ---
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -226,4 +223,4 @@ const toggleButtonTitle = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Scoped styles removed, now using Tailwind utility classes -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user