This commit is contained in:
Baobhan Sith
2025-04-28 21:25:02 +08:00
parent 6246497807
commit 6ccfca055c
7 changed files with 150 additions and 210 deletions
@@ -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 -->
+10 -25
View File
@@ -1,48 +1,40 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useConnectionsStore } from '../stores/connections.store';
import { useAuditLogStore } from '../stores/audit.store'; // 修正 Store 名称
import { useSessionStore } from '../stores/session.store'; // +++ 引入 Session Store +++
import { useAuditLogStore } from '../stores/audit.store';
import { useSessionStore } from '../stores/session.store';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import type { ConnectionInfo } from '../stores/connections.store'; // 只导入 ConnectionInfo
import type { AuditLogEntry } from '../types/audit.types'; // 引入本地 AuditLogEntry 类型
import type { ConnectionInfo } from '../stores/connections.store';
import { storeToRefs } from 'pinia';
import { formatDistanceToNow } from 'date-fns';
import { zhCN, enUS, ja } from 'date-fns/locale'; // 导入所有需要的语言包
import type { Locale } from 'date-fns'; // 导入 Locale 类型
import { zhCN, enUS, ja } from 'date-fns/locale';
import type { Locale } from 'date-fns';
const { t, locale } = useI18n();
const router = useRouter();
const connectionsStore = useConnectionsStore();
const auditLogStore = useAuditLogStore(); // 修正变量名
const sessionStore = useSessionStore(); // +++ 获取 Session Store 实例 +++
const auditLogStore = useAuditLogStore();
const sessionStore = useSessionStore();
const { connections, isLoading: isLoadingConnections } = storeToRefs(connectionsStore);
const { logs: auditLogs, isLoading: isLoadingLogs, totalLogs } = storeToRefs(auditLogStore); // 使用修正后的变量名
const { logs: auditLogs, isLoading: isLoadingLogs, totalLogs } = storeToRefs(auditLogStore);
const maxRecentConnections = 5;
const maxRecentLogs = 5;
// --- 最近连接 ---
const recentConnections = computed(() => {
console.log('[仪表盘] 从 Store 获取的原始连接列表:', JSON.parse(JSON.stringify(connections.value)));
// 优先尝试按 last_connected_at 过滤和排序
const connected = connections.value.filter(c => c.last_connected_at);
console.log('[仪表盘] 过滤后的连接 (包含 last_connected_at):', JSON.parse(JSON.stringify(connected)));
const connected = connections.value.filter(c => c.last_connected_at);
if (connected.length > 0) {
connected.sort((a, b) => (b.last_connected_at ?? 0) - (a.last_connected_at ?? 0));
const result = connected.slice(0, maxRecentConnections);
console.log('[仪表盘] 最终最近连接 (使用 last_connected_at):', JSON.parse(JSON.stringify(result)));
return result;
} else {
// 如果没有带 last_connected_at 的连接,则按 updated_at 排序显示最近更新的
console.log('[仪表盘] 未找到包含 last_connected_at 的连接,回退到按 updated_at 排序。');
const sortedByUpdate = [...connections.value].sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
const result = sortedByUpdate.slice(0, maxRecentConnections);
console.log('[仪表盘] 最终最近连接 (回退使用 updated_at):', JSON.parse(JSON.stringify(result)));
return result;
}
});
@@ -58,12 +50,9 @@ onMounted(async () => {
// 如果 connections store 还没有加载过数据,则加载
if (connections.value.length === 0) {
try {
console.log('[Dashboard] onMounted: Fetching connections...'); // 添加日志
await connectionsStore.fetchConnections();
console.log('[Dashboard] onMounted: Connections fetched.'); // 添加日志
} catch (error) {
console.error("加载连接列表失败:", error);
// 可以在这里显示错误通知
}
}
// 加载最新的审计日志
@@ -84,7 +73,6 @@ onMounted(async () => {
// --- 方法 ---
// 修改函数签名,接受 ConnectionInfo 类型
const connectTo = (connection: ConnectionInfo) => {
console.log(`[Dashboard] connectTo called for connection: ${connection.name} (ID: ${connection.id}, Type: ${connection.type})`);
// 将连接处理逻辑委托给 sessionStore
sessionStore.handleConnectRequest(connection);
};
@@ -151,9 +139,6 @@ const isFailedAction = (actionType: string): boolean => {
const lowerCaseAction = actionType.toLowerCase();
// 检查常见的失败关键词
return lowerCaseAction.includes('fail') || lowerCaseAction.includes('error') || lowerCaseAction.includes('denied');
// 或者,如果 action_type 本身不包含明确的失败词,但翻译后包含,可以这样判断:
// const translatedAction = getActionTranslation(actionType);
// return translatedAction.includes('失败') || translatedAction.toLowerCase().includes('fail');
};
</script>
@@ -1,7 +1,6 @@
<template>
<div class="p-4 bg-background text-foreground"> <!-- Outer container with padding -->
<div class="max-w-6xl mx-auto"> <!-- Inner container for max-width and centering -->
<!-- Removed temporary h1 title -->
<div class="p-4 bg-background text-foreground">
<div class="max-w-6xl mx-auto">
<NotificationSettings />
</div>
</div>
@@ -11,6 +10,4 @@
import NotificationSettings from '../components/NotificationSettings.vue';
</script>
<style scoped>
/* Remove scoped styles as they are now handled by Tailwind utility classes */
</style>
+2 -2
View File
@@ -2,8 +2,8 @@
import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useProxiesStore, ProxyInfo } from '../stores/proxies.store';
import ProxyList from '../components/ProxyList.vue'; // 引入列表组件
import AddProxyForm from '../components/AddProxyForm.vue'; // 引入表单组件
import ProxyList from '../components/ProxyList.vue';
import AddProxyForm from '../components/AddProxyForm.vue';
const { t } = useI18n();
const proxiesStore = useProxiesStore();