feat: 为rdp和vnc模态框添加最小化按钮
This commit is contained in:
@@ -31,6 +31,13 @@ const mouse = ref<any | null>(null);
|
|||||||
const desiredModalWidth = ref(1064);
|
const desiredModalWidth = ref(1064);
|
||||||
const desiredModalHeight = ref(858);
|
const desiredModalHeight = ref(858);
|
||||||
const isKeyboardDisabledForInput = ref(false); // 标记键盘是否因输入框聚焦而禁用
|
const isKeyboardDisabledForInput = ref(false); // 标记键盘是否因输入框聚焦而禁用
|
||||||
|
const isMinimized = ref(false);
|
||||||
|
const restoreButtonRef = ref<HTMLButtonElement | null>(null);
|
||||||
|
const isDraggingRestoreButton = ref(false);
|
||||||
|
const restoreButtonPosition = ref({ x: 16, y: window.innerHeight / 2 - 25 }); // 16px from left, vertically centered
|
||||||
|
let dragOffsetX = 0;
|
||||||
|
let dragOffsetY = 0;
|
||||||
|
let hasDragged = false; // 新增 hasDragged 标志
|
||||||
|
|
||||||
const MIN_MODAL_WIDTH = 1024;
|
const MIN_MODAL_WIDTH = 1024;
|
||||||
const MIN_MODAL_HEIGHT = 768;
|
const MIN_MODAL_HEIGHT = 768;
|
||||||
@@ -338,6 +345,55 @@ const enableRdpKeyboard = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const minimizeModal = () => {
|
||||||
|
isMinimized.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreModal = () => {
|
||||||
|
isMinimized.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRestoreButtonMouseDown = (event: MouseEvent) => {
|
||||||
|
if (!restoreButtonRef.value) return;
|
||||||
|
hasDragged = false; // 重置拖拽标志
|
||||||
|
isDraggingRestoreButton.value = true;
|
||||||
|
dragOffsetX = event.clientX - restoreButtonRef.value.getBoundingClientRect().left;
|
||||||
|
dragOffsetY = event.clientY - restoreButtonRef.value.getBoundingClientRect().top;
|
||||||
|
event.preventDefault();
|
||||||
|
document.addEventListener('mousemove', onRestoreButtonMouseMove);
|
||||||
|
document.addEventListener('mouseup', onRestoreButtonMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRestoreButtonMouseMove = (event: MouseEvent) => {
|
||||||
|
if (!isDraggingRestoreButton.value) return;
|
||||||
|
hasDragged = true; // 如果鼠标移动则设置拖拽标志
|
||||||
|
let newX = event.clientX - dragOffsetX;
|
||||||
|
let newY = event.clientY - dragOffsetY;
|
||||||
|
|
||||||
|
const buttonWidth = 50;
|
||||||
|
const buttonHeight = 50;
|
||||||
|
newX = Math.max(0, Math.min(newX, window.innerWidth - buttonWidth));
|
||||||
|
newY = Math.max(0, Math.min(newY, window.innerHeight - buttonHeight));
|
||||||
|
|
||||||
|
restoreButtonPosition.value = { x: newX, y: newY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRestoreButtonMouseUp = () => {
|
||||||
|
isDraggingRestoreButton.value = false;
|
||||||
|
document.removeEventListener('mousemove', onRestoreButtonMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onRestoreButtonMouseUp);
|
||||||
|
// click 事件会在 mouseup 后触发。如果我们拖拽了,我们不希望 click 事件恢复模态框。
|
||||||
|
// handleClickRestoreButton 会检查 hasDragged。
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickRestoreButton = () => {
|
||||||
|
if (!hasDragged) {
|
||||||
|
restoreModal();
|
||||||
|
}
|
||||||
|
// 为下一次交互重置
|
||||||
|
hasDragged = false;
|
||||||
|
};
|
||||||
|
|
||||||
const disconnectGuacamole = () => {
|
const disconnectGuacamole = () => {
|
||||||
removeInputListeners();
|
removeInputListeners();
|
||||||
isKeyboardDisabledForInput.value = false; // 确保状态重置
|
isKeyboardDisabledForInput.value = false; // 确保状态重置
|
||||||
@@ -444,6 +500,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disconnectGuacamole(); // 这里已经调用了 removeInputListeners
|
disconnectGuacamole(); // 这里已经调用了 removeInputListeners
|
||||||
|
document.removeEventListener('mousemove', onRestoreButtonMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onRestoreButtonMouseUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.connection, (newConnection, oldConnection) => {
|
watch(() => props.connection, (newConnection, oldConnection) => {
|
||||||
@@ -473,17 +531,35 @@ const computedModalStyle = computed(() => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-overlay p-4">
|
|
||||||
<div
|
<div
|
||||||
|
:class="[
|
||||||
|
'fixed inset-0 z-50 flex items-center justify-center p-4',
|
||||||
|
isMinimized ? '' : 'bg-overlay',
|
||||||
|
isMinimized ? 'pointer-events-none' : '' // 允许恢复按钮接收事件
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
ref="restoreButtonRef"
|
||||||
|
v-if="isMinimized"
|
||||||
|
@mousedown="onRestoreButtonMouseDown"
|
||||||
|
@click="handleClickRestoreButton"
|
||||||
|
:style="{ left: `${restoreButtonPosition.x}px`, top: `${restoreButtonPosition.y}px`, width: '50px', height: '50px' }"
|
||||||
|
class="fixed z-[100] flex items-center justify-center bg-primary text-white rounded-full shadow-lg hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50 pointer-events-auto cursor-grab active:cursor-grabbing"
|
||||||
|
:title="t('common.restore')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-window-restore fa-lg"></i>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-show="!isMinimized"
|
||||||
:style="computedModalStyle"
|
:style="computedModalStyle"
|
||||||
class="bg-background text-foreground rounded-lg shadow-xl flex flex-col overflow-hidden border border-border"
|
class="bg-background text-foreground rounded-lg shadow-xl flex flex-col overflow-hidden border border-border pointer-events-auto"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between p-3 border-b border-border flex-shrink-0">
|
<div class="flex items-center justify-between p-3 border-b border-border flex-shrink-0">
|
||||||
<h3 class="text-base font-semibold truncate">
|
<h3 class="text-base font-semibold truncate">
|
||||||
<i class="fas fa-desktop mr-2 text-text-secondary"></i>
|
<i class="fas fa-desktop mr-2 text-text-secondary"></i>
|
||||||
{{ t('remoteDesktopModal.title') }} - {{ props.connection?.name || props.connection?.host || t('remoteDesktopModal.titlePlaceholder') }}
|
{{ t('remoteDesktopModal.title') }} - {{ props.connection?.name || props.connection?.host || t('remoteDesktopModal.titlePlaceholder') }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-1">
|
||||||
<span class="text-xs px-2 py-0.5 rounded"
|
<span class="text-xs px-2 py-0.5 rounded"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-yellow-200 text-yellow-800': connectionStatus === 'connecting',
|
'bg-yellow-200 text-yellow-800': connectionStatus === 'connecting',
|
||||||
@@ -493,6 +569,13 @@ const computedModalStyle = computed(() => {
|
|||||||
}">
|
}">
|
||||||
{{ t('remoteDesktopModal.status.' + connectionStatus) }}
|
{{ t('remoteDesktopModal.status.' + connectionStatus) }}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
@click="minimizeModal"
|
||||||
|
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
||||||
|
:title="t('common.minimize')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-window-minimize fa-sm"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ const MIN_MODAL_HEIGHT = 600;
|
|||||||
const desiredModalWidth = ref(Math.max(MIN_MODAL_WIDTH, isNaN(initialStoreWidth) ? MIN_MODAL_WIDTH : initialStoreWidth));
|
const desiredModalWidth = ref(Math.max(MIN_MODAL_WIDTH, isNaN(initialStoreWidth) ? MIN_MODAL_WIDTH : initialStoreWidth));
|
||||||
const desiredModalHeight = ref(Math.max(MIN_MODAL_HEIGHT, isNaN(initialStoreHeight) ? MIN_MODAL_HEIGHT : initialStoreHeight));
|
const desiredModalHeight = ref(Math.max(MIN_MODAL_HEIGHT, isNaN(initialStoreHeight) ? MIN_MODAL_HEIGHT : initialStoreHeight));
|
||||||
const isKeyboardDisabledForInput = ref(false);
|
const isKeyboardDisabledForInput = ref(false);
|
||||||
|
const isMinimized = ref(false);
|
||||||
|
const restoreButtonRef = ref<HTMLButtonElement | null>(null);
|
||||||
|
const isDraggingRestoreButton = ref(false);
|
||||||
|
const restoreButtonPosition = ref({ x: 16, y: window.innerHeight / 2 - 25 }); // 16px from left, vertically centered (25 is half of button height 50px)
|
||||||
|
let dragOffsetX = 0;
|
||||||
|
let dragOffsetY = 0;
|
||||||
|
let hasDragged = false;
|
||||||
|
|
||||||
let vncWsBaseUrl: string;
|
let vncWsBaseUrl: string;
|
||||||
const VNC_WS_PORT_FROM_ENV = import.meta.env.VITE_VNC_WS_PORT || '8082';
|
const VNC_WS_PORT_FROM_ENV = import.meta.env.VITE_VNC_WS_PORT || '8082';
|
||||||
@@ -293,6 +300,57 @@ const enableVncKeyboard = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const minimizeModal = () => {
|
||||||
|
isMinimized.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreModal = () => {
|
||||||
|
isMinimized.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRestoreButtonMouseDown = (event: MouseEvent) => {
|
||||||
|
if (!restoreButtonRef.value) return;
|
||||||
|
hasDragged = false; // Reset drag flag
|
||||||
|
isDraggingRestoreButton.value = true;
|
||||||
|
dragOffsetX = event.clientX - restoreButtonRef.value.getBoundingClientRect().left;
|
||||||
|
dragOffsetY = event.clientY - restoreButtonRef.value.getBoundingClientRect().top;
|
||||||
|
// Prevent text selection while dragging
|
||||||
|
event.preventDefault();
|
||||||
|
document.addEventListener('mousemove', onRestoreButtonMouseMove);
|
||||||
|
document.addEventListener('mouseup', onRestoreButtonMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRestoreButtonMouseMove = (event: MouseEvent) => {
|
||||||
|
if (!isDraggingRestoreButton.value) return;
|
||||||
|
hasDragged = true; // Set drag flag if mouse moves
|
||||||
|
let newX = event.clientX - dragOffsetX;
|
||||||
|
let newY = event.clientY - dragOffsetY;
|
||||||
|
|
||||||
|
// Constrain movement within viewport
|
||||||
|
const buttonWidth = 50; // As defined in style
|
||||||
|
const buttonHeight = 50; // As defined in style
|
||||||
|
newX = Math.max(0, Math.min(newX, window.innerWidth - buttonWidth));
|
||||||
|
newY = Math.max(0, Math.min(newY, window.innerHeight - buttonHeight));
|
||||||
|
|
||||||
|
restoreButtonPosition.value = { x: newX, y: newY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRestoreButtonMouseUp = () => {
|
||||||
|
isDraggingRestoreButton.value = false;
|
||||||
|
document.removeEventListener('mousemove', onRestoreButtonMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onRestoreButtonMouseUp);
|
||||||
|
// Click event will fire after mouseup. If we dragged, we don't want click to restore.
|
||||||
|
// The handleClickRestoreButton will check hasDragged.
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickRestoreButton = () => {
|
||||||
|
if (!hasDragged) {
|
||||||
|
restoreModal();
|
||||||
|
}
|
||||||
|
// Reset for next interaction
|
||||||
|
hasDragged = false;
|
||||||
|
};
|
||||||
|
|
||||||
const disconnectGuacamole = () => {
|
const disconnectGuacamole = () => {
|
||||||
removeInputListeners();
|
removeInputListeners();
|
||||||
isKeyboardDisabledForInput.value = false;
|
isKeyboardDisabledForInput.value = false;
|
||||||
@@ -382,6 +440,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disconnectGuacamole();
|
disconnectGuacamole();
|
||||||
|
document.removeEventListener('mousemove', onRestoreButtonMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onRestoreButtonMouseUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.connection, (newConnection, oldConnection) => {
|
watch(() => props.connection, (newConnection, oldConnection) => {
|
||||||
@@ -426,17 +486,35 @@ watchEffect(() => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-overlay p-4">
|
|
||||||
<div
|
<div
|
||||||
|
:class="[
|
||||||
|
'fixed inset-0 z-50 flex items-center justify-center p-4',
|
||||||
|
isMinimized ? '' : 'bg-overlay',
|
||||||
|
isMinimized ? 'pointer-events-none' : '' // 允许恢复按钮接收事件
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
ref="restoreButtonRef"
|
||||||
|
v-if="isMinimized"
|
||||||
|
@mousedown="onRestoreButtonMouseDown"
|
||||||
|
@click="handleClickRestoreButton"
|
||||||
|
:style="{ left: `${restoreButtonPosition.x}px`, top: `${restoreButtonPosition.y}px`, width: '50px', height: '50px' }"
|
||||||
|
class="fixed z-[100] flex items-center justify-center bg-primary text-white rounded-full shadow-lg hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50 pointer-events-auto cursor-grab active:cursor-grabbing"
|
||||||
|
:title="t('common.restore')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-window-restore fa-lg"></i>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-show="!isMinimized"
|
||||||
:style="computedModalStyle"
|
:style="computedModalStyle"
|
||||||
class="bg-background text-foreground rounded-lg shadow-xl flex flex-col overflow-hidden border border-border"
|
class="bg-background text-foreground rounded-lg shadow-xl flex flex-col overflow-hidden border border-border pointer-events-auto"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between p-3 border-b border-border flex-shrink-0">
|
<div class="flex items-center justify-between p-3 border-b border-border flex-shrink-0">
|
||||||
<h3 class="text-base font-semibold truncate">
|
<h3 class="text-base font-semibold truncate">
|
||||||
<i class="fas fa-desktop mr-2 text-text-secondary"></i>
|
<i class="fas fa-desktop mr-2 text-text-secondary"></i>
|
||||||
{{ t('vncModal.title') }} - {{ props.connection?.name || props.connection?.host || t('remoteDesktopModal.titlePlaceholder') }}
|
{{ t('vncModal.title') }} - {{ props.connection?.name || props.connection?.host || t('remoteDesktopModal.titlePlaceholder') }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-1">
|
||||||
<span class="text-xs px-2 py-0.5 rounded"
|
<span class="text-xs px-2 py-0.5 rounded"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-yellow-200 text-yellow-800': connectionStatus === 'connecting',
|
'bg-yellow-200 text-yellow-800': connectionStatus === 'connecting',
|
||||||
@@ -446,6 +524,13 @@ watchEffect(() => {
|
|||||||
}">
|
}">
|
||||||
{{ t('remoteDesktopModal.status.' + connectionStatus) }}
|
{{ t('remoteDesktopModal.status.' + connectionStatus) }}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
@click="minimizeModal"
|
||||||
|
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
||||||
|
:title="t('common.minimize')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-window-minimize fa-sm"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
class="text-text-secondary hover:text-foreground transition-colors duration-150 p-1 rounded hover:bg-hover"
|
||||||
|
|||||||
Reference in New Issue
Block a user