feat: 终端头部功能.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user