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