feat: 终端头部功能.

This commit is contained in:
lijiahang
2024-01-08 19:27:44 +08:00
parent 918ce861d3
commit d1fb64a050
9 changed files with 228 additions and 71 deletions

View File

@@ -217,12 +217,20 @@ body {
margin-bottom: 16px;
}
.copy-left {
.copy-left, .copy-right {
color: rgb(var(--arcoblue-6));
cursor: pointer;
margin-right: 4px;
}
.copy-left {
margin-right: 4px;
}
.copy-right {
margin-left: 4px;
}
.span-blue {
color: rgb(var(--arcoblue-6));
}

View File

@@ -2,7 +2,8 @@ import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
export default function useCopy() {
const { isSupported, copy: c, text, copied } = useClipboard();
const { copy: c } = useClipboard();
// 复制
const copy = async (value: string | undefined, tips = `${value} 已复制`) => {
try {
if (!value) {
@@ -16,10 +17,12 @@ export default function useCopy() {
Message.error('复制失败');
}
};
// 获取剪切板内容
const readText = () => {
return navigator.clipboard.readText();
};
return {
isSupported,
copy,
text,
copied
readText,
};
}

View File

@@ -163,7 +163,7 @@ export const resetObject = (obj: any, ignore: string[] = []) => {
export const objectTruthKeyCount = (obj: any, ignore: string[] = []) => {
return Object.keys(obj)
.filter(s => !ignore.includes(s))
.reduce(function (acc, curr) {
.reduce(function(acc, curr) {
const currVal = obj[curr];
return acc + ~~(currVal !== undefined && currVal !== null && currVal?.length !== 0 && currVal !== '');
}, 0);
@@ -192,24 +192,37 @@ export function detectZoom() {
return ratio;
}
/**
* 获取页面路由
*/
export function getRoute(url = location.href) {
return url.substring(url.lastIndexOf('#') + 1).split('?')[0];
}
/**
* 获取唯一的 UUID
*/
export function getUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 调整颜色
* @param color color
* @param range 正数越浅 负数越深
*/
export function adjustColor(color: string, range: number) {
let newColor = '#';
for (let i = 0; i < 3; i++) {
let c = parseInt(color.substring(i * 2 + 1, i * 2 + 3), 16);
c += range;
if (c < 0) {
c = 0;
} else if (c > 255) {
c = 255;
}
newColor += c.toString(16).padStart(2, '0');
}
return newColor;
}
/**
* 清除 xss
*/

View File

@@ -91,6 +91,9 @@
} else if (NewConnectionType.FAVORITE === props.newConnectionType) {
// 过滤-个人收藏
list = list.filter(item => item.favorite);
} else if (NewConnectionType.LATEST === props.newConnectionType) {
// 过滤-最近连接
// todo
}
// 排序
hostList.value = list?.sort((o1, o2) => {

View File

@@ -10,19 +10,17 @@
<!-- 主机地址 -->
<span class="address-wrapper">
{{ tab.address }}
<span class="address-copy copy-left" title="复制" @click="copy(tab.address as string)">
<span class="address-copy copy-right" title="复制" @click="copy(tab.address as string)">
<icon-copy />
</span>
</span>
</div>
<!-- 右侧操作 -->
<div class="terminal-header-right">
<icon-actions class="bottom-actions"
:actions="bottomActions"
position="right" />
<span @click="pl">
粘贴
</span>
<!-- 操作按钮 -->
<icon-actions class="terminal-header-right-icon-actions"
:actions="rightActions"
position="bottom" />
</div>
</div>
<!-- 终端 -->
@@ -42,61 +40,97 @@
</script>
<script lang="ts" setup>
import type { TerminalTabItem } from '../../types/terminal.type';
import type { ITerminalSession, TerminalTabItem } from '../../types/terminal.type';
import { onMounted, onUnmounted, ref } from 'vue';
import { useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy';
import IconActions from '@/views/host/terminal/components/layout/icon-actions.vue';
import { SidebarAction } from '@/views/host/terminal/types/terminal.const';
import { adjustColor } from '@/utils';
const props = defineProps<{
tab: TerminalTabItem
}>();
const { copy } = useCopy();
const { copy, readText } = useCopy();
const { preference, sessionManager } = useTerminalStore();
const terminalRef = ref();
const session = ref<ITerminalSession>();
// 底部操作
const bottomActions: Array<SidebarAction> = [
// FIXME
// 最近连接记录
// 防止 background 自动变更
// 去顶部 去底部 ctrl+c 重新连接 command-input 状态
// (未连接禁用点击)
// (改成可配置)
// 右侧操作
const rightActions: Array<SidebarAction> = [
{
icon: 'icon-command',
content: '快捷键设置',
icon: 'icon-expand',
content: '全选',
click: () => {
session.value?.selectAll();
session.value?.focus();
}
},
{
icon: 'icon-palette',
content: '外观设置',
}, {
icon: 'icon-copy',
content: '复制选中部分',
click: () => {
copy(session.value?.getSelection(), '已复制');
session.value?.focus();
}
}, {
icon: 'icon-paste',
content: '粘贴',
click: async () => {
if (session.value?.canWrite) {
session.value?.paste(await readText());
}
}
}, {
icon: 'icon-zoom-in',
content: '增大字号',
click: () => {
if (session.value?.connected) {
session.value.setOption('fontSize', session.value.getOption('fontSize') + 1);
session.value.fit();
session.value.focus();
}
}
}, {
icon: 'icon-zoom-out',
content: '减小字号',
click: () => {
if (session.value?.connected) {
session.value.setOption('fontSize', session.value.getOption('fontSize') - 1);
session.value.fit();
session.value.focus();
}
}
}, {
icon: 'icon-eraser',
content: '清空',
click: () => {
session.value?.clear();
session.value?.focus();
}
}, {
icon: 'icon-poweroff',
content: '关闭',
click: () => {
if (session.value?.connected) {
session.value.logout();
}
}
},
];
// 调整颜色
const adjustColor = (color: string, range: number) => {
let newColor = '#';
for (let i = 0; i < 3; i++) {
let c = parseInt(color.substring(i * 2 + 1, i * 2 + 3), 16);
c += range;
if (c < 0) {
c = 0;
} else if (c > 255) {
c = 255;
}
newColor += c.toString(16).padStart(2, '0');
}
return newColor;
};
const pl = () => {
};
// 初始化会话
onMounted(async () => {
// 创建终端处理器
sessionManager.openSession(props.tab, terminalRef.value);
session.value = await sessionManager.openSession(props.tab, terminalRef.value);
});
// 会话
@@ -107,7 +141,7 @@
</script>
<style lang="less" scoped>
@terminal-header-height: 32px;
@terminal-header-height: 36px;
.terminal-container {
width: 100%;
@@ -126,14 +160,31 @@
&-left, &-right {
display: flex;
align-items: center;
font-size: 12px;
width: 100%;
height: 100%;
}
&-left:hover {
.address-copy {
display: unset;
&-left {
.address-wrapper {
height: 100%;
display: inline-flex;
align-items: center;
user-select: none;
.address-copy {
display: none;
}
&:hover {
.address-copy {
display: unset;
}
}
&:before {
content: 'IP:';
padding-right: 4px;
}
}
}
@@ -141,14 +192,19 @@
justify-content: flex-end;
}
.address-wrapper:before {
content: 'IP:';
padding-right: 4px;
user-select: none;
}
&-right-icon-actions {
display: flex;
.address-copy {
display: none;
:deep(.terminal-sidebar-icon-wrapper) {
width: 30px;
height: 30px;
}
:deep(.terminal-sidebar-icon) {
width: 28px;
height: 28px;
font-size: 20px;
}
}
}

View File

@@ -46,6 +46,7 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
sessionId,
hostId
});
return session;
}
// 获取终端会话
@@ -55,13 +56,14 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
// 关闭终端会话
closeSession(sessionId: string): void {
// 发送关闭消息
this.channel?.send(InputProtocol.CLOSE, { sessionId });
// 关闭 session
const session = this.sessions[sessionId];
if (session) {
session.close();
if (!session) {
return;
}
// 登出
session.logout();
// 关闭 session
session.close();
// 移除 session
this.sessions[sessionId] = undefined as unknown as ITerminalSession;
// session 全部关闭后 关闭 channel

View File

@@ -2,7 +2,7 @@ import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type'
import { useTerminalStore } from '@/store';
import { fontFamilySuffix } from '../types/terminal.const';
import { InputProtocol } from '../types/terminal.protocol';
import { Terminal } from 'xterm';
import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl';
@@ -21,7 +21,7 @@ export default class TerminalSession implements ITerminalSession {
public connected: boolean;
private canWrite: boolean;
public canWrite: boolean;
private readonly sessionId: string;
@@ -107,6 +107,52 @@ export default class TerminalSession implements ITerminalSession {
this.addons.fit?.fit();
}
// 聚焦
focus(): void {
this.inst.focus();
}
// 清空
clear(): void {
this.inst.clear();
this.inst.clearSelection();
this.inst.focus();
}
// 粘贴
paste(value: string): void {
this.inst.paste(value);
this.inst.focus();
}
// 选中全部
selectAll(): void {
this.inst.selectAll();
}
// 获取选中
getSelection(): string {
return this.inst.getSelection();
}
// 获取配置
getOption(option: string): any {
return this.inst.options[option as keyof ITerminalOptions] as any;
}
// 设置配置
setOption(option: string, value: any): void {
this.inst.options[option as keyof ITerminalOptions] = value;
}
// 登出
logout(): void {
// 发送关闭消息
this.channel.send(InputProtocol.CLOSE, {
sessionId: this.sessionId
});
}
// 关闭
close(): void {
try {

View File

@@ -108,6 +108,14 @@
overflow: hidden;
}
&-left {
border-right: 1px solid var(--color-bg-content);
}
&-right {
border-left: 1px solid var(--color-bg-content);
}
&-content {
width: calc(100% - var(--sidebar-width) * 2);
height: 100%;

View File

@@ -53,7 +53,7 @@ export interface ITerminalTabManager {
// 终端会话管理器定义
export interface ITerminalSessionManager {
// 打开终端会话
openSession: (tab: TerminalTabItem, dom: HTMLElement) => void;
openSession: (tab: TerminalTabItem, dom: HTMLElement) => Promise<ITerminalSession>;
// 获取终端会话
getSession: (sessionId: string) => ITerminalSession;
// 关闭终端会话
@@ -95,6 +95,8 @@ export interface ITerminalSession {
inst: Terminal;
// 是否已连接
connected: boolean;
// 是否可写
canWrite: boolean;
// 初始化
init: (dom: HTMLElement) => void;
@@ -106,6 +108,22 @@ export interface ITerminalSession {
write: (value: string) => void;
// 自适应
fit: () => void;
// 聚焦
focus: () => void;
// 清空
clear: () => void;
// 粘贴
paste: (value: string) => void;
// 选中全部
selectAll: () => void;
// 获取选中
getSelection: () => string;
// 获取配置
getOption: (option: string) => any;
// 设置配置
setOption: (option: string, value: any) => void;
// 登出
logout: () => void;
// 关闭
close: () => void;
}