🔨 优化会话工具栏.

This commit is contained in:
lijiahangmax
2025-07-09 00:19:17 +08:00
parent 0224e2f19a
commit abada92907
9 changed files with 403 additions and 280 deletions

View File

@@ -110,7 +110,10 @@ public class SessionStores {
}
}
// 超时时间
session.timeout(config.getTimeout());
Integer timeout = config.getTimeout();
if (timeout != null) {
session.timeout(timeout);
}
return session;
}

View File

@@ -71,7 +71,7 @@ public class HostSshConfigDTO implements GenericsDataModel, UpdatePasswordAction
private Long keyId;
@NotNull
@Min(value = 1)
@Min(value = 0)
@Max(value = 100000)
@Schema(description = "连接超时时间")
private Integer connectTimeout;

View File

@@ -0,0 +1,61 @@
<template>
<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>
</template>
<script lang="ts">
export default {
name: 'clipboardAction'
};
</script>
<script lang="ts" setup>
import type { IGuacdSession } from '@/views/terminal/interfaces';
import { ref, onMounted } from 'vue';
import { setAutoFocus } from '@/utils/dom';
import { readText } from '@/hooks/copy';
const props = defineProps<{
session: IGuacdSession;
}>();
const emits = defineEmits(['close']);
const clipboardData = ref('');
// 发送剪切板数据
const sendClipboardData = () => {
props.session.paste(clipboardData.value);
emits('close');
};
// 清空剪切板数据
const clearClipboardData = () => {
clipboardData.value = '';
};
// 初始化
onMounted(() => {
readText(false)
.then(s => clipboardData.value = s)
.catch(() => clipboardData.value = '');
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template>
<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>
</template>
<script lang="ts">
export default {
name: 'combinationKeyAction'
};
</script>
<script lang="ts" setup>
import type { IGuacdSession } from '@/views/terminal/interfaces';
import { GuacdCombinationKeyItems } from '@/views/terminal/types/const';
const props = defineProps<{
session: IGuacdSession;
}>();
const emits = defineEmits(['close']);
// 触发组合键
const triggerCombinationKey = (keys: Array<number>) => {
props.session.sendKeys(keys);
emits('close');
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,91 @@
<template>
<!-- 分辨率 -->
<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>
</template>
<script lang="ts">
export default {
name: 'displayAction'
};
</script>
<script lang="ts" setup>
import type { IGuacdSession } from '@/views/terminal/interfaces';
import { ref, onMounted } from 'vue';
import { useDictStore } from '@/store';
import { getDisplaySize } from '@/views/terminal/types/utils';
import { screenResolutionKey, fitDisplayValue } from '@/views/terminal/types/const';
const { toOptions, getDictValue } = useDictStore();
const props = defineProps<{
session: IGuacdSession;
}>();
const emits = defineEmits(['close']);
const displaySize = ref(fitDisplayValue);
// 临时自适应
const fitOnce = () => {
props.session.displayHandler?.fit(true);
emits('close');
};
// 设置显示大小
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;
}
}
emits('close');
};
// 初始化
onMounted(() => {
if (props.session.displayHandler?.autoFit) {
displaySize.value = fitDisplayValue;
} else {
displaySize.value = `${props.session.displayHandler?.displayWidth || 0}x${props.session.displayHandler?.displayHeight || 0}`;
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,48 @@
<template>
<a-descriptions size="large"
:label-style="{ display: 'flex', width: '48px' }"
:column="1">
<!-- 主机id -->
<a-descriptions-item label="主机id">
{{ session.info.hostId }}
</a-descriptions-item>
<!-- 主机名称 -->
<a-descriptions-item label="主机名称">
{{ session.info.name }}
</a-descriptions-item>
<!-- 主机地址 -->
<a-descriptions-item label="主机地址">
<span class="text-copy" @click="copy(session.info.address, true)">
{{ session.info.address }}
</span>
</a-descriptions-item>
<!-- 主机端口 -->
<a-descriptions-item label="主机端口">
{{ session.info.port }}
</a-descriptions-item>
<!-- 用户名 -->
<a-descriptions-item v-if="session.info.username" label="用户名">
{{ session.info.username }}
</a-descriptions-item>
</a-descriptions>
</template>
<script lang="ts">
export default {
name: 'infoAction'
};
</script>
<script lang="ts" setup>
import type { IGuacdSession } from '@/views/terminal/interfaces';
import { copy } from '@/hooks/copy';
const props = defineProps<{
session: IGuacdSession;
}>();
</script>
<style lang="less" scoped>
</style>

View File

@@ -25,9 +25,9 @@
:show-arrow="false"
:content="action.content">
<a-button class="action-bar-button"
:disabled="action.disabled"
:disabled="!action.enabled()"
:type="action.active ? 'primary' : 'secondary'"
@click="toggleClickAction(action.item)">
@click="toggleAction(action.item)">
<template #icon>
<component :is="action.icon" />
</template>
@@ -35,65 +35,28 @@
</a-tooltip>
</div>
</a-space>
<!-- 连接信息 -->
<div v-if="current === GuacdActionItemKeys.INFO" class="action-bar-content">
<info-action :session="session" />
</div>
<!-- 显示设置 -->
<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 v-else-if="current === GuacdActionItemKeys.DISPLAY" class="action-bar-content">
<display-action :session="session" @close="close" />
</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>
<combination-key-action :session="session" @close="close" />
</div>
<!-- 触发键 -->
<div v-else-if="current === GuacdActionItemKeys.TRIGGER_KEY" class="action-bar-content">
<trigger-key-action :session="session" />
</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>
<clipboard-action :session="session" @close="close" />
</div>
<!-- 文件上传 -->
<div v-else-if="current === GuacdActionItemKeys.UPLOAD" class="action-bar-content">
<!-- RDP 文件上传 -->
<div v-else-if="current === GuacdActionItemKeys.RDP_UPLOAD" class="action-bar-content">
<a-upload class="action-bar-upload"
v-model:file-list="fileList"
:auto-upload="false"
@@ -116,6 +79,8 @@
</div>
</template>
</a-popover>
<!-- sftp 上传框 -->
<sftp-upload-modal ref="sftpUploadModalRef" :session="session" />
</div>
</template>
@@ -130,44 +95,31 @@
import type { IRdpSession } from '@/views/terminal/interfaces';
import {
TerminalStatus,
GuacdCombinationKeyItems,
GuacdActionItemKeys,
RdpActionBarItems,
screenResolutionKey,
fitDisplayValue, ActionBarPosition
ActionBarPosition, TerminalSessionTypes
} 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 useGuacdActionBar from '@/views/terminal/types/use-guacd-action-bar';
import { useTerminalStore } from '@/store';
import useVisible from '@/hooks/visible';
import InfoAction from '../guacd/actions/info-action.vue';
import DisplayAction from '../guacd/actions/display-action.vue';
import TriggerKeyAction from '../guacd/actions/trigger-key-action.vue';
import ClipboardAction from '../guacd/actions/clipboard-action.vue';
import CombinationKeyAction from '../guacd/actions/combination-key-action.vue';
import SftpUploadModal from '@/views/terminal/components/view/sftp/sftp-upload-modal.vue';
const props = defineProps<{
session: IRdpSession;
direction: string;
}>();
const { preference, transferManager } = useTerminalStore();
const { toOptions, getDictValue } = useDictStore();
const { hosts, preference, openSession, reOpenSession, transferManager } = useTerminalStore();
const { visible, setVisible } = useVisible();
const {
displaySize,
clipboardData,
fitOnce,
setDisplaySize,
triggerCombinationKey,
sendClipboardData,
clearClipboardData,
disconnect,
} = useGuacdActionBar({
session: props.session,
setVisible,
});
const current = ref('');
const sftpUploadModalRef = ref();
const fileList = ref<FileItem[]>([]);
const actions = computed(() => {
@@ -178,7 +130,17 @@
return {
...item,
active: current.value === key,
disabled: (key === GuacdActionItemKeys.DISPLAY || key === GuacdActionItemKeys.SAVE_RDP || GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.CLOSE) ? false : !props.session.isWriteable(),
enabled: () => {
if (key === GuacdActionItemKeys.DISPLAY || key === GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.RECONNECT || key === GuacdActionItemKeys.SAVE_RDP || key === GuacdActionItemKeys.CLOSE) {
return true;
} else if (key === GuacdActionItemKeys.OPEN_SFTP || key === GuacdActionItemKeys.SFTP_UPLOAD) {
// 支持 SFTP 协议
if (!hosts.hostList.find(s => s.id === props.session.info.hostId)?.types?.includes?.(TerminalSessionTypes.SFTP.protocol)) {
return false;
}
}
return props.session.isWriteable();
}
};
});
});
@@ -189,31 +151,22 @@
return;
}
// 重新触发点击
toggleClickAction(current.value);
toggleAction(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.UPLOAD) {
// 文件上传
current.value = GuacdActionItemKeys.UPLOAD;
const toggleAction = (key: string) => {
if (key === GuacdActionItemKeys.OPEN_SFTP) {
// 打开 SFTP 会话
openSession(hosts.hostList.find(s => s.id === props.session.info.hostId) as any, TerminalSessionTypes.SFTP);
setVisible(false);
} else if (key === GuacdActionItemKeys.SFTP_UPLOAD) {
// 打开 SFTP 上传
sftpUploadModalRef.value.open('/');
setVisible(false);
} else if (key === GuacdActionItemKeys.RDP_UPLOAD) {
// RDP 文件上传
current.value = GuacdActionItemKeys.RDP_UPLOAD;
fileList.value = [];
} else if (key === GuacdActionItemKeys.SAVE_RDP) {
// 保存 rdp 文件
@@ -221,9 +174,14 @@
} else if (key === GuacdActionItemKeys.DISCONNECT) {
// 断开连接
disconnect();
} else if (key === GuacdActionItemKeys.RECONNECT) {
// 重新连接
reconnect();
} else if (key === GuacdActionItemKeys.CLOSE) {
// 关闭工具栏
setVisible(false);
} else {
current.value = key;
}
};
@@ -256,6 +214,29 @@
fileList.value = [];
};
// 关闭会话
const disconnect = () => {
props.session.disconnect();
setVisible(false);
};
// 关闭会话
const reconnect = () => {
const session = props.session;
// 断开连接
session.disconnect();
// 重新连接
if (session.state.canReconnect) {
reOpenSession(session.sessionKey);
}
setVisible(false);
};
// 关闭
const close = () => {
setVisible(false);
};
// 设置选中
onMounted(() => {
if (actions.value?.length) {

View File

@@ -25,9 +25,9 @@
:show-arrow="false"
:content="action.content">
<a-button class="action-bar-button"
:disabled="action.disabled"
:disabled="!action.enabled()"
:type="action.active ? 'primary' : 'secondary'"
@click="toggleClickAction(action.item)">
@click="toggleAction(action.item)">
<template #icon>
<component :is="action.icon" />
</template>
@@ -35,65 +35,35 @@
</a-tooltip>
</div>
</a-space>
<!-- 连接信息 -->
<div v-if="current === GuacdActionItemKeys.INFO" class="action-bar-content">
<info-action :session="session" />
</div>
<!-- 显示设置 -->
<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 v-else-if="current === GuacdActionItemKeys.DISPLAY" class="action-bar-content">
<display-action ref="display"
:session="session"
@close="close" />
</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>
<combination-key-action :session="session"
@close="close" />
</div>
<!-- 触发键 -->
<div v-else-if="current === GuacdActionItemKeys.TRIGGER_KEY" class="action-bar-content">
<trigger-key-action :session="session" />
</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>
<clipboard-action ref="clipboard"
:session="session"
@close="close" />
</div>
</template>
</a-popover>
<!-- sftp 上传框 -->
<sftp-upload-modal ref="sftpUploadModalRef" :session="session" />
</div>
</template>
@@ -107,43 +77,31 @@
import type { IVncSession } from '@/views/terminal/interfaces';
import {
TerminalStatus,
GuacdCombinationKeyItems,
GuacdActionItemKeys,
VncActionBarItems,
screenResolutionKey,
fitDisplayValue, ActionBarPosition,
ActionBarPosition,
TerminalSessionTypes,
} 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 { useTerminalStore } from '@/store';
import useVisible from '@/hooks/visible';
import InfoAction from '../guacd/actions/info-action.vue';
import DisplayAction from '../guacd/actions/display-action.vue';
import ClipboardAction from '../guacd/actions/clipboard-action.vue';
import TriggerKeyAction from '../guacd/actions/trigger-key-action.vue';
import CombinationKeyAction from '../guacd/actions/combination-key-action.vue';
import SftpUploadModal from '@/views/terminal/components/view/sftp/sftp-upload-modal.vue';
const props = defineProps<{
session: IVncSession;
direction: string;
}>();
const { preference } = useTerminalStore();
const { toOptions, getDictValue } = useDictStore();
const { hosts, preference, openSession, reOpenSession } = useTerminalStore();
const { visible, setVisible } = useVisible();
const {
displaySize,
clipboardData,
fitOnce,
setDisplaySize,
triggerCombinationKey,
sendClipboardData,
clearClipboardData,
disconnect,
} = useGuacdActionBar({
session: props.session,
setVisible,
});
const current = ref('');
const sftpUploadModalRef = ref();
const actions = computed(() => {
return VncActionBarItems.filter(item => {
@@ -153,7 +111,16 @@
return {
...item,
active: current.value === key,
disabled: (key === GuacdActionItemKeys.DISPLAY || GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.CLOSE) ? false : !props.session.isWriteable(),
enabled: () => {
if (key === GuacdActionItemKeys.DISPLAY || key === GuacdActionItemKeys.DISCONNECT || key === GuacdActionItemKeys.RECONNECT || key === GuacdActionItemKeys.CLOSE) {
return true;
} else if (key === GuacdActionItemKeys.OPEN_SFTP || key === GuacdActionItemKeys.SFTP_UPLOAD) {
if (!hosts.hostList.find(s => s.id === props.session.info.hostId)?.types?.includes?.(TerminalSessionTypes.SFTP.protocol)) {
return false;
}
}
return props.session.isWriteable();
}
};
});
});
@@ -163,38 +130,57 @@
if (!val) {
return;
}
// 重新触发点击
toggleClickAction(current.value);
// 重新触发
toggleAction(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 = '');
const toggleAction = (key: string) => {
if (key === GuacdActionItemKeys.OPEN_SFTP) {
// 打开 SFTP 会话
openSession(hosts.hostList.find(s => s.id === props.session.info.hostId) as any, TerminalSessionTypes.SFTP);
setVisible(false);
} else if (key === GuacdActionItemKeys.SFTP_UPLOAD) {
// 打开 SFTP 上传
sftpUploadModalRef.value.open('/');
setVisible(false);
} else if (key === GuacdActionItemKeys.DISCONNECT) {
// 断开连接
disconnect();
} else if (key === GuacdActionItemKeys.RECONNECT) {
// 重新连接
reconnect();
} else if (key === GuacdActionItemKeys.CLOSE) {
// 关闭工具栏
setVisible(false);
} else {
current.value = key;
}
};
// 关闭会话
const disconnect = () => {
props.session.disconnect();
setVisible(false);
};
// 关闭会话
const reconnect = () => {
const session = props.session;
// 断开连接
session.disconnect();
// 重新连接
if (session.state.canReconnect) {
reOpenSession(session.sessionKey);
}
setVisible(false);
};
// 关闭
const close = () => {
setVisible(false);
};
// 设置选中
onMounted(() => {
if (actions.value?.length) {

View File

@@ -1,84 +0,0 @@
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,
};
}