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; margin-bottom: 16px;
} }
.copy-left { .copy-left, .copy-right {
color: rgb(var(--arcoblue-6)); color: rgb(var(--arcoblue-6));
cursor: pointer; cursor: pointer;
margin-right: 4px; margin-right: 4px;
} }
.copy-left {
margin-right: 4px;
}
.copy-right {
margin-left: 4px;
}
.span-blue { .span-blue {
color: rgb(var(--arcoblue-6)); color: rgb(var(--arcoblue-6));
} }

View File

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

View File

@@ -163,7 +163,7 @@ export const resetObject = (obj: any, ignore: string[] = []) => {
export const objectTruthKeyCount = (obj: any, ignore: string[] = []) => { export const objectTruthKeyCount = (obj: any, ignore: string[] = []) => {
return Object.keys(obj) return Object.keys(obj)
.filter(s => !ignore.includes(s)) .filter(s => !ignore.includes(s))
.reduce(function (acc, curr) { .reduce(function(acc, curr) {
const currVal = obj[curr]; const currVal = obj[curr];
return acc + ~~(currVal !== undefined && currVal !== null && currVal?.length !== 0 && currVal !== ''); return acc + ~~(currVal !== undefined && currVal !== null && currVal?.length !== 0 && currVal !== '');
}, 0); }, 0);
@@ -192,24 +192,37 @@ export function detectZoom() {
return ratio; return ratio;
} }
/**
* 获取页面路由
*/
export function getRoute(url = location.href) {
return url.substring(url.lastIndexOf('#') + 1).split('?')[0];
}
/** /**
* 获取唯一的 UUID * 获取唯一的 UUID
*/ */
export function getUUID() { 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 r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8); const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16); 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 * 清除 xss
*/ */

View File

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

View File

@@ -10,19 +10,17 @@
<!-- 主机地址 --> <!-- 主机地址 -->
<span class="address-wrapper"> <span class="address-wrapper">
{{ tab.address }} {{ 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 /> <icon-copy />
</span> </span>
</span> </span>
</div> </div>
<!-- 右侧操作 --> <!-- 右侧操作 -->
<div class="terminal-header-right"> <div class="terminal-header-right">
<icon-actions class="bottom-actions" <!-- 操作按钮 -->
:actions="bottomActions" <icon-actions class="terminal-header-right-icon-actions"
position="right" /> :actions="rightActions"
<span @click="pl"> position="bottom" />
粘贴
</span>
</div> </div>
</div> </div>
<!-- 终端 --> <!-- 终端 -->
@@ -42,61 +40,97 @@
</script> </script>
<script lang="ts" setup> <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 { onMounted, onUnmounted, ref } from 'vue';
import { useTerminalStore } from '@/store'; import { useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
import IconActions from '@/views/host/terminal/components/layout/icon-actions.vue'; import IconActions from '@/views/host/terminal/components/layout/icon-actions.vue';
import { SidebarAction } from '@/views/host/terminal/types/terminal.const'; import { SidebarAction } from '@/views/host/terminal/types/terminal.const';
import { adjustColor } from '@/utils';
const props = defineProps<{ const props = defineProps<{
tab: TerminalTabItem tab: TerminalTabItem
}>(); }>();
const { copy } = useCopy(); const { copy, readText } = useCopy();
const { preference, sessionManager } = useTerminalStore(); const { preference, sessionManager } = useTerminalStore();
const terminalRef = ref(); const terminalRef = ref();
const session = ref<ITerminalSession>();
// 底部操作 // FIXME
const bottomActions: Array<SidebarAction> = [ // 最近连接记录
// 防止 background 自动变更
// 去顶部 去底部 ctrl+c 重新连接 command-input 状态
// (未连接禁用点击)
// (改成可配置)
// 右侧操作
const rightActions: Array<SidebarAction> = [
{ {
icon: 'icon-command', icon: 'icon-expand',
content: '快捷键设置', content: '全选',
click: () => { click: () => {
session.value?.selectAll();
session.value?.focus();
} }
}, }, {
{ icon: 'icon-copy',
icon: 'icon-palette', content: '复制选中部分',
content: '外观设置',
click: () => { 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 () => { onMounted(async () => {
// 创建终端处理器 // 创建终端处理器
sessionManager.openSession(props.tab, terminalRef.value); session.value = await sessionManager.openSession(props.tab, terminalRef.value);
}); });
// 会话 // 会话
@@ -107,7 +141,7 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@terminal-header-height: 32px; @terminal-header-height: 36px;
.terminal-container { .terminal-container {
width: 100%; width: 100%;
@@ -126,14 +160,31 @@
&-left, &-right { &-left, &-right {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 12px;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
&-left:hover { &-left {
.address-copy { .address-wrapper {
display: unset; 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; justify-content: flex-end;
} }
.address-wrapper:before { &-right-icon-actions {
content: 'IP:'; display: flex;
padding-right: 4px;
user-select: none;
}
.address-copy { :deep(.terminal-sidebar-icon-wrapper) {
display: none; 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, sessionId,
hostId hostId
}); });
return session;
} }
// 获取终端会话 // 获取终端会话
@@ -55,13 +56,14 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
// 关闭终端会话 // 关闭终端会话
closeSession(sessionId: string): void { closeSession(sessionId: string): void {
// 发送关闭消息
this.channel?.send(InputProtocol.CLOSE, { sessionId });
// 关闭 session
const session = this.sessions[sessionId]; const session = this.sessions[sessionId];
if (session) { if (!session) {
session.close(); return;
} }
// 登出
session.logout();
// 关闭 session
session.close();
// 移除 session // 移除 session
this.sessions[sessionId] = undefined as unknown as ITerminalSession; this.sessions[sessionId] = undefined as unknown as ITerminalSession;
// session 全部关闭后 关闭 channel // session 全部关闭后 关闭 channel

View File

@@ -2,7 +2,7 @@ import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type'
import { useTerminalStore } from '@/store'; import { useTerminalStore } from '@/store';
import { fontFamilySuffix } from '../types/terminal.const'; import { fontFamilySuffix } from '../types/terminal.const';
import { InputProtocol } from '../types/terminal.protocol'; import { InputProtocol } from '../types/terminal.protocol';
import { Terminal } from 'xterm'; import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl'; import { WebglAddon } from 'xterm-addon-webgl';
@@ -21,7 +21,7 @@ export default class TerminalSession implements ITerminalSession {
public connected: boolean; public connected: boolean;
private canWrite: boolean; public canWrite: boolean;
private readonly sessionId: string; private readonly sessionId: string;
@@ -107,6 +107,52 @@ export default class TerminalSession implements ITerminalSession {
this.addons.fit?.fit(); 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 { close(): void {
try { try {

View File

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

View File

@@ -53,7 +53,7 @@ export interface ITerminalTabManager {
// 终端会话管理器定义 // 终端会话管理器定义
export interface ITerminalSessionManager { export interface ITerminalSessionManager {
// 打开终端会话 // 打开终端会话
openSession: (tab: TerminalTabItem, dom: HTMLElement) => void; openSession: (tab: TerminalTabItem, dom: HTMLElement) => Promise<ITerminalSession>;
// 获取终端会话 // 获取终端会话
getSession: (sessionId: string) => ITerminalSession; getSession: (sessionId: string) => ITerminalSession;
// 关闭终端会话 // 关闭终端会话
@@ -95,6 +95,8 @@ export interface ITerminalSession {
inst: Terminal; inst: Terminal;
// 是否已连接 // 是否已连接
connected: boolean; connected: boolean;
// 是否可写
canWrite: boolean;
// 初始化 // 初始化
init: (dom: HTMLElement) => void; init: (dom: HTMLElement) => void;
@@ -106,6 +108,22 @@ export interface ITerminalSession {
write: (value: string) => void; write: (value: string) => void;
// 自适应 // 自适应
fit: () => 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; close: () => void;
} }