refactor: 终端操作抽象.

This commit is contained in:
lijiahang
2024-01-15 19:09:12 +08:00
parent 826907380b
commit 0977ef0703
10 changed files with 470 additions and 58 deletions

View File

@@ -5,6 +5,7 @@ import type {
TerminalPluginsSetting,
TerminalPreference,
TerminalSessionSetting,
TerminalShortcutSetting,
TerminalState
} from './types';
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
@@ -55,6 +56,10 @@ export default defineStore('terminal', {
interactSetting: {} as TerminalInteractSetting,
pluginsSetting: {} as TerminalPluginsSetting,
sessionSetting: {} as TerminalSessionSetting,
shortcutSetting: {
enabled: true,
keys: []
} as TerminalShortcutSetting,
},
hosts: {} as AuthorizedHostQueryResponse,
tabManager: new TerminalTabManager(),

View File

@@ -19,6 +19,7 @@ export interface TerminalPreference {
interactSetting: TerminalInteractSetting;
pluginsSetting: TerminalPluginsSetting;
sessionSetting: TerminalSessionSetting;
shortcutSetting: TerminalShortcutSetting;
}
// 显示设置
@@ -66,3 +67,18 @@ export interface TerminalSessionSetting {
terminalEmulationType: string;
scrollBackLine: number;
}
// 终端快捷键设置
export interface TerminalShortcutSetting {
enabled: boolean;
keys: Array<TerminalShortcutKey>;
}
// 终端快捷键
export interface TerminalShortcutKey {
option: string;
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
key: string;
}

View File

@@ -67,6 +67,8 @@
}
});
// TODO 快捷键逻辑 主机加载逻辑 加载中逻辑
</script>
<style lang="less" scoped>

View File

@@ -68,7 +68,7 @@
</script>
<script lang="ts" setup>
import type { ITerminalSession, TerminalTabItem, SidebarAction } from '../../types/terminal.type';
import type { ITerminalSession, TerminalTabItem, SidebarAction, ITerminalSessionHandler } from '../../types/terminal.type';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useDictStore, useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy';
@@ -82,7 +82,7 @@
tab: TerminalTabItem
}>();
const { copy, readText } = useCopy();
const { copy } = useCopy();
const { getDictValue } = useDictStore();
const { preference, tabManager, sessionManager } = useTerminalStore();
@@ -93,7 +93,7 @@
const session = ref<ITerminalSession>();
// TODO
// 设置快捷键 粘贴逻辑 禁用
// 设置快捷键
// 截屏
// sftp
@@ -134,48 +134,19 @@
};
});
// 执行终端操作
const doTerminalHandle = (handle: string) => {
// 处理器
const handler = session.value?.handler[handle as keyof ITerminalSessionHandler] as () => void;
handler && handler.call(session.value?.handler);
};
// 操作点击逻辑
const actionsClickHandler: Record<string, () => void> = {
// 去顶部
toTop: () => session.value?.toTop(),
// 去底部
toBottom: () => session.value?.toBottom(),
// 全选
checkAll: () => session.value?.selectAll(),
// 搜索
search: () => searchModal.value.toggle(),
// 复制选中部分
copy: () => session.value?.copySelection(),
// 粘贴
paste: async () => session.value?.pasteTrimEnd(await readText()),
// ctrl + c
interrupt: () => session.value?.paste(String.fromCharCode(3)),
// 回车
enter: () => session.value?.paste(String.fromCharCode(13)),
// 增大字号
fontSizePlus: () => {
if (session.value) {
session.value.setOption('fontSize', session.value.getOption('fontSize') + 1);
if (session.value.connected) {
session.value.fit();
session.value.focus();
}
}
},
// 减小字号
fontSizeSubtract: () => {
if (session.value) {
session.value.setOption('fontSize', session.value.getOption('fontSize') - 1);
if (session.value.connected) {
session.value.fit();
session.value.focus();
}
}
},
// 命令编辑器
commandEditor: () => editorModal.value.open('', ''),
// 清空
clear: () => session.value?.clear(),
// 断开连接
disconnect: () => session.value?.disconnect(),
// 关闭
@@ -189,10 +160,8 @@
icon: s.icon,
content: s.content,
visible: preference.actionBarSetting[s.item] !== false,
disabled: actionsEnabledStatus.value[s.item] === false,
click: () => {
actionsClickHandler[s.item] && actionsClickHandler[s.item]();
}
disabled: session.value?.handler.enabledStatus(s.item) === false,
click: () => doTerminalHandle(s.item)
};
});
});
@@ -200,7 +169,11 @@
// 初始化会话
onMounted(async () => {
// 创建终端处理器
session.value = await sessionManager.openSession(props.tab, terminalRef.value);
session.value = await sessionManager.openSession(props.tab, {
el: terminalRef.value,
editorModal: editorModal.value,
searchModal: searchModal.value
});
});
// 会话

View File

@@ -0,0 +1,167 @@
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ITerminalSession, ITerminalSessionHandler, ITerminalTabManager, TerminalDomRef } from '../types/terminal.type';
import type { Terminal } from 'xterm';
import { useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy';
const { copy: copyValue, readText } = useCopy();
// 终端会话处理器实现
export default class TerminalSessionHandler implements ITerminalSessionHandler {
private readonly domRef: TerminalDomRef;
private readonly inst: Terminal;
private readonly session: ITerminalSession;
private readonly preference: TerminalPreference;
private readonly tabManager: ITerminalTabManager;
constructor(session: ITerminalSession,
domRef: TerminalDomRef) {
this.session = session;
this.inst = session.inst;
this.domRef = domRef;
const { preference, tabManager } = useTerminalStore();
this.preference = preference;
this.tabManager = tabManager;
}
// 启用状态
enabledStatus(option: string): boolean {
switch (option) {
case 'paste':
case 'interrupt':
case 'enter':
case 'commandEditor':
return this.session.canWrite;
case 'disconnect':
return this.session.connected;
default:
return true;
}
}
// 复制选中
copy() {
let selection = this.inst.getSelection();
if (selection) {
// 去除尾部空格
if (this.preference.interactSetting.copyAutoTrim) {
selection = selection.trimEnd();
}
// 复制
copyValue(selection, false);
}
// 聚焦
this.inst.focus();
}
// 粘贴
paste() {
if (this.enabledStatus('paste')) {
readText().then(s => this.pasteTrimEnd(s));
}
}
// 粘贴并且去除尾部空格 (如果配置)
pasteTrimEnd(value: string) {
if (this.enabledStatus('paste')) {
if (this.preference.interactSetting.pasteAutoTrim) {
// 粘贴前去除尾部空格
this.inst.paste(value.trimEnd());
} else {
this.inst.paste(value);
}
this.inst.focus();
}
}
// 选中全部
selectAll() {
this.inst.selectAll();
this.inst.focus();
}
// 去顶部
toTop(): void {
this.inst.scrollToTop();
this.inst.focus();
}
// 去底部
toBottom(): void {
this.inst.scrollToBottom();
this.inst.focus();
}
// 打开搜索
search() {
this.domRef.searchModal?.toggle();
}
// 增大字号
fontSizePlus() {
this.fontSizeAdd(1);
}
// 减小字号
fontSizeSubtract() {
this.fontSizeAdd(-1);
}
// 字号增加
private fontSizeAdd(addSize: number) {
this.inst.options['fontSize'] = this.inst.options['fontSize'] as number + addSize;
if (this.session.connected) {
this.session.fit();
this.inst.focus();
}
}
// 打开命令编辑器
commandEditor() {
if (this.enabledStatus('commandEditor')) {
this.domRef.editorModal?.open('', '');
}
}
// ctrl + c
interrupt() {
if (this.enabledStatus('interrupt')) {
this.inst.paste(String.fromCharCode(3));
}
}
// 回车
enter() {
if (this.enabledStatus('enter')) {
this.inst.paste(String.fromCharCode(13));
}
}
// 清空
clear() {
this.inst.clear();
}
// 断开连接
disconnect() {
if (this.enabledStatus('disconnect')) {
this.session.disconnect();
}
}
// 关闭
close() {
this.tabManager.deleteTab(this.session.sessionId);
}
// 聚焦
focus() {
this.inst.focus();
}
}

View File

@@ -1,4 +1,4 @@
import type { ITerminalChannel, ITerminalSession, ITerminalSessionManager, TerminalTabItem } from '../types/terminal.type';
import type { ITerminalChannel, ITerminalSession, ITerminalSessionManager, TerminalDomRef, TerminalTabItem } from '../types/terminal.type';
import { sleep } from '@/utils';
import { InputProtocol } from '../types/terminal.protocol';
import { useDebounceFn } from '@vueuse/core';
@@ -24,7 +24,8 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
}
// 打开终端会话
async openSession(tab: TerminalTabItem, dom: HTMLElement) {
async openSession(tab: TerminalTabItem,
domRef: TerminalDomRef) {
const sessionId = tab.key;
const hostId = tab.hostId as number;
// 初始化客户端
@@ -36,7 +37,7 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
this.channel
);
// 初始化
session.init(dom);
session.init(domRef);
// 等待前端渲染完成
await sleep(100);
// 添加会话

View File

@@ -1,6 +1,6 @@
import type { UnwrapRef } from 'vue';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ITerminalChannel, ITerminalSession, TerminalAddons } from '../types/terminal.type';
import type { ITerminalChannel, ITerminalSession, ITerminalSessionHandler, TerminalAddons, TerminalDomRef } from '../types/terminal.type';
import { useTerminalStore } from '@/store';
import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const';
import { InputProtocol } from '../types/terminal.protocol';
@@ -13,13 +13,17 @@ import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import { playBell } from '@/utils/bell';
import useCopy from '@/hooks/copy';
import TerminalShortcutDispatcher from './terminal-shortcut-dispatch';
import TerminalSessionHandler from './terminal-session-handler';
const copy = useCopy();
// 终端会话实现
export default class TerminalSession implements ITerminalSession {
public hostId: number;
public readonly hostId: number;
public sessionId: string;
public inst: Terminal;
@@ -29,7 +33,7 @@ export default class TerminalSession implements ITerminalSession {
public status: number;
private readonly sessionId: string;
public handler: ITerminalSessionHandler;
private readonly channel: ITerminalChannel;
@@ -45,11 +49,12 @@ export default class TerminalSession implements ITerminalSession {
this.canWrite = false;
this.status = TerminalStatus.CONNECTING;
this.inst = undefined as unknown as Terminal;
this.handler = undefined as unknown as ITerminalSessionHandler;
this.addons = {} as TerminalAddons;
}
// 初始化
init(dom: HTMLElement): void {
init(domRef: TerminalDomRef): void {
const { preference } = useTerminalStore();
// 初始化实例
this.inst = new Terminal({
@@ -62,17 +67,39 @@ export default class TerminalSession implements ITerminalSession {
wordSeparator: preference.interactSetting.wordSeparator,
scrollback: preference.sessionSetting.scrollBackLine,
});
// 处理器
this.handler = new TerminalSessionHandler(this, domRef);
// 注册快捷键
this.registerShortcut(preference);
// 注册事件
this.registerEvent(dom, preference);
this.registerEvent(domRef.el, preference);
// 注册插件
this.registerAddions(preference);
this.registerAddons(preference);
// 打开终端
this.inst.open(dom);
this.inst.open(domRef.el);
// 自适应
this.addons.fit.fit();
}
// 注册快捷键
private registerShortcut(preference: UnwrapRef<TerminalPreference>) {
const dispatcher = new TerminalShortcutDispatcher(this, preference.shortcutSetting.keys);
// 处理自定义按键
this.inst.attachCustomKeyEventHandler((e: KeyboardEvent) => {
e.preventDefault();
// 未开启
if (!preference.shortcutSetting.enabled) {
return true;
}
// 只监听 keydown 事件
if (e.type !== 'keydown') {
return true;
}
// 调度快捷键
return dispatcher.dispatch(e);
});
}
// 注册事件
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
// 注册输入事件
@@ -127,7 +154,7 @@ export default class TerminalSession implements ITerminalSession {
}
// 注册插件
private registerAddions(preference: UnwrapRef<TerminalPreference>) {
private registerAddons(preference: UnwrapRef<TerminalPreference>) {
this.addons.fit = new FitAddon();
this.addons.search = new SearchAddon();
// 超链接插件

View File

@@ -0,0 +1,167 @@
import type { TerminalShortcutKey } from '@/store/modules/terminal/types';
import type { ITerminalSession, ITerminalShortcutDispatcher } from '../types/terminal.type';
import useCopy from '@/hooks/copy';
const { readText } = useCopy();
// 终端快捷键调度实现
export default class TerminalShortcutDispatch implements ITerminalShortcutDispatcher {
private readonly session: ITerminalSession;
private readonly keys: Array<TerminalShortcutKey>;
constructor(session: ITerminalSession, keys: Array<TerminalShortcutKey>) {
this.session = session;
// this.keys = keys;
this.keys = [
{
option: 'copy',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'C'
}, {
option: 'paste',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'V'
}, {
option: 'toTop',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'ArrowUp'
}, {
option: 'toBottom',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'ArrowDown'
}, {
option: 'selectAll',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'A'
}, {
option: 'search',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'F'
}, {
option: 'fontSizePlus',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: '+'
}, {
option: 'fontSizeSubtract',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: '_'
}, {
option: 'commandEditor',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'O'
}, {
option: 'close',
ctrlKey: true,
shiftKey: true,
altKey: false,
key: 'W'
}
];
}
// 调度快捷键
dispatch(e: KeyboardEvent): boolean {
console.log(e);
for (const key of this.keys) {
if (key.altKey === e.altKey
&& key.shiftKey === e.shiftKey
&& key.ctrlKey === e.ctrlKey
&& key.key === e.key) {
const runner = this[key.option as keyof this] as () => void;
runner && runner.apply(this);
return false;
}
}
return true;
}
// 复制
private copy(): void {
this.session.copySelection();
}
// 粘贴
private paste(): void {
// FIXME status
readText().then((e) => {
this.session.pasteTrimEnd(e);
});
}
// 去顶部
private toTop(): void {
this.session.toTop();
}
// 去底部
private toBottom(): void {
this.session.toBottom();
}
// 全选
private selectAll(): void {
this.session.selectAll();
}
// 搜索
private search(): void {
// fixme
}
// 增大字号
private fontSizePlus(): void {
this.fontSizeAdd(1);
}
// 减小字号
private fontSizeSubtract(): void {
this.fontSizeAdd(-1);
}
// 字号增加
private fontSizeAdd(addSize: number) {
this.session.setOption('fontSize', this.session.getOption('fontSize') + addSize);
if (this.session.connected) {
this.session.fit();
this.session.focus();
}
}
// 命令编辑器
private commandEditor(): void {
// fixme
}
// 关闭终端
private close(): void {
}
private to(): void {
}
// 切换 tab
// 打开 新
}

View File

@@ -77,7 +77,7 @@ export const ActionBarItems = [
icon: 'icon-down',
content: '去底部',
}, {
item: 'checkAll',
item: 'selectAll',
icon: 'icon-expand',
content: '全选',
}, {

View File

@@ -77,6 +77,13 @@ export interface OutputPayload {
[key: string]: string;
}
// 终端 dom 元素引用
export interface TerminalDomRef {
el: HTMLElement;
searchModal: any;
editorModal: any;
}
// 终端 tab 管理器定义
export interface ITerminalTabManager {
// 当前 tab
@@ -97,7 +104,7 @@ export interface ITerminalTabManager {
// 终端会话管理器定义
export interface ITerminalSessionManager {
// 打开终端会话
openSession: (tab: TerminalTabItem, dom: HTMLElement) => Promise<ITerminalSession>;
openSession: (tab: TerminalTabItem, domRef: TerminalDomRef) => Promise<ITerminalSession>;
// 获取终端会话
getSession: (sessionId: string) => ITerminalSession;
// 关闭终端会话
@@ -145,6 +152,7 @@ export interface TerminalAddons {
// 终端会话定义
export interface ITerminalSession {
hostId: number;
sessionId: string;
// terminal 实例
inst: Terminal;
// 是否已连接
@@ -153,9 +161,11 @@ export interface ITerminalSession {
canWrite: boolean;
// 状态
status: number;
// 处理器
handler: ITerminalSessionHandler;
// 初始化
init: (dom: HTMLElement) => void;
init: (domRef: TerminalDomRef) => void;
// 连接
connect: () => void;
// 设置是否可写
@@ -191,3 +201,47 @@ export interface ITerminalSession {
// 关闭
close: () => void;
}
// 终端会话处理器定义
export interface ITerminalSessionHandler {
// 启用状态
enabledStatus: (option: string) => boolean;
// 复制选中
copy: () => void;
// 粘贴
paste: () => void;
// 粘贴并且去除尾部空格 (如果配置)
pasteTrimEnd: (value: string) => void;
// 选中全部
selectAll: () => void;
// 去顶部
toTop: () => void;
// 去底部
toBottom: () => void;
// 打开搜索
search: () => void;
// 增大字号
fontSizePlus: () => void;
// 减小字号
fontSizeSubtract: () => void;
// 打开命令编辑器
commandEditor: () => void;
// 中断
interrupt: () => void;
// 回车
enter: () => void;
// 清空
clear: () => void;
// 断开连接
disconnect: () => void;
// 关闭
close: () => void;
// 聚焦
focus: () => void;
}
// 终端快捷键调度定义
export interface ITerminalShortcutDispatcher {
// 调度快捷键
dispatch(e: KeyboardEvent): boolean;
}