🔨 添加 vnc 会话.

This commit is contained in:
lijiahangmax
2025-07-07 14:47:18 +08:00
parent 4643c37a5a
commit 1abc47bb56
16 changed files with 466 additions and 447 deletions

View File

@@ -619,5 +619,189 @@ body[terminal-theme='dark'] .arco-modal-container {
font-size: 16px;
margin: 0 8px 0 4px;
}
}
// guacd 容器
.guacd-container {
// guacd 视口
.guacd-viewport {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
:deep(> div) {
position: relative;
z-index: 8;
}
}
// guacd 状态遮罩
.guacd-status-mask {
width: 100%;
height: 100%;
position: absolute;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
// guacd 工具栏
.guacd-action-bar {
position: absolute;
cursor: pointer;
z-index: 9998;
&.top {
top: 4px;
left: 50%;
transform: translateX(-50%);
}
&.right {
right: 4px;
top: 50%;
transform: translateY(-50%);
}
// 工具栏触发器
.action-bar-trigger {
display: flex;
border-radius: 8px;
transition: .3s all;
background: var(--color-bg-rdp-toolbar);
filter: contrast(50%) brightness(50%);
&.top {
width: 240px;
height: 8px;
&:hover {
transform: translateY(2px);
}
}
&.right {
width: 8px;
height: 228px;
&:hover {
transform: translateX(-2px);
}
}
&:hover {
background: var(--color-bg-rdp-toolbar-hover);
}
}
}
}
// guacd 工具栏
@guacd-action-size: 42px;
.guacd-action-bar-popover {
--actions-width: calc(var(--action-count) * @guacd-action-size + (var(--action-count) - 1) * 16px);
background: var(--color-bg-2);
.arco-popover-content {
margin-top: 0;
display: flex;
}
.action-bar-button {
width: @guacd-action-size !important;
height: @guacd-action-size !important;
font-size: 20px;
}
.action-bar-content {
display: flex;
flex-direction: column;
}
.action-bar-content-footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.display-size-label {
padding-right: 6px;
user-select: none;
text-align: end;
}
.display-size-input {
width: 198px;
}
.action-bar-upload, .action-bar-clipboard {
display: flex;
}
.combination-key-item {
span {
display: block;
padding: 6px 12px;
cursor: pointer;
background: var(--color-fill-1);
border-radius: 2px;
user-select: none;
transition: 0.2s ALL;
&:hover {
background: var(--color-fill-2);
}
}
}
}
.guacd-action-bar-popover.top {
.arco-popover-content {
flex-direction: column;
width: var(--actions-width);
}
.action-bar-content {
margin-top: 16px;
max-height: 224px;
overflow-x: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
.action-bar-upload, .action-bar-clipboard {
height: 186px;
}
}
.guacd-action-bar-popover.right {
.arco-popover-content {
flex-direction: row-reverse;
height: var(--actions-width);
}
.action-bar-content {
margin-right: 16px;
width: 344px;
overflow-x: hidden;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
.action-bar-upload, .action-bar-clipboard {
height: calc(var(--actions-width) - 40px);
}
}

View File

@@ -8,7 +8,9 @@ import type {
TerminalSshDisplaySetting,
TerminalSshInteractSetting,
TerminalSshPluginsSetting,
TerminalState
TerminalState,
TerminalVncActionBarSetting,
TerminalVncGraphSetting
} from './types';
import type {
IDomViewportHandler,
@@ -56,8 +58,12 @@ export const TerminalPreferenceItem = {
RDP_GRAPH_SETTING: 'rdpGraphSetting',
// rdp 操作栏设置
RDP_ACTION_BAR_SETTING: 'rdpActionBarSetting',
// 会话设置
// rdp 会话设置
RDP_SESSION_SETTING: 'rdpSessionSetting',
// vnc 图形化设置
VNC_GRAPH_SETTING: 'vncGraphSetting',
// vnc 工具栏设置
VNC_ACTION_BAR_SETTING: 'vncActionBarSetting',
// 快捷键设置
SHORTCUT_SETTING: 'shortcutSetting',
};
@@ -77,6 +83,8 @@ export default defineStore('terminal', {
rdpGraphSetting: {} as TerminalRdpGraphSetting,
rdpSessionSetting: {} as TerminalRdpSessionSetting,
rdpActionBarSetting: {} as TerminalRdpActionBarSetting,
vncGraphSetting: {} as TerminalVncGraphSetting,
vncActionBarSetting: {} as TerminalVncActionBarSetting,
shortcutSetting: {
enabled: false,
keys: []

View File

@@ -23,6 +23,8 @@ export interface TerminalPreference {
rdpGraphSetting: TerminalRdpGraphSetting;
rdpActionBarSetting: TerminalRdpActionBarSetting;
rdpSessionSetting: TerminalRdpSessionSetting;
vncGraphSetting: TerminalVncGraphSetting;
vncActionBarSetting: TerminalVncActionBarSetting;
shortcutSetting: TerminalShortcutSetting;
}
@@ -68,6 +70,7 @@ export interface TerminalSshInteractSetting {
wordSeparator: string;
terminalEmulationType: string;
scrollBackLine: number;
replaceBackspace: boolean;
}
// RDP 图形化设置
@@ -111,6 +114,31 @@ export interface TerminalRdpSessionSetting {
driveMountMode?: string;
}
// VNC 图形化设置
export interface TerminalVncGraphSetting {
displaySize?: string;
displayWidth?: number;
displayHeight?: number;
colorDepth?: number;
forceLossless?: boolean;
swapRedBlue?: boolean;
cursor?: string;
compressLevel?: number;
qualityLevel?: number;
}
// VNC 操作栏设置
export interface TerminalVncActionBarSetting {
position?: string;
display?: boolean;
combinationKey?: boolean;
clipboard?: boolean;
disconnect?: boolean;
close?: boolean;
[key: string]: unknown;
}
// 终端快捷键设置
export interface TerminalShortcutSetting {
enabled: boolean;

View File

@@ -31,45 +31,21 @@
<icon-right />
</a-button>
</a-tooltip>
<!-- 打开 SSH -->
<a-tooltip v-if="handler.host?.types?.includes(TerminalSessionTypes.SSH.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 SSH">
<a-button class="combined-handler-action icon-button"
@click="openSession(handler.host as any, TerminalSessionTypes.SSH)">
<icon-thunderbolt />
</a-button>
</a-tooltip>
<!-- 打开 SFTP -->
<a-tooltip v-if="handler.host?.types?.includes(TerminalSessionTypes.SSH.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 SFTP">
<a-button class="combined-handler-action icon-button"
@click="openSession(handler.host as any, TerminalSessionTypes.SFTP)">
<icon-folder />
</a-button>
</a-tooltip>
<!-- 打开 RDP -->
<a-tooltip v-if="handler.host?.types?.includes(TerminalSessionTypes.RDP.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 RDP">
<a-button class="combined-handler-action icon-button"
@click="openSession(handler.host as any, TerminalSessionTypes.RDP)">
<icon-computer />
</a-button>
</a-tooltip>
<!-- 打开会话 -->
<template v-for="type in TerminalSessionTypes">
<template v-if="handler.host?.types?.includes(type.protocol)">
<a-tooltip position="top"
:mini="true"
:auto-fix-position="false"
:content="`打开 ${type.type}`"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content">
<a-button class="combined-handler-action icon-button" @click="openSession(handler.host as any, type)">
<component :is="type.connectIcon || type.icon" />
</a-button>
</a-tooltip>
</template>
</template>
</div>
</div>
</div>

View File

@@ -40,45 +40,21 @@
</div>
<!-- 操作 -->
<div class="host-item-actions">
<!-- 打开 SSH -->
<a-tooltip v-if="item.types?.includes(TerminalSessionTypes.SSH.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 SSH">
<a-button class="host-item-action icon-button"
@click="clickHost(item, TerminalSessionTypes.SSH)">
<icon-thunderbolt />
</a-button>
</a-tooltip>
<!-- 打开 SFTP -->
<a-tooltip v-if="item.types?.includes(TerminalSessionTypes.SSH.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 SFTP">
<a-button class="host-item-action icon-button"
@click="clickHost(item, TerminalSessionTypes.SFTP)">
<icon-folder />
</a-button>
</a-tooltip>
<!-- 打开 RDP -->
<a-tooltip v-if="item.types?.includes(TerminalSessionTypes.RDP.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 RDP">
<a-button class="host-item-action icon-button"
@click="clickHost(item, TerminalSessionTypes.RDP)">
<icon-computer />
</a-button>
</a-tooltip>
<!-- 打开会话 -->
<template v-for="type in TerminalSessionTypes">
<template v-if="item.types?.includes(type.protocol)">
<a-tooltip position="top"
:mini="true"
:auto-fix-position="false"
:content="`打开 ${type.type}`"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content">
<a-button class="host-item-action icon-button" @click="openHostSession(item, type)">
<component :is="type.connectIcon || type.icon" />
</a-button>
</a-tooltip>
</template>
</template>
</div>
</div>
</a-list-item>
@@ -144,7 +120,7 @@
defineExpose({ open });
// 打开终端
const clickHost = (item: HostQueryResponse, tab: TerminalSessionType) => {
const openHostSession = (item: HostQueryResponse, tab: TerminalSessionType) => {
openSession(item, tab, panelIndex.value);
setVisible(false);
};

View File

@@ -111,48 +111,23 @@
</div>
<!-- 操作 -->
<div class="host-item-right-actions">
<!-- 打开 SSH -->
<a-tooltip v-if="item.types?.includes(TerminalSessionTypes.SSH.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 SSH">
<div class="terminal-sidebar-icon-wrapper">
<a-button class="terminal-sidebar-icon" @click="openSession(item, TerminalSessionTypes.SSH)">
<icon-thunderbolt />
</a-button>
</div>
</a-tooltip>
<!-- 打开 SFTP -->
<a-tooltip v-if="item.types?.includes(TerminalSessionTypes.SSH.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 SFTP">
<div class="terminal-sidebar-icon-wrapper">
<a-button class="terminal-sidebar-icon" @click="openSession(item, TerminalSessionTypes.SFTP)">
<icon-folder />
</a-button>
</div>
</a-tooltip>
<!-- 打开 RDP -->
<a-tooltip v-if="item.types?.includes(TerminalSessionTypes.RDP.type)"
position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="打开 RDP">
<div class="terminal-sidebar-icon-wrapper">
<a-button class="terminal-sidebar-icon" @click="openSession(item, TerminalSessionTypes.RDP)">
<icon-computer />
</a-button>
</div>
</a-tooltip>
<!-- 打开会话 -->
<template v-for="type in TerminalSessionTypes">
<template v-if="item.types?.includes(type.protocol)">
<a-tooltip position="top"
:mini="true"
:auto-fix-position="false"
:content="`打开 ${type.type}`"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content">
<div class="terminal-sidebar-icon-wrapper">
<a-button class="terminal-sidebar-icon" @click="openSession(item, type)">
<component :is="type.connectIcon || type.icon" />
</a-button>
</div>
</a-tooltip>
</template>
</template>
<!-- 主机设置 -->
<a-tooltip position="top"
:mini="true"

View File

@@ -68,7 +68,7 @@
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { onBeforeMount, ref } from 'vue';
import { ref, onMounted } from 'vue';
import { NewConnectionType, newConnectionTypeKey } from '../../types/const';
import { useDictStore, useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
@@ -86,7 +86,7 @@
const filterOptions = ref<Array<SelectOptionData>>([]);
// 初始化过滤器项
onBeforeMount(() => {
onMounted(() => {
filterOptions.value = getAuthorizedHostOptions(hosts.hostList);
});

View File

@@ -6,7 +6,7 @@
dot />
<!-- 会话关闭 -->
<a-card v-if="session.state.connectStatus === TerminalStatus.CLOSED"
class="rdp-status-wrapper"
class="status-wrapper"
title="会话已关闭">
<!-- 错误信息 -->
<a-descriptions size="large"
@@ -51,12 +51,12 @@
<script lang="ts">
export default {
name: 'rdpStatus'
name: 'guacdStatus'
};
</script>
<script lang="ts" setup>
import type { IRdpSession } from '@/views/terminal/interfaces';
import type { IGuacdSession } from '@/views/terminal/interfaces';
import { TerminalStatus, TerminalCloseCode, TerminalMessages } from '@/views/terminal/types/const';
import { copy } from '@/hooks/copy';
import { dateFormat } from '@/utils';
@@ -64,7 +64,7 @@
import useVisible from '@/hooks/visible';
const props = defineProps<{
session: IRdpSession;
session: IGuacdSession;
}>();
const { visible, setVisible } = useVisible(true);
@@ -73,7 +73,7 @@
</script>
<style lang="less" scoped>
.rdp-status-wrapper {
.status-wrapper {
width: 520px;
min-height: 180px;
border-radius: 8px;

View File

@@ -4,27 +4,27 @@
<a-popover v-model:popup-visible="visible"
:title="undefined"
trigger="click"
:content-class="['tool-bar-popover', direction]"
:content-style="{ '--action-count': Math.max(actions.length, 6) }"
:position="direction === 'right' ? 'left' : 'bottom'"
: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="tool-bar" :class="[direction === 'right' ? 'right' : 'top']" />
<div class="action-bar-trigger" :class="[direction === ActionBarPosition.RIGHT ? 'right' : 'top']" />
<!-- 工具内容 -->
<template #content>
<!-- 按钮 -->
<a-space class="tool-bar-actions"
:direction="direction === 'right' ? 'vertical' : 'horizontal'"
<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 === 'right' ? 'left' : 'bottom'"
:position="direction === ActionBarPosition.RIGHT ? 'left' : 'bottom'"
content-class="terminal-tooltip-content"
:show-arrow="false"
:content="action.content">
<a-button class="tool-bar-button"
<a-button class="action-bar-button"
:disabled="action.disabled"
:type="action.active ? 'primary' : 'secondary'"
@click="toggleClickAction(action.item)">
@@ -36,7 +36,7 @@
</div>
</a-space>
<!-- 显示设置 -->
<div v-if="current === RdpActionItemKeys.DISPLAY" class="tool-bar-content">
<div v-if="current === GuacdActionItemKeys.DISPLAY" class="action-bar-content">
<!-- 分辨率 -->
<a-space>
<span class="display-size-label">分辨率</span>
@@ -47,7 +47,7 @@
allow-create />
</a-space>
<!-- 按钮 -->
<a-space class="tool-bar-content-footer">
<a-space class="action-bar-content-footer">
<a-button type="primary"
size="small"
@click="fitOnce">
@@ -61,9 +61,9 @@
</a-space>
</div>
<!-- 组合键 -->
<div v-else-if="current === RdpActionItemKeys.COMBINATION_KEY" class="tool-bar-content">
<div v-else-if="current === GuacdActionItemKeys.COMBINATION_KEY" class="action-bar-content">
<a-row :gutter="[12, 12]" wrap>
<a-col v-for="item in RdpCombinationKeyItems"
<a-col v-for="item in GuacdCombinationKeyItems"
:key="item.name"
:span="12"
class="combination-key-item"
@@ -73,14 +73,14 @@
</a-row>
</div>
<!-- 剪切板 -->
<div v-else-if="current === RdpActionItemKeys.CLIPBOARD" class="tool-bar-content">
<a-textarea class="tool-bar-clipboard"
<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="tool-bar-content-footer">
<a-space class="action-bar-content-footer">
<a-button size="small" @click="clearClipboardData">
清空
</a-button>
@@ -93,8 +93,8 @@
</a-space>
</div>
<!-- 文件上传 -->
<div v-else-if="current === RdpActionItemKeys.UPLOAD" class="tool-bar-content">
<a-upload class="tool-bar-upload"
<div v-else-if="current === GuacdActionItemKeys.UPLOAD" class="action-bar-content">
<a-upload class="action-bar-upload"
v-model:file-list="fileList"
:auto-upload="false"
:show-file-list="false"
@@ -102,7 +102,7 @@
:tip="fileList.length ? fileList[0]?.name : '选择文件后会自动上传至驱动目录'"
@change="onSelectFile" />
<!-- 按钮 -->
<a-space class="tool-bar-content-footer">
<a-space class="action-bar-content-footer">
<a-button size="small" @click="clearUploadFile">
清空
</a-button>
@@ -130,18 +130,18 @@
import type { IRdpSession } from '@/views/terminal/interfaces';
import {
TerminalStatus,
RdpCombinationKeyItems,
RdpActionItemKeys,
GuacdCombinationKeyItems,
GuacdActionItemKeys,
RdpActionBarItems,
screenResolutionKey,
fitDisplayValue
fitDisplayValue, ActionBarPosition
} from '@/views/terminal/types/const';
import { computed, ref, watch, onMounted } from 'vue';
import { setAutoFocus } from '@/utils/dom';
import { saveAs } from 'file-saver';
import { readText } from '@/hooks/copy';
import { useTerminalStore, useDictStore } from '@/store';
import { getDisplaySize } from '@/views/terminal/types/utils';
import useGuacdActionBar from '@/views/terminal/types/use-guacd-action-bar';
import useVisible from '@/hooks/visible';
const props = defineProps<{
@@ -153,9 +153,21 @@
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 displaySize = ref(fitDisplayValue);
const clipboardData = ref('');
const fileList = ref<FileItem[]>([]);
const actions = computed(() => {
@@ -166,7 +178,7 @@
return {
...item,
active: current.value === key,
disabled: (key === RdpActionItemKeys.DISPLAY || key === RdpActionItemKeys.SAVE_RDP || RdpActionItemKeys.DISCONNECT || key === RdpActionItemKeys.CLOSE) ? false : !props.session.isWriteable(),
disabled: (key === GuacdActionItemKeys.DISPLAY || key === GuacdActionItemKeys.SAVE_RDP || GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.CLOSE) ? false : !props.session.isWriteable(),
};
});
});
@@ -182,70 +194,39 @@
// 触发 action
const toggleClickAction = (key: string) => {
if (key === RdpActionItemKeys.DISPLAY) {
if (key === GuacdActionItemKeys.DISPLAY) {
// 显示设置
current.value = RdpActionItemKeys.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 === RdpActionItemKeys.COMBINATION_KEY) {
} else if (key === GuacdActionItemKeys.COMBINATION_KEY) {
// 组合键
current.value = RdpActionItemKeys.COMBINATION_KEY;
} else if (key === RdpActionItemKeys.CLIPBOARD) {
current.value = GuacdActionItemKeys.COMBINATION_KEY;
} else if (key === GuacdActionItemKeys.CLIPBOARD) {
// 剪切板
current.value = RdpActionItemKeys.CLIPBOARD;
current.value = GuacdActionItemKeys.CLIPBOARD;
readText(false)
.then(s => clipboardData.value = s)
.catch(() => clipboardData.value = '');
} else if (key === RdpActionItemKeys.UPLOAD) {
} else if (key === GuacdActionItemKeys.UPLOAD) {
// 文件上传
current.value = RdpActionItemKeys.UPLOAD;
current.value = GuacdActionItemKeys.UPLOAD;
fileList.value = [];
} else if (key === RdpActionItemKeys.SAVE_RDP) {
} else if (key === GuacdActionItemKeys.SAVE_RDP) {
// 保存 rdp 文件
saveRdpFile();
} else if (key === RdpActionItemKeys.DISCONNECT) {
} else if (key === GuacdActionItemKeys.DISCONNECT) {
// 断开连接
disconnect();
} else if (key === RdpActionItemKeys.CLOSE) {
} else if (key === GuacdActionItemKeys.CLOSE) {
// 关闭工具栏
setVisible(false);
}
};
// 临时自适应
const fitOnce = () => {
props.session.displayHandler?.fit(true);
setVisible(false);
};
// 设置显示大小
const setDisplaySize = () => {
const displayHandler = props.session.displayHandler;
if (!displayHandler) {
return;
}
if (displaySize.value === fitDisplayValue) {
// 设置自适应
displayHandler.autoFit = true;
displayHandler.fit(true);
} else {
try {
// 获取大小
const [width, height] = getDisplaySize(displaySize.value, true);
// 取消自适应
displayHandler.autoFit = false;
// 设置大小
displayHandler.resize(width, height);
} catch (e) {
return;
}
}
setVisible(false);
};
// 保存 rdp 文件
const saveRdpFile = () => {
const address = props.session.info.address;
@@ -255,24 +236,6 @@
saveAs(new Blob([content], { type: 'text/plain;charset=utf-8' }), `${address}.rdp`);
};
// 触发组合键
const triggerCombinationKey = (keys: Array<number>) => {
props.session.sendKeys(keys);
setVisible(false);
};
// 发送剪切板数据
const sendClipboardData = () => {
// 粘贴
props.session.paste(clipboardData.value);
setVisible(false);
};
// 清空剪切板数据
const clearClipboardData = () => {
clipboardData.value = '';
};
// 上传文件
const uploadFile = () => {
const file = fileList.value[0].file as File;
@@ -293,12 +256,6 @@
fileList.value = [];
};
// 关闭会话
const disconnect = () => {
props.session.disconnect();
setVisible(false);
};
// 设置选中
onMounted(() => {
if (actions.value?.length) {
@@ -309,127 +266,4 @@
</script>
<style lang="less" scoped>
.tool-bar {
display: flex;
border-radius: 8px;
transition: .3s all;
background: var(--color-bg-rdp-toolbar);
filter: contrast(50%) brightness(50%);
&.top {
width: 240px;
height: 8px;
&:hover {
transform: translateY(2px);
}
}
&.right {
width: 8px;
height: 228px;
&:hover {
transform: translateX(-2px);
}
}
&:hover {
background: var(--color-bg-rdp-toolbar-hover);
}
}
</style>
<style lang="less">
@action-size: 42px;
.tool-bar-popover.top {
.arco-popover-content {
flex-direction: column;
width: var(--actions-width);
}
.tool-bar-content {
margin-top: 16px;
max-height: var(--actions-width);
}
.tool-bar-upload, .tool-bar-clipboard {
height: 186px;
}
}
.tool-bar-popover.right {
.arco-popover-content {
flex-direction: row-reverse;
max-height: var(--actions-width);
}
.tool-bar-content {
margin-right: 16px;
width: var(--actions-width);
}
.tool-bar-upload, .tool-bar-clipboard {
height: calc(var(--actions-width) - 40px);
}
}
.tool-bar-popover {
--actions-width: calc(var(--action-count) * @action-size + (var(--action-count) - 1) * 16px);
background: var(--color-bg-2);
.arco-popover-content {
margin-top: 0;
display: flex;
}
.tool-bar-button {
width: @action-size !important;
height: @action-size !important;
font-size: 20px;
}
.tool-bar-content {
display: flex;
flex-direction: column;
}
.tool-bar-content-footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
}
.display-size-label {
padding-right: 6px;
user-select: none;
text-align: end;
}
.display-size-input {
width: 198px;
}
.tool-bar-upload, .tool-bar-clipboard {
display: flex;
}
.combination-key-item {
span {
display: block;
padding: 6px 12px;
cursor: pointer;
background: var(--color-fill-1);
border-radius: 2px;
user-select: none;
transition: 0.2s ALL;
&:hover {
background: var(--color-fill-2);
}
}
}
}
</style>

View File

@@ -1,17 +1,17 @@
<template>
<div class="rdp-container">
<div class="guacd-container">
<!-- 状态 -->
<rdp-status v-if="session"
class="rdp-status-mask"
:session="session" />
<guacd-status v-if="session"
class="guacd-status-mask"
:session="session" />
<!-- 工具栏 -->
<rdp-action-bar v-if="session"
class="rdp-action-bar"
:class="[toolbarDirection === 'right' ? 'right' : 'top']"
class="guacd-action-bar"
:class="[actionBarDirection === 'right' ? 'right' : 'top']"
:session="session"
:direction="toolbarDirection" />
<!-- rdp 视口 -->
<div class="rdp-viewport" ref="viewport" />
:direction="actionBarDirection" />
<!-- 视口 -->
<div class="guacd-viewport" ref="viewport" />
</div>
</template>
@@ -25,8 +25,9 @@
import type { TerminalSessionTabItem, IRdpSession } from '@/views/terminal/interfaces';
import { onMounted, ref, onUnmounted } from 'vue';
import { useTerminalStore } from '@/store';
import RdpStatus from './rdp-status.vue';
import { ActionBarPosition } from '@/views/terminal/types/const';
import RdpActionBar from './rdp-action-bar.vue';
import GuacdStatus from '../guacd/guacd-status.vue';
const props = defineProps<{
item: TerminalSessionTabItem;
@@ -34,7 +35,7 @@
const { preference, sessionManager } = useTerminalStore();
const toolbarDirection = ref('top');
const actionBarDirection = ref(ActionBarPosition.TOP);
const viewport = ref();
const session = ref<IRdpSession>();
@@ -42,7 +43,7 @@
// 初始化会话
onMounted(async () => {
// 工具栏方向
toolbarDirection.value = preference.rdpActionBarSetting.position || 'top';
actionBarDirection.value = preference.rdpActionBarSetting.position || ActionBarPosition.TOP;
// 创建终端会话
session.value = sessionManager.createSession<IRdpSession>(props.item);
// 打开终端会话
@@ -60,53 +61,5 @@
</script>
<style scoped lang="less">
.rdp-container {
width: 100%;
height: 100%;
position: relative;
}
.rdp-viewport {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
:deep(> div) {
position: relative;
z-index: 8;
}
}
.rdp-status-mask {
width: 100%;
height: 100%;
position: absolute;
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.rdp-action-bar {
position: absolute;
cursor: pointer;
z-index: 9998;
&.top {
top: 4px;
left: 50%;
transform: translateX(-50%);
}
&.right {
right: 4px;
top: 50%;
transform: translateY(-50%);
}
}
<style lang="less" scoped>
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="sftp-container">
<div>
<a-split class="split-view"
v-model:size="splitSize"
:min="0.3"
@@ -69,7 +69,7 @@
</script>
<script lang="ts" setup>
import type { ISftpSession, SftpFile, TerminalSessionTabItem } from '@/views/terminal/interfaces';
import type { ISftpSession, ISftpSessionHandler, SftpFile, TerminalSessionTabItem } from '@/views/terminal/interfaces';
import { onMounted, onUnmounted, ref } from 'vue';
import { useTerminalStore } from '@/store';
import { Message } from '@arco-design/web-vue';
@@ -297,8 +297,7 @@
}
// 创建终端会话
session.value = sessionManager.createSession<ISftpSession>(props.item);
// 打开终端会话
await sessionManager.openSftp(props.item, {
const handler: ISftpSessionHandler = {
setLoading: setTableLoading,
connectCallback,
onClose,
@@ -311,7 +310,8 @@
resolveDownloadFlatDirectory,
resolveSftpGetContent,
resolveSftpSetContent,
});
};
await sessionManager.openSftp(props.item, handler);
});
// 关闭会话
@@ -326,15 +326,9 @@
<style lang="less" scoped>
@sftp-table-header-height: 32px + 8px;
.sftp-container {
.split-view {
width: 100%;
height: 100%;
position: relative;
.split-view {
width: 100%;
height: 100%;
}
}
.sftp-table-container, .sftp-editor-container {

View File

@@ -1,5 +1,5 @@
<template>
<div class="ssh-container">
<div>
<!-- 头部 -->
<ssh-header :session="session" @handle="doTerminalHandle" />
<!-- 终端右键菜单 -->
@@ -94,12 +94,6 @@
<style lang="less" scoped>
@ssh-header-height: 36px;
.ssh-container {
width: 100%;
height: 100%;
position: relative;
}
.ssh-wrapper {
width: 100%;
height: calc(100% - @ssh-header-height);

View File

@@ -79,8 +79,8 @@ export interface ISftpSessionHandler {
resolveSftpSetContent: (result: string, msg: string, token: string) => void;
}
// rdp 会话视图处理器定义
export interface IRdpSessionDisplayHandler {
// guacd 会话视图处理器定义
export interface IGuacdSessionDisplayHandler {
displayWidth: number;
displayHeight: number;
displayDpi: number;
@@ -101,8 +101,8 @@ export interface IRdpSessionDisplayHandler {
setDisplaySize: (width: number, height: number) => void;
}
// rdp 会话剪切板处理器定义
export interface IRdpSessionClipboardHandler {
// guacd 会话剪切板处理器定义
export interface IGuacdSessionClipboardHandler {
// 发送数据到远程剪切板
sendDataToRemoteClipboard: (data: string | File | Blob) => void;
// 接收远程剪切板数据

View File

@@ -3,8 +3,8 @@ import type { Terminal } from '@xterm/xterm';
import type { ISearchOptions } from '@xterm/addon-search';
import type Guacamole from 'guacamole-common-js';
import type {
IRdpSessionClipboardHandler,
IRdpSessionDisplayHandler,
IGuacdSessionClipboardHandler,
IGuacdSessionDisplayHandler,
ISftpSessionHandler,
ISshSessionHandler,
TerminalSessionTabItem
@@ -108,6 +108,9 @@ export interface ITerminalSession<State extends ReactiveSessionState = ReactiveS
setConnected: () => void;
// 设置已关闭
setClosed: () => void;
// 是否可写
isWriteable: () => boolean;
}
// SSH 会话定义
@@ -160,29 +163,32 @@ export interface ISftpSession extends ITerminalSession {
export interface IGuacdSession extends ITerminalSession<GuacdReactiveSessionStatus>, IDomViewportHandler {
// guacd 客户端
client: Guacamole.Client;
// FIXME VNC 可以再抽象
// 会话配置
config: GuacdInitConfig;
// 视图处理器
displayHandler: IGuacdSessionDisplayHandler;
// 剪切板处理器
clipboardHandler: IGuacdSessionClipboardHandler;
// 初始化
init: (config: GuacdInitConfig) => Promise<void>;
// 发送键
sendKeys: (keys: Array<number>) => void;
// 粘贴
paste: (data: string) => void;
}
// RDP 会话定义
export interface IRdpSession extends IGuacdSession {
fileSystemName: string;
// 会话配置
config: GuacdInitConfig;
// 视图处理器
displayHandler: IRdpSessionDisplayHandler;
// 剪切板处理器
clipboardHandler: IRdpSessionClipboardHandler;
// 初始化
init: (config: GuacdInitConfig) => Promise<void>;
// 文件系统事件
onFileSystemEvent: (event: Record<string, any>) => void;
// 发送键
sendKeys: (keys: Array<number>) => void;
// 粘贴
paste: (data: string) => void;
// 是否可写
isWriteable: () => boolean;
}
// VNC 会话定义
export interface IVncSession extends IGuacdSession {
}
// sftp 文件
@@ -210,6 +216,8 @@ export interface ITerminalSessionManager {
openSftp: (item: TerminalSessionTabItem, handler: ISftpSessionHandler) => Promise<void>;
// 打开 rdp 会话
openRdp: (item: TerminalSessionTabItem, config: GuacdInitConfig) => Promise<void>;
// 打开 vnc 会话
openVnc: (item: TerminalSessionTabItem, config: GuacdInitConfig) => Promise<void>;
// 重新打开会话
reOpenSession: (sessionKey: string) => Promise<void>;
// 创建终端会话

View File

@@ -1,6 +1,6 @@
import type { Reactive } from 'vue';
import { reactive } from 'vue';
import type { ITerminalChannel, ITerminalSession, ReactiveSessionState, SessionHostInfo, TerminalSessionTabItem } from '@/views/terminal/interfaces';
import type { ITerminalChannel, ITerminalSession, ReactiveSessionState, TerminalSessionTabItem, SessionHostInfo } from '@/views/terminal/interfaces';
import { TerminalStatus } from '@/views/terminal/types/const';
// 会话基类
@@ -86,4 +86,9 @@ export default abstract class BaseSession<State extends ReactiveSessionState, Ch
this.state.connectStatus = TerminalStatus.CLOSED;
}
// 是否可写
isWriteable(): boolean {
return this.state.connected && this.state.canWrite;
}
}

View File

@@ -0,0 +1,84 @@
import { ref } from 'vue';
import type { IGuacdSession } from '../interfaces';
import { fitDisplayValue } from './const';
import { getDisplaySize } from './utils';
// guacd 工具栏配置
export interface UseGuacdActionBarOptions {
session: IGuacdSession;
setVisible: (visible: boolean) => void;
}
// 使用主机配置表单
export default function useGuacdActionBar(options: UseGuacdActionBarOptions) {
const { session, setVisible } = options;
const displaySize = ref(fitDisplayValue);
const clipboardData = ref('');
// 临时自适应
const fitOnce = () => {
session.displayHandler?.fit(true);
setVisible(false);
};
// 设置显示大小
const setDisplaySize = () => {
const displayHandler = session.displayHandler;
if (!displayHandler) {
return;
}
if (displaySize.value === fitDisplayValue) {
// 设置自适应
displayHandler.autoFit = true;
displayHandler.fit(true);
} else {
try {
// 获取大小
const [width, height] = getDisplaySize(displaySize.value, true);
// 取消自适应
displayHandler.autoFit = false;
// 设置大小
displayHandler.resize(width, height);
} catch (e) {
return;
}
}
setVisible(false);
};
// 触发组合键
const triggerCombinationKey = (keys: Array<number>) => {
session.sendKeys(keys);
setVisible(false);
};
// 发送剪切板数据
const sendClipboardData = () => {
// 粘贴
session.paste(clipboardData.value);
setVisible(false);
};
// 清空剪切板数据
const clearClipboardData = () => {
clipboardData.value = '';
};
// 关闭会话
const disconnect = () => {
session.disconnect();
setVisible(false);
};
return {
displaySize,
clipboardData,
fitOnce,
setDisplaySize,
triggerCombinationKey,
sendClipboardData,
clearClipboardData,
disconnect,
};
}