🔨 添加 vnc 会话.
This commit is contained in:
@@ -32,15 +32,25 @@
|
||||
<span class="tab-title-icon">
|
||||
<component :is="item.icon" />
|
||||
</span>
|
||||
{{ item.title }}
|
||||
<span>{{ item.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<!-- ssh -->
|
||||
<ssh-view v-if="item.type === TerminalSessionTypes.SSH.type" :item="item" />
|
||||
<ssh-view v-if="item.type === TerminalSessionTypes.SSH.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
<!-- sftp -->
|
||||
<sftp-view v-else-if="item.type === TerminalSessionTypes.SFTP.type" :item="item" />
|
||||
<sftp-view v-else-if="item.type === TerminalSessionTypes.SFTP.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
<!-- rdp -->
|
||||
<rdp-view v-else-if="item.type === TerminalSessionTypes.RDP.type" :item="item" />
|
||||
<rdp-view v-else-if="item.type === TerminalSessionTypes.RDP.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
<!-- vnc -->
|
||||
<vnc-view v-else-if="item.type === TerminalSessionTypes.VNC.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
@@ -60,6 +70,7 @@
|
||||
import SshView from '../view/ssh/ssh-view.vue';
|
||||
import SftpView from '../view/sftp/sftp-view.vue';
|
||||
import RdpView from '../view/rdp/rdp-view.vue';
|
||||
import VncView from '../view/vnc/vnc-view.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
index: number;
|
||||
@@ -118,56 +129,62 @@
|
||||
.terminal-panel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-title-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px 18px 9px 14px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: calc(100% - 3px);
|
||||
height: 2px;
|
||||
background: var(--color);
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-extra {
|
||||
margin-right: 8px;
|
||||
|
||||
.extra-icon {
|
||||
color: var(--color-panel-text-1);
|
||||
transition: 0.2s;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.tab-title-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
padding: 4px 18px 4px 14px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-panel-icon-1);
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: calc(100% - 3px);
|
||||
height: 2px;
|
||||
background: var(--color);
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-extra {
|
||||
margin-right: 8px;
|
||||
|
||||
.extra-icon {
|
||||
color: var(--color-panel-text-1);
|
||||
transition: 0.2s;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-panel-icon-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div v-if="session.state.connectStatus === TerminalStatus.CONNECTED">
|
||||
<!-- 工具栏 -->
|
||||
<a-popover v-model:popup-visible="visible"
|
||||
:title="undefined"
|
||||
trigger="click"
|
||||
:content-class="['guacd-action-bar-popover', direction]"
|
||||
:content-style="{ '--action-count': Math.max(actions.length, direction === ActionBarPosition.RIGHT ? 5 : 6) }"
|
||||
:position="direction === ActionBarPosition.RIGHT ? 'left' : 'bottom'"
|
||||
:show-arrow="false"
|
||||
:auto-fix-position="false">
|
||||
<!-- 触发器 -->
|
||||
<div class="action-bar-trigger" :class="[direction === ActionBarPosition.RIGHT ? 'right' : 'top']" />
|
||||
<!-- 工具内容 -->
|
||||
<template #content>
|
||||
<!-- 按钮 -->
|
||||
<a-space class="action-bar-actions"
|
||||
:direction="direction === ActionBarPosition.RIGHT ? 'vertical' : 'horizontal'"
|
||||
:size="16">
|
||||
<div v-for="action in actions" :key="action.item">
|
||||
<a-tooltip :mini="true"
|
||||
:auto-fix-position="false"
|
||||
:position="direction === ActionBarPosition.RIGHT ? 'left' : 'bottom'"
|
||||
content-class="terminal-tooltip-content"
|
||||
:show-arrow="false"
|
||||
:content="action.content">
|
||||
<a-button class="action-bar-button"
|
||||
:disabled="action.disabled"
|
||||
:type="action.active ? 'primary' : 'secondary'"
|
||||
@click="toggleClickAction(action.item)">
|
||||
<template #icon>
|
||||
<component :is="action.icon" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-space>
|
||||
<!-- 显示设置 -->
|
||||
<div v-if="current === GuacdActionItemKeys.DISPLAY" class="action-bar-content">
|
||||
<!-- 分辨率 -->
|
||||
<a-space>
|
||||
<span class="display-size-label">分辨率</span>
|
||||
<a-select v-model="displaySize"
|
||||
class="display-size-input"
|
||||
placeholder="请选择分辨率"
|
||||
:options="toOptions(screenResolutionKey)"
|
||||
allow-create />
|
||||
</a-space>
|
||||
<!-- 按钮 -->
|
||||
<a-space class="action-bar-content-footer">
|
||||
<a-button type="primary"
|
||||
size="small"
|
||||
@click="fitOnce">
|
||||
临时自适应
|
||||
</a-button>
|
||||
<a-button type="primary"
|
||||
size="small"
|
||||
@click="setDisplaySize">
|
||||
设置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 组合键 -->
|
||||
<div v-else-if="current === GuacdActionItemKeys.COMBINATION_KEY" class="action-bar-content">
|
||||
<a-row :gutter="[12, 12]" wrap>
|
||||
<a-col v-for="item in GuacdCombinationKeyItems"
|
||||
:key="item.name"
|
||||
:span="12"
|
||||
class="combination-key-item"
|
||||
@click="triggerCombinationKey(item.keys)">
|
||||
<span>{{ item.name }}</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<!-- 剪切板 -->
|
||||
<div v-else-if="current === GuacdActionItemKeys.CLIPBOARD" class="action-bar-content">
|
||||
<a-textarea class="action-bar-clipboard"
|
||||
v-model="clipboardData"
|
||||
:ref="setAutoFocus"
|
||||
placeholder="远程剪切板"
|
||||
allow-clear />
|
||||
<!-- 按钮 -->
|
||||
<a-space class="action-bar-content-footer">
|
||||
<a-button size="small" @click="clearClipboardData">
|
||||
清空
|
||||
</a-button>
|
||||
<a-button type="primary"
|
||||
size="small"
|
||||
:disabled="!clipboardData"
|
||||
@click="sendClipboardData">
|
||||
发送
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'vncActionBar'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IVncSession } from '@/views/terminal/interfaces';
|
||||
import {
|
||||
TerminalStatus,
|
||||
GuacdCombinationKeyItems,
|
||||
GuacdActionItemKeys,
|
||||
VncActionBarItems,
|
||||
screenResolutionKey,
|
||||
fitDisplayValue, ActionBarPosition,
|
||||
} from '@/views/terminal/types/const';
|
||||
import { computed, ref, watch, onMounted } from 'vue';
|
||||
import { setAutoFocus } from '@/utils/dom';
|
||||
import { readText } from '@/hooks/copy';
|
||||
import { useTerminalStore, useDictStore } from '@/store';
|
||||
import useGuacdActionBar from '@/views/terminal/types/use-guacd-action-bar';
|
||||
import useVisible from '@/hooks/visible';
|
||||
|
||||
const props = defineProps<{
|
||||
session: IVncSession;
|
||||
direction: string;
|
||||
}>();
|
||||
|
||||
const { preference } = useTerminalStore();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
|
||||
const {
|
||||
displaySize,
|
||||
clipboardData,
|
||||
fitOnce,
|
||||
setDisplaySize,
|
||||
triggerCombinationKey,
|
||||
sendClipboardData,
|
||||
clearClipboardData,
|
||||
disconnect,
|
||||
} = useGuacdActionBar({
|
||||
session: props.session,
|
||||
setVisible,
|
||||
});
|
||||
|
||||
const current = ref('');
|
||||
|
||||
const actions = computed(() => {
|
||||
return VncActionBarItems.filter(item => {
|
||||
return preference.vncActionBarSetting[item.item] !== false;
|
||||
}).map(item => {
|
||||
const key = item.item;
|
||||
return {
|
||||
...item,
|
||||
active: current.value === key,
|
||||
disabled: (key === GuacdActionItemKeys.DISPLAY || GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.CLOSE) ? false : !props.session.isWriteable(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 修改数据值
|
||||
watch(() => visible.value, (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
// 重新触发点击
|
||||
toggleClickAction(current.value);
|
||||
});
|
||||
|
||||
// 触发 action
|
||||
const toggleClickAction = (key: string) => {
|
||||
if (key === GuacdActionItemKeys.DISPLAY) {
|
||||
// 显示设置
|
||||
current.value = GuacdActionItemKeys.DISPLAY;
|
||||
if (props.session.displayHandler?.autoFit) {
|
||||
displaySize.value = fitDisplayValue;
|
||||
} else {
|
||||
displaySize.value = `${props.session.displayHandler?.displayWidth || 0}x${props.session.displayHandler?.displayHeight || 0}`;
|
||||
}
|
||||
} else if (key === GuacdActionItemKeys.COMBINATION_KEY) {
|
||||
// 组合键
|
||||
current.value = GuacdActionItemKeys.COMBINATION_KEY;
|
||||
} else if (key === GuacdActionItemKeys.CLIPBOARD) {
|
||||
// 剪切板
|
||||
current.value = GuacdActionItemKeys.CLIPBOARD;
|
||||
readText(false)
|
||||
.then(s => clipboardData.value = s)
|
||||
.catch(() => clipboardData.value = '');
|
||||
} else if (key === GuacdActionItemKeys.DISCONNECT) {
|
||||
// 断开连接
|
||||
disconnect();
|
||||
} else if (key === GuacdActionItemKeys.CLOSE) {
|
||||
// 关闭工具栏
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置选中
|
||||
onMounted(() => {
|
||||
if (actions.value?.length) {
|
||||
current.value = actions.value[0].item;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="guacd-container">
|
||||
<!-- 状态 -->
|
||||
<guacd-status v-if="session"
|
||||
class="guacd-status-mask"
|
||||
:session="session" />
|
||||
<!-- 工具栏 -->
|
||||
<vnc-action-bar v-if="session"
|
||||
class="guacd-action-bar"
|
||||
:class="[actionBarDirection === 'right' ? 'right' : 'top']"
|
||||
:session="session"
|
||||
:direction="actionBarDirection" />
|
||||
<!-- 视口 -->
|
||||
<div class="guacd-viewport" ref="viewport" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'vnc-view'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalSessionTabItem, IVncSession } from '@/views/terminal/interfaces';
|
||||
import { onMounted, ref, onUnmounted } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { ActionBarPosition } from '@/views/terminal/types/const';
|
||||
import GuacdStatus from '../guacd/guacd-status.vue';
|
||||
import VncActionBar from './vnc-action-bar.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
item: TerminalSessionTabItem;
|
||||
}>();
|
||||
|
||||
const { preference, sessionManager } = useTerminalStore();
|
||||
|
||||
const actionBarDirection = ref(ActionBarPosition.TOP);
|
||||
|
||||
const viewport = ref();
|
||||
const session = ref<IVncSession>();
|
||||
|
||||
// 初始化会话
|
||||
onMounted(async () => {
|
||||
// 工具栏方向
|
||||
actionBarDirection.value = preference.vncActionBarSetting.position || ActionBarPosition.TOP;
|
||||
// 创建终端会话
|
||||
session.value = sessionManager.createSession<IVncSession>(props.item);
|
||||
// 打开终端会话
|
||||
await sessionManager.openVnc(props.item, {
|
||||
viewport: viewport.value,
|
||||
});
|
||||
});
|
||||
|
||||
// 关闭会话
|
||||
onUnmounted(() => {
|
||||
if (props.item.key) {
|
||||
sessionManager.closeSession(props.item.key);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GuacdReactiveSessionStatus, IGuacdChannel, ITerminalSession } from '@/views/terminal/interfaces';
|
||||
import type { ITerminalSession, IGuacdChannel, GuacdReactiveSessionStatus } from '@/views/terminal/interfaces';
|
||||
import type { OutputPayload } from '../../types/protocol';
|
||||
import { InputProtocol, OutputProtocol } from '../../types/protocol';
|
||||
import { TerminalCloseCode, TerminalMessages } from '@/views/terminal/types/const';
|
||||
@@ -147,7 +147,8 @@ export default abstract class BaseGuacdChannel<T extends ITerminalSession<GuacdR
|
||||
if (Date.now() < this.lastSentTime + PING_FREQUENCY) {
|
||||
return;
|
||||
}
|
||||
this.sendInstruction(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', Date.now());
|
||||
// this.sendInstruction(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', Date.now());
|
||||
this.send(InputProtocol.PING);
|
||||
}
|
||||
|
||||
// 发送指令 guacd 内部调用
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TerminalSessionTypes } from '@/views/terminal/types/const';
|
||||
import { getTerminalAccessToken, openTerminalAccessChannel } from '@/api/terminal/terminal';
|
||||
import BaseGuacdChannel from './base-guacd-channel';
|
||||
|
||||
// 终端通信会话 Rdp 会话实现
|
||||
// 终端通信会话 RDP 会话实现
|
||||
export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
|
||||
|
||||
// 打开 channel
|
||||
@@ -20,7 +20,7 @@ export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
|
||||
enableAudioInput: sessionSetting.enableAudioInput,
|
||||
enableAudioOutput: sessionSetting.enableAudioOutput,
|
||||
driveMountMode: sessionSetting.driveMountMode,
|
||||
colorDepth: graphSetting.colorDepth || 16,
|
||||
colorDepth: graphSetting.colorDepth || 24,
|
||||
forceLossless: graphSetting.forceLossless,
|
||||
enableWallpaper: graphSetting.enableWallpaper,
|
||||
enableTheming: graphSetting.enableTheming,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { IVncSession } from '@/views/terminal/interfaces';
|
||||
import type { OutputPayload } from '@/views/terminal/types/protocol';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { TerminalSessionTypes } from '@/views/terminal/types/const';
|
||||
import { getTerminalAccessToken, openTerminalAccessChannel } from '@/api/terminal/terminal';
|
||||
import BaseGuacdChannel from './base-guacd-channel';
|
||||
|
||||
// 终端通信会话 VNC 会话实现
|
||||
export default class VncChannel extends BaseGuacdChannel<IVncSession> {
|
||||
|
||||
// 打开 channel
|
||||
protected async openChannel(): Promise<void> {
|
||||
const setting = useTerminalStore().preference.vncGraphSetting;
|
||||
const { data } = await getTerminalAccessToken({
|
||||
hostId: this.session.info.hostId,
|
||||
connectType: TerminalSessionTypes.VNC.type,
|
||||
extra: {
|
||||
colorDepth: setting.colorDepth || 24,
|
||||
forceLossless: setting.forceLossless,
|
||||
swapRedBlue: setting.swapRedBlue,
|
||||
cursor: setting.cursor,
|
||||
compressLevel: setting.compressLevel,
|
||||
qualityLevel: setting.qualityLevel,
|
||||
}
|
||||
});
|
||||
// 打开 channel
|
||||
this.client = await openTerminalAccessChannel(TerminalSessionTypes.VNC.channel, data);
|
||||
}
|
||||
|
||||
// 处理修改大小
|
||||
processResize({ width, height }: OutputPayload): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { IRdpSession, IRdpSessionClipboardHandler } from '@/views/terminal/interfaces';
|
||||
import type { IGuacdSession, IGuacdSessionClipboardHandler } from '@/views/terminal/interfaces';
|
||||
import Guacamole from 'guacamole-common-js';
|
||||
import { copyToClipboard } from '@/hooks/copy';
|
||||
import { isString } from '@/utils/is';
|
||||
|
||||
// rdp 会话剪切板处理器实现
|
||||
export default class RdpSessionClipboardHandler implements IRdpSessionClipboardHandler {
|
||||
// guacd 会话剪切板处理器实现
|
||||
export default class GuacdSessionClipboardHandler implements IGuacdSessionClipboardHandler {
|
||||
|
||||
private readonly session: IRdpSession;
|
||||
private readonly session: IGuacdSession;
|
||||
|
||||
constructor(session: IRdpSession) {
|
||||
constructor(session: IGuacdSession) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { IRdpSession, IRdpSessionDisplayHandler } from '@/views/terminal/interfaces';
|
||||
import type { IGuacdSession, IGuacdSessionDisplayHandler } from '@/views/terminal/interfaces';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import Guacamole from 'guacamole-common-js';
|
||||
|
||||
// rdp 会话视图处理器实现
|
||||
export default class RdpSessionDisplayHandler implements IRdpSessionDisplayHandler {
|
||||
// guacd 会话视图处理器实现
|
||||
export default class GuacdSessionDisplayHandler implements IGuacdSessionDisplayHandler {
|
||||
|
||||
private readonly session: IRdpSession;
|
||||
private readonly session: IGuacdSession;
|
||||
|
||||
public displayWidth: number;
|
||||
public displayHeight: number;
|
||||
@@ -21,7 +21,7 @@ export default class RdpSessionDisplayHandler implements IRdpSessionDisplayHandl
|
||||
|
||||
private readonly focusSink: () => void;
|
||||
|
||||
constructor(session: IRdpSession) {
|
||||
constructor(session: IGuacdSession) {
|
||||
this.session = session;
|
||||
this.displayWidth = 0;
|
||||
this.displayHeight = 0;
|
||||
@@ -0,0 +1,194 @@
|
||||
import type {
|
||||
GuacdInitConfig,
|
||||
GuacdReactiveSessionStatus,
|
||||
IGuacdChannel,
|
||||
IGuacdSession,
|
||||
IGuacdSessionClipboardHandler,
|
||||
IGuacdSessionDisplayHandler,
|
||||
TerminalSessionTabItem
|
||||
} from '@/views/terminal/interfaces';
|
||||
import type { OutputPayload } from '@/views/terminal/types/protocol';
|
||||
import { InputProtocol } from '@/views/terminal/types/protocol';
|
||||
import { TerminalCloseCode, TerminalMessages } from '@/views/terminal/types/const';
|
||||
import { screenshot } from '@/views/terminal/types/utils';
|
||||
import Guacamole from 'guacamole-common-js';
|
||||
import BaseSession from './base-session';
|
||||
import GuacdSessionClipboardHandler from '../handler/guacd-session-clipboard-handler';
|
||||
|
||||
export const CONNECT_TIMEOUT = 30000;
|
||||
|
||||
// guacd 会话基类
|
||||
export default abstract class BaseGuacdSession extends BaseSession<GuacdReactiveSessionStatus, IGuacdChannel> implements IGuacdSession {
|
||||
|
||||
public config: GuacdInitConfig;
|
||||
|
||||
public client: Guacamole.Client;
|
||||
|
||||
public displayHandler: IGuacdSessionDisplayHandler;
|
||||
|
||||
public clipboardHandler: IGuacdSessionClipboardHandler;
|
||||
|
||||
protected connectTimeoutId?: number;
|
||||
|
||||
protected constructor(item: TerminalSessionTabItem) {
|
||||
super(item, {
|
||||
closeCode: 0,
|
||||
closeMessage: ''
|
||||
});
|
||||
this.client = undefined as unknown as Guacamole.Client;
|
||||
this.config = {} as unknown as GuacdInitConfig;
|
||||
this.displayHandler = undefined as unknown as IGuacdSessionDisplayHandler;
|
||||
this.clipboardHandler = undefined as unknown as IGuacdSessionClipboardHandler;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async init(config: GuacdInitConfig) {
|
||||
this.config = config;
|
||||
// 初始化
|
||||
await this.reInit();
|
||||
}
|
||||
|
||||
// 初始化 channel
|
||||
async reInit(): Promise<void> {
|
||||
// 初始化 channel
|
||||
this.channel = this.createChannel();
|
||||
// 创建 client
|
||||
this.client = new Guacamole.Client(this.channel);
|
||||
// 初始化 display
|
||||
this.displayHandler = this.createDisplay();
|
||||
// 初始化剪切板
|
||||
this.clipboardHandler = this.createClipboard();
|
||||
// 初始化 display
|
||||
this.displayHandler.init();
|
||||
// 初始化 channel
|
||||
await this.channel.init();
|
||||
// 注册 client 事件
|
||||
this.registerClientEvent();
|
||||
}
|
||||
|
||||
// 创建 channel
|
||||
protected abstract createChannel(): IGuacdChannel;
|
||||
|
||||
// 创建 display
|
||||
protected abstract createDisplay(): IGuacdSessionDisplayHandler;
|
||||
|
||||
// 创建 clipboard
|
||||
protected createClipboard() {
|
||||
// 创建 clipboard handler
|
||||
return new GuacdSessionClipboardHandler(this);
|
||||
}
|
||||
|
||||
// 注册 client 事件
|
||||
protected registerClientEvent() {
|
||||
// 错误回调
|
||||
this.client.onerror = (state) => {
|
||||
// 错误回调触发关闭
|
||||
this.channel.closeTunnel(state.code, state.message || TerminalMessages.sessionClosed);
|
||||
};
|
||||
// 状态回调
|
||||
this.client.onstatechange = (state) => {
|
||||
if (state === Guacamole.Client.State.CONNECTED) {
|
||||
// 触发连接成功回调
|
||||
this.onConnected();
|
||||
}
|
||||
};
|
||||
// 剪切板回调
|
||||
this.client.onclipboard = this.clipboardHandler.receiveRemoteClipboardData.bind(this);
|
||||
}
|
||||
|
||||
// 连接会话
|
||||
connect(): void {
|
||||
// 清空超时检查任务
|
||||
window.clearTimeout(this.connectTimeoutId);
|
||||
// 设置连接中
|
||||
super.setConnecting();
|
||||
// 连接 client 其实就是打开 channel 和 display
|
||||
this.client.connect();
|
||||
// 发送 connect 命令
|
||||
this.channel.send(InputProtocol.CONNECT, {
|
||||
body: JSON.stringify({
|
||||
width: this.displayHandler?.displayWidth,
|
||||
height: this.displayHandler?.displayHeight,
|
||||
dpi: this.displayHandler?.displayDpi,
|
||||
})
|
||||
});
|
||||
// 定时检查是否连接成功
|
||||
this.connectTimeoutId = window.setTimeout(() => {
|
||||
// 未连接上证明连接超时
|
||||
if (!this.state.connected) {
|
||||
this.channel.closeTunnel(TerminalCloseCode.CONNECT_TIMEOUT, TerminalMessages.connectTimeout);
|
||||
}
|
||||
}, CONNECT_TIMEOUT);
|
||||
}
|
||||
|
||||
// 连接成功回调
|
||||
protected onConnected() {
|
||||
// 手动触发管道已连接
|
||||
this.channel.processConnected({} as unknown as OutputPayload);
|
||||
}
|
||||
|
||||
// 发送键
|
||||
sendKeys(keys: Array<number>): void {
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.client.sendKeyEvent(1, keys[i]);
|
||||
}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.client.sendKeyEvent(0, keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 粘贴
|
||||
paste(data: string): void {
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
// 发送至远程剪切板
|
||||
this.clipboardHandler?.sendDataToRemoteClipboard(data);
|
||||
// 发送粘贴命令
|
||||
setTimeout(() => {
|
||||
this.sendKeys([65507, 118]);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus(): void {
|
||||
this.displayHandler?.focus?.();
|
||||
}
|
||||
|
||||
// 失焦
|
||||
blur(): void {
|
||||
this.displayHandler?.blur?.();
|
||||
}
|
||||
|
||||
// 自适应
|
||||
fit(): void {
|
||||
this.displayHandler?.fit(false);
|
||||
}
|
||||
|
||||
// 修改大小
|
||||
resize(width: number, height: number): void {
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
// 发送重置大小
|
||||
this.channel.send(InputProtocol.RESIZE, { width, height, });
|
||||
}
|
||||
|
||||
// 截屏
|
||||
async screenshot() {
|
||||
await screenshot(this.client?.getDisplay()?.getElement() as HTMLElement);
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
disconnect(): void {
|
||||
super.disconnect();
|
||||
// 关闭 client
|
||||
this.client?.disconnect();
|
||||
// 关闭超时检查任务
|
||||
clearTimeout(this.connectTimeoutId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +1,49 @@
|
||||
import type {
|
||||
IGuacdChannel,
|
||||
IRdpSession,
|
||||
TerminalSessionTabItem,
|
||||
GuacdInitConfig,
|
||||
IRdpSessionDisplayHandler,
|
||||
GuacdReactiveSessionStatus,
|
||||
IRdpSessionClipboardHandler
|
||||
} from '@/views/terminal/interfaces';
|
||||
import type { OutputPayload } from '@/views/terminal/types/protocol';
|
||||
import type { IGuacdChannel, IGuacdSessionDisplayHandler, IRdpSession, TerminalSessionTabItem } from '@/views/terminal/interfaces';
|
||||
import { InputProtocol } from '@/views/terminal/types/protocol';
|
||||
import { TerminalMessages, fitDisplayValue, TerminalCloseCode } from '@/views/terminal/types/const';
|
||||
import { screenshot } from '@/views/terminal/types/utils';
|
||||
import { fitDisplayValue } from '@/views/terminal/types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import Guacamole from 'guacamole-common-js';
|
||||
import BaseSession from './base-session';
|
||||
import RdpChannel from '../channel/rdp-channel';
|
||||
import RdpSessionDisplayHandler from '../handler/rdp-session-display-handler';
|
||||
import RdpSessionClipboardHandler from '../handler/rdp-session-clipboard-handler';
|
||||
import BaseGuacdSession from './base-guacd-session';
|
||||
import GuacdSessionDisplayHandler from '../handler/guacd-session-display-handler';
|
||||
|
||||
export const AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2';
|
||||
|
||||
export const CONNECT_TIMEOUT = 30000;
|
||||
|
||||
// RDP 会话实现
|
||||
export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus, IGuacdChannel> implements IRdpSession {
|
||||
export default class RdpSession extends BaseGuacdSession implements IRdpSession {
|
||||
|
||||
public fileSystemName: string;
|
||||
|
||||
public config: GuacdInitConfig;
|
||||
|
||||
public client: Guacamole.Client;
|
||||
|
||||
public displayHandler: IRdpSessionDisplayHandler;
|
||||
|
||||
public clipboardHandler: IRdpSessionClipboardHandler;
|
||||
|
||||
private connectTimeoutId?: number;
|
||||
|
||||
constructor(item: TerminalSessionTabItem) {
|
||||
super(item, {
|
||||
closeCode: 0,
|
||||
closeMessage: ''
|
||||
});
|
||||
super(item);
|
||||
this.fileSystemName = 'Shared Driver';
|
||||
this.client = undefined as unknown as Guacamole.Client;
|
||||
this.config = {} as unknown as GuacdInitConfig;
|
||||
this.displayHandler = undefined as unknown as IRdpSessionDisplayHandler;
|
||||
this.clipboardHandler = undefined as unknown as IRdpSessionClipboardHandler;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async init(config: GuacdInitConfig) {
|
||||
this.config = config;
|
||||
// 初始化
|
||||
await this.reInit();
|
||||
// 创建 channel
|
||||
protected createChannel(): IGuacdChannel {
|
||||
return new RdpChannel(this);
|
||||
}
|
||||
|
||||
// 初始化 channel
|
||||
async reInit(): Promise<void> {
|
||||
// 创建 display
|
||||
protected createDisplay(): IGuacdSessionDisplayHandler {
|
||||
const rdpGraphSetting = useTerminalStore().preference.rdpGraphSetting;
|
||||
// 创建 channel
|
||||
this.channel = new RdpChannel(this);
|
||||
// 创建 client
|
||||
this.client = new Guacamole.Client(this.channel);
|
||||
// 创建 display handler
|
||||
this.displayHandler = new RdpSessionDisplayHandler(this);
|
||||
// 创建 clipboard handler
|
||||
this.clipboardHandler = new RdpSessionClipboardHandler(this);
|
||||
// 设置 display autoFit
|
||||
// 创建 display
|
||||
const displayHandler = new GuacdSessionDisplayHandler(this);
|
||||
// 设置自适应
|
||||
const autoFit = rdpGraphSetting?.displaySize === fitDisplayValue;
|
||||
this.displayHandler.autoFit = autoFit;
|
||||
displayHandler.autoFit = autoFit;
|
||||
// 非自适应设置分辨率
|
||||
if (!autoFit) {
|
||||
this.displayHandler.setDisplaySize(rdpGraphSetting?.displayWidth || 1024, rdpGraphSetting?.displayHeight || 768);
|
||||
displayHandler.setDisplaySize(rdpGraphSetting?.displayWidth || 1024, rdpGraphSetting?.displayHeight || 768);
|
||||
}
|
||||
// 初始化 display
|
||||
this.displayHandler.init();
|
||||
// 初始化 channel
|
||||
await this.channel.init();
|
||||
// 注册 client 事件
|
||||
this.registerClientEvent();
|
||||
return displayHandler;
|
||||
}
|
||||
|
||||
// 注册 client 事件
|
||||
private registerClientEvent() {
|
||||
// 错误回调
|
||||
this.client.onerror = (state) => {
|
||||
// 错误回调触发关闭
|
||||
this.channel.closeTunnel(state.code, state.message || TerminalMessages.sessionClosed);
|
||||
};
|
||||
// 状态回调
|
||||
this.client.onstatechange = (state) => {
|
||||
if (state === Guacamole.Client.State.CONNECTED) {
|
||||
// 触发已连接
|
||||
this.onConnected();
|
||||
}
|
||||
};
|
||||
// 剪切板回调
|
||||
this.client.onclipboard = this.clipboardHandler.receiveRemoteClipboardData.bind(this);
|
||||
// 文件系统回调
|
||||
protected registerClientEvent() {
|
||||
super.registerClientEvent();
|
||||
// 注册文件系统回调
|
||||
this.client.onfilesystem = (_, fileSystemName) => {
|
||||
if (fileSystemName) {
|
||||
this.fileSystemName = fileSystemName;
|
||||
@@ -117,35 +62,9 @@ export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus,
|
||||
};
|
||||
}
|
||||
|
||||
// 连接会话
|
||||
connect(): void {
|
||||
// 清空超时检查任务
|
||||
window.clearTimeout(this.connectTimeoutId);
|
||||
// 设置连接中
|
||||
super.setConnecting();
|
||||
// 连接 client 其实就是打开 channel 和 display
|
||||
this.client.connect();
|
||||
// 发送 connect 命令
|
||||
this.channel.send(InputProtocol.CONNECT, {
|
||||
body: JSON.stringify({
|
||||
width: this.displayHandler?.displayWidth,
|
||||
height: this.displayHandler?.displayHeight,
|
||||
dpi: this.displayHandler?.displayDpi,
|
||||
})
|
||||
});
|
||||
// 定时检查是否连接成功
|
||||
this.connectTimeoutId = window.setTimeout(() => {
|
||||
// 未连接上证明连接超时
|
||||
if (!this.state.connected) {
|
||||
this.channel.closeTunnel(TerminalCloseCode.CONNECT_TIMEOUT, TerminalMessages.rdpConnectTimeout);
|
||||
}
|
||||
}, CONNECT_TIMEOUT);
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
private onConnected() {
|
||||
// 手动触发管道已连接
|
||||
this.channel.processConnected({} as unknown as OutputPayload);
|
||||
// 连接成功回调
|
||||
protected onConnected() {
|
||||
super.onConnected();
|
||||
// 监听音频输入
|
||||
if (useTerminalStore().preference.rdpSessionSetting?.enableAudioInput) {
|
||||
const requestAudioStream = (client: Guacamole.Client) => {
|
||||
@@ -173,81 +92,11 @@ export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus,
|
||||
this.channel.send(InputProtocol.RDP_FILE_SYSTEM_EVENT, { event: JSON.stringify(event) });
|
||||
}
|
||||
|
||||
// 发送键
|
||||
sendKeys(keys: Array<number>): void {
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.client.sendKeyEvent(1, keys[i]);
|
||||
}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
this.client.sendKeyEvent(0, keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 粘贴
|
||||
paste(data: string): void {
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
// 发送至远程剪切板
|
||||
this.clipboardHandler?.sendDataToRemoteClipboard(data);
|
||||
// 发送粘贴命令
|
||||
setTimeout(() => {
|
||||
this.sendKeys([65507, 118]);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus(): void {
|
||||
this.displayHandler?.focus?.();
|
||||
}
|
||||
|
||||
// 失焦
|
||||
blur(): void {
|
||||
this.displayHandler?.blur?.();
|
||||
}
|
||||
|
||||
// 自适应
|
||||
fit(): void {
|
||||
this.displayHandler?.fit(false);
|
||||
}
|
||||
|
||||
// 修改大小
|
||||
resize(width: number, height: number): void {
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
// 发送重置大小
|
||||
this.channel.send(InputProtocol.RESIZE, { width, height, });
|
||||
}
|
||||
|
||||
// 截屏
|
||||
async screenshot() {
|
||||
await screenshot(this.client?.getDisplay()?.getElement() as HTMLElement);
|
||||
}
|
||||
|
||||
// 是否可写
|
||||
isWriteable(): boolean {
|
||||
return this.state.connected && this.state.canWrite;
|
||||
}
|
||||
|
||||
// 设置为已关闭
|
||||
setClosed() {
|
||||
// 设置为已关闭
|
||||
super.setClosed();
|
||||
// 断开连接
|
||||
disconnect(): void {
|
||||
super.disconnect();
|
||||
// 关闭文件传输
|
||||
useTerminalStore().transferManager.rdp.closeBySessionKey(this.sessionKey);
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
disconnect(): void {
|
||||
super.disconnect();
|
||||
// 关闭 client
|
||||
this.client?.disconnect();
|
||||
// 关闭超时检查任务
|
||||
clearTimeout(this.connectTimeoutId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { XtermAddons } from '@/types/xterm';
|
||||
import { defaultFontFamily } from '@/types/xterm';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { InputProtocol } from '@/views/terminal/types/protocol';
|
||||
import { TerminalShortcutType } from '@/views/terminal/types/const';
|
||||
import { BACKSPACE_CHAR, CTRL_H_CHAR, TerminalShortcutType } from '@/views/terminal/types/const';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
@@ -127,15 +127,20 @@ export default class SshSession extends BaseSession<ReactiveSessionState, ISshCh
|
||||
|
||||
// 注册事件
|
||||
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
|
||||
// 是否替换退格符
|
||||
const replaceBackspace = preference.sshInteractSetting.replaceBackspace;
|
||||
// 注册输入事件
|
||||
this.inst.onData(s => {
|
||||
if (!this.state.canWrite || !this.state.connected) {
|
||||
this.inst.onData(command => {
|
||||
// 不可写
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
// 替换退格符
|
||||
if (replaceBackspace) {
|
||||
command = command.replace(BACKSPACE_CHAR, CTRL_H_CHAR);
|
||||
}
|
||||
// 输入
|
||||
this.channel.send(InputProtocol.SSH_INPUT, {
|
||||
command: s
|
||||
});
|
||||
this.channel.send(InputProtocol.SSH_INPUT, { command });
|
||||
});
|
||||
// 启用响铃
|
||||
if (preference.sshInteractSetting.enableBell) {
|
||||
@@ -165,7 +170,8 @@ export default class SshSession extends BaseSession<ReactiveSessionState, ISshCh
|
||||
addEventListen(dom, 'contextmenu', async () => {
|
||||
// 右键粘贴逻辑
|
||||
if (preference.sshInteractSetting.rightClickPaste) {
|
||||
if (!this.state.canWrite || !this.state.connected) {
|
||||
// 不可写
|
||||
if (!this.isWriteable()) {
|
||||
return;
|
||||
}
|
||||
// 未开启右键选中 || 开启并无选中的内容则粘贴
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ISshSession,
|
||||
ITerminalSession,
|
||||
ITerminalSessionManager,
|
||||
IVncSession,
|
||||
SshInitConfig,
|
||||
TerminalSessionTabItem
|
||||
} from '@/views/terminal/interfaces';
|
||||
@@ -17,6 +18,7 @@ import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
import SshSession from './ssh-session';
|
||||
import SftpSession from './sftp-session';
|
||||
import RdpSession from './rdp-session';
|
||||
import VncSession from './vnc-session';
|
||||
|
||||
// 终端会话管理器实现
|
||||
export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||
@@ -80,7 +82,23 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||
// 连接会话
|
||||
session.connect();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
// 异常关闭
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 打开 vnc 会话
|
||||
async openVnc(item: TerminalSessionTabItem, config: GuacdInitConfig): Promise<void> {
|
||||
// 获取会话
|
||||
const session: IVncSession = this.getSession(item.key);
|
||||
try {
|
||||
// 初始化 session
|
||||
await session.init(config);
|
||||
// 等待前端渲染完成
|
||||
await sleep(100);
|
||||
// 连接会话
|
||||
session.connect();
|
||||
} catch (ex) {
|
||||
// 异常关闭
|
||||
session.close();
|
||||
}
|
||||
@@ -110,6 +128,9 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||
} else if (item.type === TerminalSessionTypes.RDP.type) {
|
||||
// RDP 会话
|
||||
session = new RdpSession(item);
|
||||
} else if (item.type === TerminalSessionTypes.VNC.type) {
|
||||
// VNC 会话
|
||||
session = new VncSession(item);
|
||||
} else {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { IVncSession, TerminalSessionTabItem, IGuacdSessionDisplayHandler, IGuacdChannel } from '@/views/terminal/interfaces';
|
||||
import { fitDisplayValue } from '@/views/terminal/types/const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import BaseGuacdSession from './base-guacd-session';
|
||||
import VncChannel from '../channel/vnc-channel';
|
||||
import GuacdSessionDisplayHandler from '../handler/guacd-session-display-handler';
|
||||
|
||||
// VNC 会话实现
|
||||
export default class VncSession extends BaseGuacdSession implements IVncSession {
|
||||
|
||||
constructor(item: TerminalSessionTabItem) {
|
||||
super(item);
|
||||
}
|
||||
|
||||
// 创建 channel
|
||||
protected createChannel(): IGuacdChannel {
|
||||
return new VncChannel(this);
|
||||
};
|
||||
|
||||
// 创建 display
|
||||
protected createDisplay(): IGuacdSessionDisplayHandler {
|
||||
const vncGraphSetting = useTerminalStore().preference.vncGraphSetting;
|
||||
// 创建 display
|
||||
const displayHandler = new GuacdSessionDisplayHandler(this);
|
||||
// 设置自适应
|
||||
const autoFit = vncGraphSetting?.displaySize === fitDisplayValue;
|
||||
displayHandler.autoFit = autoFit;
|
||||
// 非自适应设置分辨率
|
||||
if (!autoFit) {
|
||||
displayHandler.setDisplaySize(vncGraphSetting?.displayWidth || 1024, vncGraphSetting?.displayHeight || 768);
|
||||
}
|
||||
return displayHandler;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user