mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
480 lines
15 KiB
Vue
480 lines
15 KiB
Vue
<template>
|
||
<div>
|
||
<div ref="viewportRef" class="viewport" :style="{ width: state.size.width + 'px', height: state.size.height + 'px' }">
|
||
<div ref="displayRef" class="display" tabindex="0" />
|
||
<div class="btn-box">
|
||
<SvgIcon name="DocumentCopy" @click="openPaste" :size="20" class="pointer-icon mr10" title="剪贴板" />
|
||
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr10" title="文件管理" />
|
||
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr10" title="全屏" />
|
||
|
||
<el-dropdown>
|
||
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr10" title="发送快捷键" style="color: #fff" />
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65535'])"> Ctrl + Alt + Delete </el-dropdown-item>
|
||
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65288'])"> Ctrl + Alt + Backspace </el-dropdown-item>
|
||
<el-dropdown-item @click="openSendKeyboard(['65515', '100'])"> Windows + D </el-dropdown-item>
|
||
<el-dropdown-item @click="openSendKeyboard(['65515', '101'])"> Windows + E </el-dropdown-item>
|
||
<el-dropdown-item @click="openSendKeyboard(['65515', '114'])"> Windows + R </el-dropdown-item>
|
||
<el-dropdown-item @click="openSendKeyboard(['65515'])"> Windows </el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
|
||
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
|
||
</div>
|
||
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
|
||
</div>
|
||
<el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
|
||
<machine-file
|
||
:machine-id="state.filesystemDialog.machineId"
|
||
:auth-cert-name="state.filesystemDialog.authCertName"
|
||
:protocol="state.filesystemDialog.protocol"
|
||
:file-id="state.filesystemDialog.fileId"
|
||
:path="state.filesystemDialog.path"
|
||
/>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import Guacamole from './guac/guacamole-common';
|
||
import { getMachineRdpSocketUrl } from '@/views/ops/machine/api';
|
||
import clipboard from './guac/clipboard';
|
||
import { reactive, ref } from 'vue';
|
||
import { TerminalStatus } from '@/components/terminal/common';
|
||
import ClipboardDialog from '@/components/terminal-rdp/guac/ClipboardDialog.vue';
|
||
import { TerminalExpose } from '@/components/terminal-rdp/index';
|
||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
|
||
import { useEventListener } from '@vueuse/core';
|
||
import { debounce } from 'lodash';
|
||
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
|
||
import { ElMessage } from 'element-plus';
|
||
|
||
const viewportRef = ref({} as any);
|
||
const displayRef = ref({} as any);
|
||
const clipboardRef = ref({} as any);
|
||
|
||
const props = defineProps({
|
||
machineId: {
|
||
type: Number,
|
||
required: true,
|
||
},
|
||
authCert: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
clipboardList: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(['statusChange']);
|
||
|
||
const state = reactive({
|
||
client: null as any,
|
||
display: null as any,
|
||
displayElm: {} as any,
|
||
clipboard: {} as any,
|
||
keyboard: {} as any,
|
||
mouse: null as any,
|
||
touchpad: null as any,
|
||
errorMessage: '',
|
||
arguments: {},
|
||
status: TerminalStatus.NoConnected,
|
||
size: {
|
||
height: 710,
|
||
width: 1024,
|
||
force: false,
|
||
},
|
||
enableClipboard: true,
|
||
clipboardDialog: {
|
||
visible: false,
|
||
},
|
||
filesystemDialog: {
|
||
visible: false,
|
||
authCertName: '',
|
||
machineId: 0,
|
||
protocol: 1,
|
||
title: '',
|
||
fileId: 0,
|
||
path: '',
|
||
},
|
||
fullscreen: false,
|
||
beforeFullSize: {
|
||
height: 710,
|
||
width: 1024,
|
||
},
|
||
});
|
||
|
||
const installKeyboard = () => {
|
||
state.keyboard = new Guacamole.Keyboard(state.displayElm);
|
||
uninstallKeyboard();
|
||
state.keyboard.onkeydown = (keysym: any) => {
|
||
state.client.sendKeyEvent(1, keysym);
|
||
};
|
||
state.keyboard.onkeyup = (keysym: any) => {
|
||
state.client.sendKeyEvent(0, keysym);
|
||
};
|
||
};
|
||
const uninstallKeyboard = () => {
|
||
state.keyboard!.onkeydown = state.keyboard!.onkeyup = () => {};
|
||
};
|
||
|
||
const installMouse = () => {
|
||
state.mouse = new Guacamole.Mouse(state.displayElm);
|
||
// Hide software cursor when mouse leaves display
|
||
state.mouse.onmouseout = () => {
|
||
if (!state.display) return;
|
||
state.display.showCursor(false);
|
||
};
|
||
state.mouse.onmousedown = state.mouse.onmouseup = state.mouse.onmousemove = handleMouseState;
|
||
};
|
||
|
||
const installTouchpad = () => {
|
||
state.touchpad = new Guacamole.Mouse.Touchpad(state.displayElm);
|
||
|
||
state.touchpad.onmousedown =
|
||
state.touchpad.onmouseup =
|
||
state.touchpad.onmousemove =
|
||
(st: any) => {
|
||
// 记录按下时,光标所在位置
|
||
console.log(st);
|
||
handleMouseState(st, true);
|
||
};
|
||
|
||
// 记录单指按压时候手在屏幕的位置
|
||
state.displayElm.ontouchend = (event: TouchEvent) => {
|
||
console.log('end', event);
|
||
state.displayElm.ontouchend = () => {};
|
||
};
|
||
};
|
||
|
||
const setClipboard = (data: string) => {
|
||
clipboardRef.value.setValue(data);
|
||
};
|
||
|
||
const installClipboard = () => {
|
||
state.enableClipboard = clipboard.install(state.client) as any;
|
||
clipboard.installWatcher(props.clipboardList, setClipboard);
|
||
state.client.onclipboard = clipboard.onClipboard;
|
||
};
|
||
|
||
const installResize = () => {
|
||
// 在resize事件结束后300毫秒执行
|
||
useEventListener('resize', debounce(resize, 300));
|
||
};
|
||
|
||
const installDisplay = () => {
|
||
let { width, height, force } = state.size;
|
||
state.display = state.client.getDisplay();
|
||
const displayElm = displayRef.value;
|
||
displayElm.appendChild(state.display.getElement());
|
||
displayElm.addEventListener('contextmenu', (e: any) => {
|
||
e.stopPropagation();
|
||
if (e.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
e.returnValue = false;
|
||
});
|
||
state.client.connect('width=' + width + '&height=' + height + '&force=' + force);
|
||
window.onunload = () => state.client.disconnect();
|
||
|
||
// allows focusing on the display div so that keyboard doesn't always go to session
|
||
displayElm.onclick = () => {
|
||
displayElm.focus();
|
||
};
|
||
displayElm.onfocus = () => {
|
||
displayElm.className = 'focus';
|
||
};
|
||
displayElm.onblur = () => {
|
||
displayElm.className = '';
|
||
};
|
||
|
||
state.displayElm = displayElm;
|
||
};
|
||
|
||
const installClient = () => {
|
||
let tunnel = new Guacamole.WebSocketTunnel(getMachineRdpSocketUrl(props.authCert)) as any;
|
||
if (state.client) {
|
||
state.display?.scale(0);
|
||
uninstallKeyboard();
|
||
state.client.disconnect();
|
||
}
|
||
|
||
state.client = new Guacamole.Client(tunnel);
|
||
|
||
tunnel.onerror = (status: any) => {
|
||
// eslint-disable-next-line no-console
|
||
console.error(`Tunnel failed ${JSON.stringify(status)}`);
|
||
// state.connectionState = states.TUNNEL_ERROR;
|
||
};
|
||
|
||
tunnel.onstatechange = (st: any) => {
|
||
console.log('statechange', st);
|
||
state.status = st;
|
||
switch (st) {
|
||
case TunnelState.CONNECTING: // 'CONNECTING'
|
||
break;
|
||
case TunnelState.OPEN: // 'OPEN'
|
||
state.status = TerminalStatus.Connected;
|
||
emit('statusChange', TerminalStatus.Connected);
|
||
break;
|
||
case TunnelState.CLOSED: // 'CLOSED'
|
||
state.status = TerminalStatus.Disconnected;
|
||
emit('statusChange', TerminalStatus.Disconnected);
|
||
break;
|
||
case TunnelState.UNSTABLE: // 'UNSTABLE'
|
||
state.status = TerminalStatus.Error;
|
||
emit('statusChange', TerminalStatus.Error);
|
||
break;
|
||
}
|
||
};
|
||
|
||
state.client.onstatechange = (clientState: any) => {
|
||
console.log('clientState', clientState);
|
||
switch (clientState) {
|
||
case ClientState.IDLE:
|
||
console.log('连接空闲');
|
||
break;
|
||
case ClientState.CONNECTING:
|
||
console.log('连接中...');
|
||
break;
|
||
case ClientState.WAITING:
|
||
console.log('等待服务器响应...');
|
||
break;
|
||
case ClientState.CONNECTED:
|
||
console.log('连接成功...');
|
||
break;
|
||
// eslint-disable-next-line no-fallthrough
|
||
case ClientState.DISCONNECTING:
|
||
console.log('断开连接中...');
|
||
break;
|
||
case ClientState.DISCONNECTED:
|
||
console.log('已断开连接...');
|
||
break;
|
||
}
|
||
};
|
||
|
||
state.client.onerror = (error: any) => {
|
||
state.client.disconnect();
|
||
console.error(`Client error ${JSON.stringify(error)}`);
|
||
state.errorMessage = error.message;
|
||
// state.connectionState = states.CLIENT_ERROR;
|
||
};
|
||
|
||
state.client.onsync = () => {};
|
||
|
||
state.client.onargv = (stream: any, mimetype: any, name: any) => {
|
||
if (mimetype !== 'text/plain') return;
|
||
|
||
const reader = new Guacamole.StringReader(stream);
|
||
|
||
// Assemble received data into a single string
|
||
let value = '';
|
||
reader.ontext = (text: any) => {
|
||
value += text;
|
||
};
|
||
|
||
// Test mutability once stream is finished, storing the current value for the argument only if it is mutable
|
||
reader.onend = () => {
|
||
const stream = state.client.createArgumentValueStream('text/plain', name);
|
||
stream.onack = (status: any) => {
|
||
if (status.isError()) {
|
||
// ignore reject
|
||
return;
|
||
}
|
||
state.arguments[name] = value;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
|
||
const resize = () => {
|
||
const elm = viewportRef.value;
|
||
if (!elm || !elm.offsetWidth) {
|
||
// resize is being called on the hidden window
|
||
return;
|
||
}
|
||
|
||
let box = elm.parentElement;
|
||
|
||
state.size.width = box.clientWidth;
|
||
state.size.height = box.clientHeight;
|
||
|
||
const width = parseInt(String(box.clientWidth));
|
||
const height = parseInt(String(box.clientHeight));
|
||
|
||
if (state.display.getWidth() !== width || state.display.getHeight() !== height) {
|
||
if (state.status !== TerminalStatus.Connected) {
|
||
connect(width, height);
|
||
} else {
|
||
state.client.sendSize(width, height);
|
||
}
|
||
}
|
||
// setting timeout so display has time to get the correct size
|
||
// setTimeout(() => {
|
||
// const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
|
||
// state.display.scale(scale);
|
||
// console.log(state.size, scale);
|
||
// }, 100);
|
||
};
|
||
|
||
const handleMouseState = (mouseState: any, showCursor = false) => {
|
||
state.client.getDisplay().showCursor(showCursor);
|
||
|
||
const scaledMouseState = Object.assign({}, mouseState, {
|
||
x: mouseState.x / state.display.getScale(),
|
||
y: mouseState.y / state.display.getScale(),
|
||
});
|
||
state.client.sendMouseState(scaledMouseState);
|
||
};
|
||
|
||
const connect = (width: number, height: number, force = false) => {
|
||
if (!width && !height) {
|
||
if (state.size && state.size.width && state.size.height) {
|
||
width = state.size.width;
|
||
height = state.size.height;
|
||
} else {
|
||
// 获取当前viewportRef宽高
|
||
width = viewportRef.value.clientWidth;
|
||
height = viewportRef.value.clientHeight;
|
||
}
|
||
}
|
||
state.size = { width, height, force };
|
||
|
||
installClient();
|
||
installDisplay();
|
||
installKeyboard();
|
||
installMouse();
|
||
installTouchpad();
|
||
installClipboard();
|
||
installResize();
|
||
};
|
||
|
||
const disconnect = () => {
|
||
uninstallKeyboard();
|
||
state.client?.disconnect();
|
||
};
|
||
|
||
const blur = () => {
|
||
uninstallKeyboard();
|
||
};
|
||
|
||
const focus = () => {};
|
||
|
||
const openPaste = async () => {
|
||
state.clipboardDialog.visible = true;
|
||
};
|
||
|
||
const closePaste = async () => {
|
||
installKeyboard();
|
||
};
|
||
|
||
const onsubmitClipboard = (val: string) => {
|
||
state.clipboardDialog.visible = false;
|
||
installKeyboard();
|
||
clipboard.sendRemoteClipboard(state.client, val);
|
||
};
|
||
|
||
const openFilesystem = async () => {
|
||
state.filesystemDialog.protocol = 2;
|
||
state.filesystemDialog.machineId = props.machineId;
|
||
state.filesystemDialog.authCertName = props.authCert;
|
||
state.filesystemDialog.fileId = props.machineId;
|
||
state.filesystemDialog.path = '/';
|
||
state.filesystemDialog.title = `远程桌面文件管理`;
|
||
state.filesystemDialog.visible = true;
|
||
};
|
||
|
||
const openFullScreen = function () {
|
||
launchIntoFullscreen(viewportRef.value);
|
||
state.fullscreen = true;
|
||
|
||
// 记录原始尺寸
|
||
state.beforeFullSize = {
|
||
width: state.size.width,
|
||
height: state.size.height,
|
||
};
|
||
|
||
// 使用新的宽高重新连接
|
||
setTimeout(() => {
|
||
connect(viewportRef.value.clientWidth, viewportRef.value.clientHeight, false);
|
||
}, 500);
|
||
|
||
watchFullscreenChange(watchFullscreen);
|
||
};
|
||
|
||
function watchFullscreen(event: Event, isFull: boolean) {
|
||
if (!isFull) {
|
||
closeFullScreen();
|
||
}
|
||
}
|
||
|
||
const closeFullScreen = function () {
|
||
exitFullscreen();
|
||
|
||
state.fullscreen = false;
|
||
|
||
// 使用新的宽高重新连接
|
||
setTimeout(() => {
|
||
connect(state.beforeFullSize.width, state.beforeFullSize.height, false);
|
||
}, 500);
|
||
|
||
// 取消注册esc事件,退出全屏
|
||
unWatchFullscreenChange(watchFullscreen);
|
||
};
|
||
|
||
const openSendKeyboard = (keys: string[]) => {
|
||
if (!state.client) {
|
||
return;
|
||
}
|
||
for (let i = 0; i < keys.length; i++) {
|
||
state.client.sendKeyEvent(1, keys[i]);
|
||
}
|
||
for (let j = 0; j < keys.length; j++) {
|
||
state.client.sendKeyEvent(0, keys[j]);
|
||
}
|
||
ElMessage.success('发送组合键成功');
|
||
};
|
||
|
||
const exposes = {
|
||
connect,
|
||
disconnect,
|
||
init: connect,
|
||
close: disconnect,
|
||
fitTerminal: resize,
|
||
focus,
|
||
blur,
|
||
setRemoteClipboard: onsubmitClipboard,
|
||
} as TerminalExpose;
|
||
|
||
defineExpose(exposes);
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.viewport {
|
||
position: relative;
|
||
width: 1024px;
|
||
min-height: 710px;
|
||
z-index: 1;
|
||
}
|
||
.display {
|
||
overflow: hidden;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.btn-box {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 30px;
|
||
padding: 5px 0 5px 10px;
|
||
background: #dddddd4a;
|
||
color: #fff;
|
||
border-radius: 3px;
|
||
}
|
||
</style>
|