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

View File

@@ -19,6 +19,7 @@ export interface TerminalPreference {
interactSetting: TerminalInteractSetting; interactSetting: TerminalInteractSetting;
pluginsSetting: TerminalPluginsSetting; pluginsSetting: TerminalPluginsSetting;
sessionSetting: TerminalSessionSetting; sessionSetting: TerminalSessionSetting;
shortcutSetting: TerminalShortcutSetting;
} }
// 显示设置 // 显示设置
@@ -66,3 +67,18 @@ export interface TerminalSessionSetting {
terminalEmulationType: string; terminalEmulationType: string;
scrollBackLine: number; 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> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -68,7 +68,7 @@
</script> </script>
<script lang="ts" setup> <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 { computed, onMounted, onUnmounted, ref } from 'vue';
import { useDictStore, useTerminalStore } from '@/store'; import { useDictStore, useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
@@ -82,7 +82,7 @@
tab: TerminalTabItem tab: TerminalTabItem
}>(); }>();
const { copy, readText } = useCopy(); const { copy } = useCopy();
const { getDictValue } = useDictStore(); const { getDictValue } = useDictStore();
const { preference, tabManager, sessionManager } = useTerminalStore(); const { preference, tabManager, sessionManager } = useTerminalStore();
@@ -93,7 +93,7 @@
const session = ref<ITerminalSession>(); const session = ref<ITerminalSession>();
// TODO // TODO
// 设置快捷键 粘贴逻辑 禁用 // 设置快捷键
// 截屏 // 截屏
// sftp // 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> = { const actionsClickHandler: Record<string, () => void> = {
// 去顶部
toTop: () => session.value?.toTop(),
// 去底部
toBottom: () => session.value?.toBottom(),
// 全选
checkAll: () => session.value?.selectAll(),
// 搜索 // 搜索
search: () => searchModal.value.toggle(), 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('', ''), commandEditor: () => editorModal.value.open('', ''),
// 清空
clear: () => session.value?.clear(),
// 断开连接 // 断开连接
disconnect: () => session.value?.disconnect(), disconnect: () => session.value?.disconnect(),
// 关闭 // 关闭
@@ -189,10 +160,8 @@
icon: s.icon, icon: s.icon,
content: s.content, content: s.content,
visible: preference.actionBarSetting[s.item] !== false, visible: preference.actionBarSetting[s.item] !== false,
disabled: actionsEnabledStatus.value[s.item] === false, disabled: session.value?.handler.enabledStatus(s.item) === false,
click: () => { click: () => doTerminalHandle(s.item)
actionsClickHandler[s.item] && actionsClickHandler[s.item]();
}
}; };
}); });
}); });
@@ -200,7 +169,11 @@
// 初始化会话 // 初始化会话
onMounted(async () => { 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 { sleep } from '@/utils';
import { InputProtocol } from '../types/terminal.protocol'; import { InputProtocol } from '../types/terminal.protocol';
import { useDebounceFn } from '@vueuse/core'; 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 sessionId = tab.key;
const hostId = tab.hostId as number; const hostId = tab.hostId as number;
// 初始化客户端 // 初始化客户端
@@ -36,7 +37,7 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
this.channel this.channel
); );
// 初始化 // 初始化
session.init(dom); session.init(domRef);
// 等待前端渲染完成 // 等待前端渲染完成
await sleep(100); await sleep(100);
// 添加会话 // 添加会话

View File

@@ -1,6 +1,6 @@
import type { UnwrapRef } from 'vue'; import type { UnwrapRef } from 'vue';
import type { TerminalPreference } from '@/store/modules/terminal/types'; 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 { useTerminalStore } from '@/store';
import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const'; import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const';
import { InputProtocol } from '../types/terminal.protocol'; import { InputProtocol } from '../types/terminal.protocol';
@@ -13,13 +13,17 @@ import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl'; import { WebglAddon } from 'xterm-addon-webgl';
import { playBell } from '@/utils/bell'; import { playBell } from '@/utils/bell';
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
import TerminalShortcutDispatcher from './terminal-shortcut-dispatch';
import TerminalSessionHandler from './terminal-session-handler';
const copy = useCopy(); const copy = useCopy();
// 终端会话实现 // 终端会话实现
export default class TerminalSession implements ITerminalSession { export default class TerminalSession implements ITerminalSession {
public hostId: number; public readonly hostId: number;
public sessionId: string;
public inst: Terminal; public inst: Terminal;
@@ -29,7 +33,7 @@ export default class TerminalSession implements ITerminalSession {
public status: number; public status: number;
private readonly sessionId: string; public handler: ITerminalSessionHandler;
private readonly channel: ITerminalChannel; private readonly channel: ITerminalChannel;
@@ -45,11 +49,12 @@ export default class TerminalSession implements ITerminalSession {
this.canWrite = false; this.canWrite = false;
this.status = TerminalStatus.CONNECTING; this.status = TerminalStatus.CONNECTING;
this.inst = undefined as unknown as Terminal; this.inst = undefined as unknown as Terminal;
this.handler = undefined as unknown as ITerminalSessionHandler;
this.addons = {} as TerminalAddons; this.addons = {} as TerminalAddons;
} }
// 初始化 // 初始化
init(dom: HTMLElement): void { init(domRef: TerminalDomRef): void {
const { preference } = useTerminalStore(); const { preference } = useTerminalStore();
// 初始化实例 // 初始化实例
this.inst = new Terminal({ this.inst = new Terminal({
@@ -62,17 +67,39 @@ export default class TerminalSession implements ITerminalSession {
wordSeparator: preference.interactSetting.wordSeparator, wordSeparator: preference.interactSetting.wordSeparator,
scrollback: preference.sessionSetting.scrollBackLine, 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(); 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>) { 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.fit = new FitAddon();
this.addons.search = new SearchAddon(); 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', icon: 'icon-down',
content: '去底部', content: '去底部',
}, { }, {
item: 'checkAll', item: 'selectAll',
icon: 'icon-expand', icon: 'icon-expand',
content: '全选', content: '全选',
}, { }, {

View File

@@ -77,6 +77,13 @@ export interface OutputPayload {
[key: string]: string; [key: string]: string;
} }
// 终端 dom 元素引用
export interface TerminalDomRef {
el: HTMLElement;
searchModal: any;
editorModal: any;
}
// 终端 tab 管理器定义 // 终端 tab 管理器定义
export interface ITerminalTabManager { export interface ITerminalTabManager {
// 当前 tab // 当前 tab
@@ -97,7 +104,7 @@ export interface ITerminalTabManager {
// 终端会话管理器定义 // 终端会话管理器定义
export interface ITerminalSessionManager { export interface ITerminalSessionManager {
// 打开终端会话 // 打开终端会话
openSession: (tab: TerminalTabItem, dom: HTMLElement) => Promise<ITerminalSession>; openSession: (tab: TerminalTabItem, domRef: TerminalDomRef) => Promise<ITerminalSession>;
// 获取终端会话 // 获取终端会话
getSession: (sessionId: string) => ITerminalSession; getSession: (sessionId: string) => ITerminalSession;
// 关闭终端会话 // 关闭终端会话
@@ -145,6 +152,7 @@ export interface TerminalAddons {
// 终端会话定义 // 终端会话定义
export interface ITerminalSession { export interface ITerminalSession {
hostId: number; hostId: number;
sessionId: string;
// terminal 实例 // terminal 实例
inst: Terminal; inst: Terminal;
// 是否已连接 // 是否已连接
@@ -153,9 +161,11 @@ export interface ITerminalSession {
canWrite: boolean; canWrite: boolean;
// 状态 // 状态
status: number; status: number;
// 处理器
handler: ITerminalSessionHandler;
// 初始化 // 初始化
init: (dom: HTMLElement) => void; init: (domRef: TerminalDomRef) => void;
// 连接 // 连接
connect: () => void; connect: () => void;
// 设置是否可写 // 设置是否可写
@@ -191,3 +201,47 @@ export interface ITerminalSession {
// 关闭 // 关闭
close: () => void; 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;
}