🔨 添加 vnc 会话.
This commit is contained in:
@@ -32,15 +32,25 @@
|
|||||||
<span class="tab-title-icon">
|
<span class="tab-title-icon">
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
</span>
|
</span>
|
||||||
{{ item.title }}
|
<span>{{ item.title }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<!-- ssh -->
|
<!-- 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 -->
|
||||||
<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 -->
|
||||||
<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-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,6 +70,7 @@
|
|||||||
import SshView from '../view/ssh/ssh-view.vue';
|
import SshView from '../view/ssh/ssh-view.vue';
|
||||||
import SftpView from '../view/sftp/sftp-view.vue';
|
import SftpView from '../view/sftp/sftp-view.vue';
|
||||||
import RdpView from '../view/rdp/rdp-view.vue';
|
import RdpView from '../view/rdp/rdp-view.vue';
|
||||||
|
import VncView from '../view/vnc/vnc-view.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
index: number;
|
index: number;
|
||||||
@@ -118,56 +129,62 @@
|
|||||||
.terminal-panel-container {
|
.terminal-panel-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.tab-title-wrapper {
|
.tab-title-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
padding: 4px 18px 4px 14px;
|
||||||
border-radius: 50%;
|
background: var(--bg);
|
||||||
|
position: relative;
|
||||||
|
transition: all .3s;
|
||||||
|
|
||||||
|
.tab-title-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&: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 type { OutputPayload } from '../../types/protocol';
|
||||||
import { InputProtocol, OutputProtocol } from '../../types/protocol';
|
import { InputProtocol, OutputProtocol } from '../../types/protocol';
|
||||||
import { TerminalCloseCode, TerminalMessages } from '@/views/terminal/types/const';
|
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) {
|
if (Date.now() < this.lastSentTime + PING_FREQUENCY) {
|
||||||
return;
|
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 内部调用
|
// 发送指令 guacd 内部调用
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TerminalSessionTypes } from '@/views/terminal/types/const';
|
|||||||
import { getTerminalAccessToken, openTerminalAccessChannel } from '@/api/terminal/terminal';
|
import { getTerminalAccessToken, openTerminalAccessChannel } from '@/api/terminal/terminal';
|
||||||
import BaseGuacdChannel from './base-guacd-channel';
|
import BaseGuacdChannel from './base-guacd-channel';
|
||||||
|
|
||||||
// 终端通信会话 Rdp 会话实现
|
// 终端通信会话 RDP 会话实现
|
||||||
export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
|
export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
|
||||||
|
|
||||||
// 打开 channel
|
// 打开 channel
|
||||||
@@ -20,7 +20,7 @@ export default class RdpChannel extends BaseGuacdChannel<IRdpSession> {
|
|||||||
enableAudioInput: sessionSetting.enableAudioInput,
|
enableAudioInput: sessionSetting.enableAudioInput,
|
||||||
enableAudioOutput: sessionSetting.enableAudioOutput,
|
enableAudioOutput: sessionSetting.enableAudioOutput,
|
||||||
driveMountMode: sessionSetting.driveMountMode,
|
driveMountMode: sessionSetting.driveMountMode,
|
||||||
colorDepth: graphSetting.colorDepth || 16,
|
colorDepth: graphSetting.colorDepth || 24,
|
||||||
forceLossless: graphSetting.forceLossless,
|
forceLossless: graphSetting.forceLossless,
|
||||||
enableWallpaper: graphSetting.enableWallpaper,
|
enableWallpaper: graphSetting.enableWallpaper,
|
||||||
enableTheming: graphSetting.enableTheming,
|
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 Guacamole from 'guacamole-common-js';
|
||||||
import { copyToClipboard } from '@/hooks/copy';
|
import { copyToClipboard } from '@/hooks/copy';
|
||||||
import { isString } from '@/utils/is';
|
import { isString } from '@/utils/is';
|
||||||
|
|
||||||
// rdp 会话剪切板处理器实现
|
// guacd 会话剪切板处理器实现
|
||||||
export default class RdpSessionClipboardHandler implements IRdpSessionClipboardHandler {
|
export default class GuacdSessionClipboardHandler implements IGuacdSessionClipboardHandler {
|
||||||
|
|
||||||
private readonly session: IRdpSession;
|
private readonly session: IGuacdSession;
|
||||||
|
|
||||||
constructor(session: IRdpSession) {
|
constructor(session: IGuacdSession) {
|
||||||
this.session = session;
|
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 { useDebounceFn } from '@vueuse/core';
|
||||||
import Guacamole from 'guacamole-common-js';
|
import Guacamole from 'guacamole-common-js';
|
||||||
|
|
||||||
// rdp 会话视图处理器实现
|
// guacd 会话视图处理器实现
|
||||||
export default class RdpSessionDisplayHandler implements IRdpSessionDisplayHandler {
|
export default class GuacdSessionDisplayHandler implements IGuacdSessionDisplayHandler {
|
||||||
|
|
||||||
private readonly session: IRdpSession;
|
private readonly session: IGuacdSession;
|
||||||
|
|
||||||
public displayWidth: number;
|
public displayWidth: number;
|
||||||
public displayHeight: number;
|
public displayHeight: number;
|
||||||
@@ -21,7 +21,7 @@ export default class RdpSessionDisplayHandler implements IRdpSessionDisplayHandl
|
|||||||
|
|
||||||
private readonly focusSink: () => void;
|
private readonly focusSink: () => void;
|
||||||
|
|
||||||
constructor(session: IRdpSession) {
|
constructor(session: IGuacdSession) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.displayWidth = 0;
|
this.displayWidth = 0;
|
||||||
this.displayHeight = 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 {
|
import type { IGuacdChannel, IGuacdSessionDisplayHandler, IRdpSession, TerminalSessionTabItem } from '@/views/terminal/interfaces';
|
||||||
IGuacdChannel,
|
|
||||||
IRdpSession,
|
|
||||||
TerminalSessionTabItem,
|
|
||||||
GuacdInitConfig,
|
|
||||||
IRdpSessionDisplayHandler,
|
|
||||||
GuacdReactiveSessionStatus,
|
|
||||||
IRdpSessionClipboardHandler
|
|
||||||
} from '@/views/terminal/interfaces';
|
|
||||||
import type { OutputPayload } from '@/views/terminal/types/protocol';
|
|
||||||
import { InputProtocol } from '@/views/terminal/types/protocol';
|
import { InputProtocol } from '@/views/terminal/types/protocol';
|
||||||
import { TerminalMessages, fitDisplayValue, TerminalCloseCode } from '@/views/terminal/types/const';
|
import { fitDisplayValue } from '@/views/terminal/types/const';
|
||||||
import { screenshot } from '@/views/terminal/types/utils';
|
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import Guacamole from 'guacamole-common-js';
|
import Guacamole from 'guacamole-common-js';
|
||||||
import BaseSession from './base-session';
|
|
||||||
import RdpChannel from '../channel/rdp-channel';
|
import RdpChannel from '../channel/rdp-channel';
|
||||||
import RdpSessionDisplayHandler from '../handler/rdp-session-display-handler';
|
import BaseGuacdSession from './base-guacd-session';
|
||||||
import RdpSessionClipboardHandler from '../handler/rdp-session-clipboard-handler';
|
import GuacdSessionDisplayHandler from '../handler/guacd-session-display-handler';
|
||||||
|
|
||||||
export const AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2';
|
export const AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2';
|
||||||
|
|
||||||
export const CONNECT_TIMEOUT = 30000;
|
|
||||||
|
|
||||||
// RDP 会话实现
|
// RDP 会话实现
|
||||||
export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus, IGuacdChannel> implements IRdpSession {
|
export default class RdpSession extends BaseGuacdSession implements IRdpSession {
|
||||||
|
|
||||||
public fileSystemName: string;
|
public fileSystemName: string;
|
||||||
|
|
||||||
public config: GuacdInitConfig;
|
|
||||||
|
|
||||||
public client: Guacamole.Client;
|
|
||||||
|
|
||||||
public displayHandler: IRdpSessionDisplayHandler;
|
|
||||||
|
|
||||||
public clipboardHandler: IRdpSessionClipboardHandler;
|
|
||||||
|
|
||||||
private connectTimeoutId?: number;
|
|
||||||
|
|
||||||
constructor(item: TerminalSessionTabItem) {
|
constructor(item: TerminalSessionTabItem) {
|
||||||
super(item, {
|
super(item);
|
||||||
closeCode: 0,
|
|
||||||
closeMessage: ''
|
|
||||||
});
|
|
||||||
this.fileSystemName = 'Shared Driver';
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 创建 channel
|
||||||
async init(config: GuacdInitConfig) {
|
protected createChannel(): IGuacdChannel {
|
||||||
this.config = config;
|
return new RdpChannel(this);
|
||||||
// 初始化
|
|
||||||
await this.reInit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 channel
|
// 创建 display
|
||||||
async reInit(): Promise<void> {
|
protected createDisplay(): IGuacdSessionDisplayHandler {
|
||||||
const rdpGraphSetting = useTerminalStore().preference.rdpGraphSetting;
|
const rdpGraphSetting = useTerminalStore().preference.rdpGraphSetting;
|
||||||
// 创建 channel
|
// 创建 display
|
||||||
this.channel = new RdpChannel(this);
|
const displayHandler = new GuacdSessionDisplayHandler(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
|
|
||||||
const autoFit = rdpGraphSetting?.displaySize === fitDisplayValue;
|
const autoFit = rdpGraphSetting?.displaySize === fitDisplayValue;
|
||||||
this.displayHandler.autoFit = autoFit;
|
displayHandler.autoFit = autoFit;
|
||||||
|
// 非自适应设置分辨率
|
||||||
if (!autoFit) {
|
if (!autoFit) {
|
||||||
this.displayHandler.setDisplaySize(rdpGraphSetting?.displayWidth || 1024, rdpGraphSetting?.displayHeight || 768);
|
displayHandler.setDisplaySize(rdpGraphSetting?.displayWidth || 1024, rdpGraphSetting?.displayHeight || 768);
|
||||||
}
|
}
|
||||||
// 初始化 display
|
return displayHandler;
|
||||||
this.displayHandler.init();
|
|
||||||
// 初始化 channel
|
|
||||||
await this.channel.init();
|
|
||||||
// 注册 client 事件
|
|
||||||
this.registerClientEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册 client 事件
|
// 注册 client 事件
|
||||||
private registerClientEvent() {
|
protected registerClientEvent() {
|
||||||
// 错误回调
|
super.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);
|
|
||||||
// 文件系统回调
|
|
||||||
this.client.onfilesystem = (_, fileSystemName) => {
|
this.client.onfilesystem = (_, fileSystemName) => {
|
||||||
if (fileSystemName) {
|
if (fileSystemName) {
|
||||||
this.fileSystemName = fileSystemName;
|
this.fileSystemName = fileSystemName;
|
||||||
@@ -117,35 +62,9 @@ export default class RdpSession extends BaseSession<GuacdReactiveSessionStatus,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接会话
|
// 连接成功回调
|
||||||
connect(): void {
|
protected onConnected() {
|
||||||
// 清空超时检查任务
|
super.onConnected();
|
||||||
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);
|
|
||||||
// 监听音频输入
|
// 监听音频输入
|
||||||
if (useTerminalStore().preference.rdpSessionSetting?.enableAudioInput) {
|
if (useTerminalStore().preference.rdpSessionSetting?.enableAudioInput) {
|
||||||
const requestAudioStream = (client: Guacamole.Client) => {
|
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) });
|
this.channel.send(InputProtocol.RDP_FILE_SYSTEM_EVENT, { event: JSON.stringify(event) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送键
|
// 断开连接
|
||||||
sendKeys(keys: Array<number>): void {
|
disconnect(): void {
|
||||||
if (!this.isWriteable()) {
|
super.disconnect();
|
||||||
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();
|
|
||||||
// 关闭文件传输
|
// 关闭文件传输
|
||||||
useTerminalStore().transferManager.rdp.closeBySessionKey(this.sessionKey);
|
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 { defaultFontFamily } from '@/types/xterm';
|
||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { InputProtocol } from '@/views/terminal/types/protocol';
|
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 { Terminal } from '@xterm/xterm';
|
||||||
import { FitAddon } from '@xterm/addon-fit';
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
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>) {
|
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
|
||||||
|
// 是否替换退格符
|
||||||
|
const replaceBackspace = preference.sshInteractSetting.replaceBackspace;
|
||||||
// 注册输入事件
|
// 注册输入事件
|
||||||
this.inst.onData(s => {
|
this.inst.onData(command => {
|
||||||
if (!this.state.canWrite || !this.state.connected) {
|
// 不可写
|
||||||
|
if (!this.isWriteable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 替换退格符
|
||||||
|
if (replaceBackspace) {
|
||||||
|
command = command.replace(BACKSPACE_CHAR, CTRL_H_CHAR);
|
||||||
|
}
|
||||||
// 输入
|
// 输入
|
||||||
this.channel.send(InputProtocol.SSH_INPUT, {
|
this.channel.send(InputProtocol.SSH_INPUT, { command });
|
||||||
command: s
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
// 启用响铃
|
// 启用响铃
|
||||||
if (preference.sshInteractSetting.enableBell) {
|
if (preference.sshInteractSetting.enableBell) {
|
||||||
@@ -165,7 +170,8 @@ export default class SshSession extends BaseSession<ReactiveSessionState, ISshCh
|
|||||||
addEventListen(dom, 'contextmenu', async () => {
|
addEventListen(dom, 'contextmenu', async () => {
|
||||||
// 右键粘贴逻辑
|
// 右键粘贴逻辑
|
||||||
if (preference.sshInteractSetting.rightClickPaste) {
|
if (preference.sshInteractSetting.rightClickPaste) {
|
||||||
if (!this.state.canWrite || !this.state.connected) {
|
// 不可写
|
||||||
|
if (!this.isWriteable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 未开启右键选中 || 开启并无选中的内容则粘贴
|
// 未开启右键选中 || 开启并无选中的内容则粘贴
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
ISshSession,
|
ISshSession,
|
||||||
ITerminalSession,
|
ITerminalSession,
|
||||||
ITerminalSessionManager,
|
ITerminalSessionManager,
|
||||||
|
IVncSession,
|
||||||
SshInitConfig,
|
SshInitConfig,
|
||||||
TerminalSessionTabItem
|
TerminalSessionTabItem
|
||||||
} from '@/views/terminal/interfaces';
|
} from '@/views/terminal/interfaces';
|
||||||
@@ -17,6 +18,7 @@ import { addEventListen, removeEventListen } from '@/utils/event';
|
|||||||
import SshSession from './ssh-session';
|
import SshSession from './ssh-session';
|
||||||
import SftpSession from './sftp-session';
|
import SftpSession from './sftp-session';
|
||||||
import RdpSession from './rdp-session';
|
import RdpSession from './rdp-session';
|
||||||
|
import VncSession from './vnc-session';
|
||||||
|
|
||||||
// 终端会话管理器实现
|
// 终端会话管理器实现
|
||||||
export default class TerminalSessionManager implements ITerminalSessionManager {
|
export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||||
@@ -80,7 +82,23 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
|||||||
// 连接会话
|
// 连接会话
|
||||||
session.connect();
|
session.connect();
|
||||||
} catch (ex) {
|
} 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();
|
session.close();
|
||||||
}
|
}
|
||||||
@@ -110,6 +128,9 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
|||||||
} else if (item.type === TerminalSessionTypes.RDP.type) {
|
} else if (item.type === TerminalSessionTypes.RDP.type) {
|
||||||
// RDP 会话
|
// RDP 会话
|
||||||
session = new RdpSession(item);
|
session = new RdpSession(item);
|
||||||
|
} else if (item.type === TerminalSessionTypes.VNC.type) {
|
||||||
|
// VNC 会话
|
||||||
|
session = new VncSession(item);
|
||||||
} else {
|
} else {
|
||||||
return undefined as unknown as T;
|
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