feat: 终端头部指令.
This commit is contained in:
@@ -189,7 +189,12 @@ body[terminal-theme='dark'] .host-layout {
|
||||
&:hover {
|
||||
background: var(--color-sidebar-icon-bg);
|
||||
}
|
||||
|
||||
&.disabled-item {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 终端设置容器
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-arrow"
|
||||
:content="action.content">
|
||||
<div class="terminal-sidebar-icon-wrapper">
|
||||
<div class="terminal-sidebar-icon-wrapper" v-if="action.visible !== false">
|
||||
<div class="terminal-sidebar-icon"
|
||||
:class="iconClass"
|
||||
@click="action.click">
|
||||
:class="[iconClass, action.disabled !== false ? '' : 'disabled-item']"
|
||||
@click="action.disabled !== false ? action.click() : false">
|
||||
<component :is="action.icon" :style="action?.iconStyle" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 头部 -->
|
||||
<div class="terminal-header"
|
||||
:style="{
|
||||
background: adjustColor(preference.themeSchema.background, -10)
|
||||
background: adjustColor(themeSchema.background, -12)
|
||||
}">
|
||||
<!-- 左侧操作 -->
|
||||
<div class="terminal-header-left">
|
||||
@@ -17,16 +17,27 @@
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="terminal-header-right">
|
||||
<!-- 代码输入框 -->
|
||||
<a-textarea class="command-input mr8"
|
||||
v-model="commandInput"
|
||||
:auto-size="{ minRows: 1, maxRows: 1 }"
|
||||
placeholder="F8 发送命令"
|
||||
allow-clear
|
||||
@keyup="writeCommandInput" />
|
||||
<!-- 操作按钮 -->
|
||||
<icon-actions class="terminal-header-right-icon-actions"
|
||||
:actions="rightActions"
|
||||
position="bottom" />
|
||||
<!-- 状态 -->
|
||||
<a-badge style="margin: 0 2px 0 8px;"
|
||||
:status="getDictValue(connectStatusKey, session ? session.status : 0, 'status')"
|
||||
:text="getDictValue(connectStatusKey, session ? session.status : 0)" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 终端 -->
|
||||
<div class="terminal-wrapper"
|
||||
:style="{
|
||||
background: preference.themeSchema.background
|
||||
background: themeSchema.background
|
||||
}">
|
||||
<div class="terminal-inst" ref="terminalRef" />
|
||||
</div>
|
||||
@@ -41,11 +52,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ITerminalSession, TerminalTabItem } from '../../types/terminal.type';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useDictStore, 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 { connectStatusKey, SidebarAction } from '@/views/host/terminal/types/terminal.const';
|
||||
import { adjustColor } from '@/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -53,79 +64,127 @@
|
||||
}>();
|
||||
|
||||
const { copy, readText } = useCopy();
|
||||
const { getDictValue } = useDictStore();
|
||||
const { preference, sessionManager } = useTerminalStore();
|
||||
|
||||
const commandInput = ref();
|
||||
const themeSchema = preference.themeSchema;
|
||||
const terminalRef = ref();
|
||||
const session = ref<ITerminalSession>();
|
||||
|
||||
// FIXME
|
||||
// 最近连接记录
|
||||
// 防止 background 自动变更
|
||||
// 去顶部 去底部 ctrl+c 重新连接 command-input 状态
|
||||
// (未连接禁用点击)
|
||||
// (改成可配置)
|
||||
// 命令编辑器 搜索
|
||||
// (改成可配置/拆分)
|
||||
// 自定义 font siderBar 颜色, 集成到主题里面, 现在的问题是切换主题字体颜色就变了
|
||||
// 右键菜单补充
|
||||
// 搜索插件, link插件
|
||||
|
||||
// 发送命令
|
||||
const writeCommandInput = async (e: KeyboardEvent) => {
|
||||
const value = commandInput.value;
|
||||
if (value && e.code === 'F8' && session.value?.canWrite) {
|
||||
session.value.paste(value);
|
||||
commandInput.value = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 右侧操作
|
||||
const rightActions: Array<SidebarAction> = [
|
||||
const rightActions = computed<Array<SidebarAction>>(() => [
|
||||
{
|
||||
icon: 'icon-up',
|
||||
content: '去顶部',
|
||||
click: () => {
|
||||
session.value?.toTop();
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-down',
|
||||
content: '去底部',
|
||||
click: () => {
|
||||
session.value?.toBottom();
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-expand',
|
||||
content: '全选',
|
||||
click: () => {
|
||||
session.value?.selectAll();
|
||||
session.value?.focus();
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-copy',
|
||||
content: '复制选中部分',
|
||||
click: () => {
|
||||
copy(session.value?.getSelection(), '已复制');
|
||||
session.value?.focus();
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-paste',
|
||||
content: '粘贴',
|
||||
disabled: session.value?.canWrite,
|
||||
click: async () => {
|
||||
if (session.value?.canWrite) {
|
||||
session.value?.paste(await readText());
|
||||
}
|
||||
session.value?.paste(await readText());
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-formula',
|
||||
content: 'ctrl + c',
|
||||
disabled: session.value?.canWrite,
|
||||
click: () => {
|
||||
session.value?.paste(String.fromCharCode(3));
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-play-arrow-fill',
|
||||
content: '回车',
|
||||
disabled: session.value?.canWrite,
|
||||
click: () => {
|
||||
session.value?.paste(String.fromCharCode(13));
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-code-square',
|
||||
content: '命令编辑器',
|
||||
disabled: session.value?.canWrite,
|
||||
click: () => {
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-search',
|
||||
content: '搜索',
|
||||
click: () => {
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-zoom-in',
|
||||
content: '增大字号',
|
||||
click: () => {
|
||||
if (session.value?.connected) {
|
||||
if (session.value) {
|
||||
session.value.setOption('fontSize', session.value.getOption('fontSize') + 1);
|
||||
session.value.fit();
|
||||
session.value.focus();
|
||||
if (session.value.connected) {
|
||||
session.value.fit();
|
||||
session.value.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-zoom-out',
|
||||
content: '减小字号',
|
||||
click: () => {
|
||||
if (session.value?.connected) {
|
||||
if (session.value) {
|
||||
session.value.setOption('fontSize', session.value.getOption('fontSize') - 1);
|
||||
session.value.fit();
|
||||
session.value.focus();
|
||||
if (session.value.connected) {
|
||||
session.value.fit();
|
||||
session.value.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-eraser',
|
||||
icon: 'icon-delete',
|
||||
content: '清空',
|
||||
click: () => {
|
||||
session.value?.clear();
|
||||
session.value?.focus();
|
||||
}
|
||||
}, {
|
||||
icon: 'icon-poweroff',
|
||||
content: '关闭',
|
||||
disabled: session.value?.connected,
|
||||
click: () => {
|
||||
if (session.value?.connected) {
|
||||
session.value.logout();
|
||||
}
|
||||
session.value?.logout();
|
||||
}
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
// 初始化会话
|
||||
onMounted(async () => {
|
||||
@@ -190,6 +249,10 @@
|
||||
|
||||
&-right {
|
||||
justify-content: flex-end;
|
||||
|
||||
.command-input {
|
||||
width: 36%;
|
||||
}
|
||||
}
|
||||
|
||||
&-right-icon-actions {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
OutputPayload
|
||||
} from '../types/terminal.type';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import { TerminalStatus } from '../types/terminal.const';
|
||||
|
||||
// 终端输出消息体处理器实现
|
||||
export default class TerminalOutputProcessor implements ITerminalOutputProcessor {
|
||||
@@ -25,6 +26,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
// 未成功展示错误信息
|
||||
if (!success) {
|
||||
session.write(`[91m${msg || ''}[0m`);
|
||||
session.status = TerminalStatus.CLOSED;
|
||||
return;
|
||||
}
|
||||
// 发送 connect 命令
|
||||
@@ -38,6 +40,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
// 未成功展示错误信息
|
||||
if (!success) {
|
||||
session.write(`[91m${msg || ''}[0m`);
|
||||
session.status = TerminalStatus.CLOSED;
|
||||
return;
|
||||
}
|
||||
// 设置可写
|
||||
@@ -54,6 +57,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
// 提示消息
|
||||
session.write(`\r\n[91m${msg || ''}[0m`);
|
||||
// 设置状态
|
||||
session.status = TerminalStatus.CLOSED;
|
||||
session.connected = false;
|
||||
// 设置不可写
|
||||
session.setCanWrite(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { fontFamilySuffix } from '../types/terminal.const';
|
||||
import { fontFamilySuffix, TerminalStatus } from '../types/terminal.const';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import { ITerminalOptions, Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
@@ -23,6 +23,8 @@ export default class TerminalSession implements ITerminalSession {
|
||||
|
||||
public canWrite: boolean;
|
||||
|
||||
public status: number;
|
||||
|
||||
private readonly sessionId: string;
|
||||
|
||||
private readonly channel: ITerminalChannel;
|
||||
@@ -37,6 +39,7 @@ export default class TerminalSession implements ITerminalSession {
|
||||
this.channel = channel;
|
||||
this.connected = false;
|
||||
this.canWrite = false;
|
||||
this.status = TerminalStatus.CONNECTING;
|
||||
this.inst = undefined as unknown as Terminal;
|
||||
this.addons = {} as TerminalAddons;
|
||||
}
|
||||
@@ -65,6 +68,7 @@ export default class TerminalSession implements ITerminalSession {
|
||||
|
||||
// 设置已连接
|
||||
connect(): void {
|
||||
this.status = TerminalStatus.CONNECTED;
|
||||
this.connected = true;
|
||||
// 注册输入事件
|
||||
this.inst.onData(s => {
|
||||
@@ -98,7 +102,7 @@ export default class TerminalSession implements ITerminalSession {
|
||||
}
|
||||
|
||||
// 写入数据
|
||||
write(value: string): void {
|
||||
write(value: string | Uint8Array): void {
|
||||
this.inst.write(value);
|
||||
}
|
||||
|
||||
@@ -128,11 +132,26 @@ export default class TerminalSession implements ITerminalSession {
|
||||
// 选中全部
|
||||
selectAll(): void {
|
||||
this.inst.selectAll();
|
||||
this.inst.focus();
|
||||
}
|
||||
|
||||
// 获取选中
|
||||
getSelection(): string {
|
||||
return this.inst.getSelection();
|
||||
const selection = this.inst.getSelection();
|
||||
this.inst.focus();
|
||||
return selection;
|
||||
}
|
||||
|
||||
// 去顶部
|
||||
toTop(): void {
|
||||
this.inst.scrollToTop();
|
||||
this.inst.focus();
|
||||
}
|
||||
|
||||
// 去底部
|
||||
toBottom(): void {
|
||||
this.inst.scrollToBottom();
|
||||
this.inst.focus();
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
|
||||
@@ -5,6 +5,8 @@ import { getUUID } from '@/utils';
|
||||
export interface SidebarAction {
|
||||
icon: string;
|
||||
content: string;
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
iconStyle?: CSSProperties;
|
||||
click: () => void;
|
||||
}
|
||||
@@ -60,6 +62,16 @@ export const ExtraSshAuthType = {
|
||||
CUSTOM_IDENTITY: 'CUSTOM_IDENTITY',
|
||||
};
|
||||
|
||||
// 终端状态
|
||||
export const TerminalStatus = {
|
||||
// 连接中
|
||||
CONNECTING: 0,
|
||||
// 已连接
|
||||
CONNECTED: 1,
|
||||
// 已断开
|
||||
CLOSED: 2
|
||||
};
|
||||
|
||||
// 获取会话id
|
||||
export const nextSessionId = (): string => {
|
||||
return getUUID().replaceAll('-', '').substring(0, 10);
|
||||
@@ -87,15 +99,18 @@ export const fontWeightKey = 'terminalFontWeight';
|
||||
export const cursorStyleKey = 'terminalCursorStyle';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const NewConnectionTypeKey = 'terminalNewConnectionType';
|
||||
export const newConnectionTypeKey = 'terminalNewConnectionType';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const extraSshAuthTypeKey = 'hostExtraSshAuthType';
|
||||
|
||||
// 终端状态
|
||||
export const connectStatusKey = 'terminalConnectStatus';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [
|
||||
darkThemeKey, fontFamilyKey,
|
||||
fontSizeKey, fontWeightKey,
|
||||
cursorStyleKey, NewConnectionTypeKey,
|
||||
extraSshAuthTypeKey
|
||||
cursorStyleKey, newConnectionTypeKey,
|
||||
extraSshAuthTypeKey, connectStatusKey
|
||||
];
|
||||
|
||||
@@ -97,6 +97,8 @@ export interface ITerminalSession {
|
||||
connected: boolean;
|
||||
// 是否可写
|
||||
canWrite: boolean;
|
||||
// 状态
|
||||
status: number;
|
||||
|
||||
// 初始化
|
||||
init: (dom: HTMLElement) => void;
|
||||
@@ -105,7 +107,7 @@ export interface ITerminalSession {
|
||||
// 设置是否可写
|
||||
setCanWrite: (canWrite: boolean) => void;
|
||||
// 写入数据
|
||||
write: (value: string) => void;
|
||||
write: (value: string | Uint8Array) => void;
|
||||
// 自适应
|
||||
fit: () => void;
|
||||
// 聚焦
|
||||
@@ -118,6 +120,10 @@ export interface ITerminalSession {
|
||||
selectAll: () => void;
|
||||
// 获取选中
|
||||
getSelection: () => string;
|
||||
// 去顶部
|
||||
toTop: () => void;
|
||||
// 去底部
|
||||
toBottom: () => void;
|
||||
// 获取配置
|
||||
getOption: (option: string) => any;
|
||||
// 设置配置
|
||||
|
||||
Reference in New Issue
Block a user