feat: 终端操作栏配置化.

This commit is contained in:
lijiahang
2024-01-11 16:55:37 +08:00
parent 8e03368e85
commit 25acde6d1e
16 changed files with 409 additions and 221 deletions

View File

@@ -1,4 +1,4 @@
import type { TerminalDisplaySetting, TerminalPreference, TerminalState } from './types';
import type { TerminalActionBarSetting, TerminalDisplaySetting, TerminalPreference, TerminalState } from './types';
import type { TerminalTheme } from '@/api/asset/host-terminal';
import { getTerminalThemes } from '@/api/asset/host-terminal';
import { defineStore } from 'pinia';
@@ -15,14 +15,17 @@ export const PreferenceItem = {
THEME: 'theme',
// 显示设置
DISPLAY_SETTING: 'displaySetting',
// 操作栏设置
ACTION_BAR_SETTING: 'actionBarSetting',
};
export default defineStore('terminal', {
state: (): TerminalState => ({
preference: {
newConnectionType: 'group',
theme: {} as TerminalTheme,
displaySetting: {} as TerminalDisplaySetting,
theme: {} as TerminalTheme
actionBarSetting: {} as TerminalActionBarSetting,
},
tabManager: new TerminalTabManager(),
sessionManager: new TerminalSessionManager()

View File

@@ -10,8 +10,9 @@ export interface TerminalState {
// 终端配置
export interface TerminalPreference {
newConnectionType: string;
displaySetting: TerminalDisplaySetting;
theme: TerminalTheme;
displaySetting: TerminalDisplaySetting;
actionBarSetting: TerminalActionBarSetting;
}
// 显示设置
@@ -25,3 +26,11 @@ export interface TerminalDisplaySetting {
cursorBlink?: boolean;
}
// 操作栏设置
export interface TerminalActionBarSetting {
commandInput?: boolean;
connectStatus?: boolean;
[key: string]: unknown;
}

View File

@@ -192,11 +192,14 @@ body[terminal-theme='dark'] .arco-modal-container {
background: var(--color-sidebar-icon-bg);
}
&.checked-item {
background: var(--color-sidebar-icon-bg);
}
&.disabled-item {
cursor: not-allowed;
}
}
}
// 终端设置容器
@@ -239,6 +242,14 @@ body[terminal-theme='dark'] .arco-modal-container {
.terminal-setting-body {
display: flex;
&.block-body {
display: flex;
width: 100%;
padding: 16px;
border: 1px solid var(--color-fill-4);
border-radius: 4px;
}
}
}

View File

@@ -9,7 +9,11 @@
:content="action.content">
<div class="terminal-sidebar-icon-wrapper" v-if="action.visible !== false">
<div class="terminal-sidebar-icon"
:class="[iconClass, action.disabled !== false ? '' : 'disabled-item']"
:class="[
iconClass,
action.disabled !== false ? '' : 'disabled-item',
action.checked === true ? 'checked-item' : '',
]"
@click="action.disabled !== false ? action.click() : false">
<component :is="action.icon" :style="action?.iconStyle" />
</div>

View File

@@ -85,12 +85,13 @@
}
&-logo-text {
height: var(--header-height);
margin: 0;
display: flex;
align-items: center;
padding: 0 8px;
font-size: 16px;
overflow: hidden;
white-space: nowrap;
}
&-tabs {

View File

@@ -0,0 +1,120 @@
<template>
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
操作栏设置
</h3>
</div>
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 立即生效 (无需刷新页面)</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body block-body setting-body">
<a-form class="terminal-setting-form"
:model="formModel"
layout="vertical">
<a-space>
<!-- 顶部操作按钮 -->
<a-form-item field="actions" label="顶部操作按钮">
<icon-actions class="form-item-actions"
:actions="actions"
position="bottom" />
</a-form-item>
<!-- 命令输入框 -->
<a-form-item field="showCommandInput" label="命令输入框">
<a-switch v-model="formModel.commandInput"
class="form-item-command-input"
:default-checked="true"
type="round" />
</a-form-item>
<!-- 连接状态 -->
<a-form-item field="showStatus" label="连接状态">
<a-switch v-model="formModel.connectStatus"
:default-checked="true"
type="round" />
</a-form-item>
</a-space>
</a-form>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'TerminalActionBarBlock'
};
</script>
<script lang="ts" setup>
import type { TerminalActionBarSetting } from '@/store/modules/terminal/types';
import type { SidebarAction } from '../../types/terminal.type';
import { computed, ref, watch } from 'vue';
import { useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { ActionBarItems } from '../../types/terminal.const';
import IconActions from '../layout/icon-actions.vue';
const { preference, updateTerminalPreference } = useTerminalStore();
const formModel = ref<TerminalActionBarSetting>({ ...preference.actionBarSetting });
// 监听同步
watch(formModel, (v) => {
if (!v) {
return;
}
// 同步
updateTerminalPreference(PreferenceItem.ACTION_BAR_SETTING, formModel.value, true);
}, { deep: true });
// 右侧操作
const actions = computed<Array<SidebarAction>>(() => {
return ActionBarItems.map(s => {
return {
icon: s.icon,
content: (formModel.value[s.item] === false ? '显示 ' : '隐藏 ') + s.content,
checked: formModel.value[s.item] !== false,
click: () => {
formModel.value[s.item] = formModel.value[s.item] === false;
}
};
});
});
</script>
<style lang="less" scoped>
.form-item-actions {
display: flex;
background-color: var(--color-fill-2);
padding: 4px;
border-radius: 4px;
:deep(.terminal-sidebar-icon-wrapper) {
width: 36px;
height: 36px;
}
:deep(.terminal-sidebar-icon) {
width: 28px;
height: 28px;
font-size: 20px;
}
}
.form-item-actions, .form-item-command-input {
margin-right: 48px;
}
:deep(.arco-form) {
.arco-form-item-label {
user-select: none;
}
.arco-form-item {
margin-bottom: 0;
}
}
</style>

View File

@@ -3,78 +3,80 @@
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
显示设置
显示偏好
</h3>
</div>
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body">
<div class="terminal-setting-form">
<a-form :model="formModel" layout="vertical">
<a-space>
<!-- 字体样式 -->
<a-form-item field="fontFamily" label="字体样式">
<a-select v-model="formModel.fontFamily"
class="form-item form-item-font-family"
placeholder="请选择字体样式"
:options="toOptions(fontFamilyKey)"
:allow-create="true"
:filter-option="labelFilter">
<template #option="{ data }">
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
</template>
</a-select>
</a-form-item>
<!-- 字体大小 -->
<a-form-item field="fontSize" label="字体大小">
<a-select v-model="formModel.fontSize"
class="form-item form-item-font-size"
placeholder="请选择字体大小"
:options="toOptions(fontSizeKey)" />
</a-form-item>
<!-- 行高 -->
<a-form-item field="lineHeight" label="行高">
<a-input-number v-model="formModel.lineHeight"
class="form-item form-item-line-height"
placeholder="请输入行高"
:precision="2"
:min="1"
:max="2"
hide-button />
</a-form-item>
</a-space>
<a-space>
<!-- 普通文本字重 -->
<a-form-item field="fontWeight" label="普通文本字重">
<a-select v-model="formModel.fontWeight"
class="form-item form-item-font-weight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
<!-- 加粗文本字重 -->
<a-form-item field="fontWeightBold" label="加粗文本字重">
<a-select v-model="formModel.fontWeightBold"
class="form-item form-item-font-bold-weight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
</a-space>
<a-space>
<!-- 光标样式 -->
<a-form-item field="cursorStyle" label="光标样式">
<a-radio-group type="button"
v-model="formModel.cursorStyle"
class="form-item form-item-cursor-style usn"
:options="toRadioOptions(cursorStyleKey)" />
</a-form-item>
<!-- 光标闪烁 -->
<a-form-item field="cursorBlink" label="光标是否闪烁">
<a-switch v-model="formModel.cursorBlink"
type="round"
class="form-item form-item-cursor-blink" />
</a-form-item>
</a-space>
</a-form>
</div>
<div class="terminal-setting-body block-body setting-body">
<a-form class="terminal-setting-form"
:model="formModel"
layout="vertical">
<a-space>
<!-- 字体样式 -->
<a-form-item field="fontFamily" label="字体样式">
<a-select v-model="formModel.fontFamily"
class="form-item-font-family"
placeholder="请选择字体样式"
:options="toOptions(fontFamilyKey)"
:allow-create="true"
:filter-option="labelFilter">
<template #option="{ data }">
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
</template>
</a-select>
</a-form-item>
<!-- 字体大小 -->
<a-form-item field="fontSize" label="字体大小">
<a-select v-model="formModel.fontSize"
class="form-item-font-size"
placeholder="请选择字体大小"
:options="toOptions(fontSizeKey)" />
</a-form-item>
<!-- 行高 -->
<a-form-item field="lineHeight" label="行高">
<a-input-number v-model="formModel.lineHeight"
class="form-item-line-height"
placeholder="请输入行高"
:precision="2"
:min="1"
:max="2"
hide-button />
</a-form-item>
</a-space>
<a-space>
<!-- 普通文本字重 -->
<a-form-item field="fontWeight" label="普通文本字重">
<a-select v-model="formModel.fontWeight"
class="form-item-font-weight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
<!-- 加粗文本字重 -->
<a-form-item field="fontWeightBold" label="加粗文本字重">
<a-select v-model="formModel.fontWeightBold"
class="form-item-font-bold-weight"
placeholder="请选择字重"
:options="toOptions(fontWeightKey)" />
</a-form-item>
</a-space>
<a-space>
<!-- 光标样式 -->
<a-form-item field="cursorStyle" label="光标样式">
<a-radio-group type="button"
v-model="formModel.cursorStyle"
class="form-item-cursor-style usn"
:options="toRadioOptions(cursorStyleKey)" />
</a-form-item>
<!-- 光标闪烁 -->
<a-form-item field="cursorBlink" label="光标是否闪烁">
<a-switch v-model="formModel.cursorBlink"
type="round"
class="form-item-cursor-blink" />
</a-form-item>
</a-space>
</a-form>
<!-- 预览区域 -->
<div class="terminal-example">
<span class="terminal-example-label">预览效果</span>
@@ -90,7 +92,7 @@
<script lang="ts">
export default {
name: 'TerminalFontBlock'
name: 'TerminalDisplayBlock'
};
</script>
@@ -109,15 +111,6 @@
const previewTerminal = ref();
const formModel = ref<TerminalDisplaySetting>({ ...preference.displaySetting });
// 监听主题变化 动态修改预览样式
watch(() => preference.theme, (v) => {
if (!v) {
return;
}
const options = previewTerminal.value?.term?.options;
options && (options.theme = v);
});
// 监听内容变化
watch(formModel, (v) => {
if (!v) {
@@ -136,7 +129,7 @@
}
});
// 同步
updateTerminalPreference(PreferenceItem.DISPLAY_SETTING, formModel.value);
updateTerminalPreference(PreferenceItem.DISPLAY_SETTING, formModel.value, true);
// 聚焦
previewTerminal.value.term.focus();
}, { deep: true });
@@ -146,13 +139,8 @@
<style lang="less" scoped>
@terminal-width: 458px;
.terminal-setting-body {
.setting-body {
height: 248px;
width: 100%;
padding: 16px;
border: 1px solid var(--color-fill-4);
border-radius: 4px;
display: flex;
justify-content: space-between;
}

View File

@@ -2,21 +2,24 @@
<div class="terminal-setting-container">
<div class="terminal-setting-wrapper">
<!-- 主标题 -->
<h2 class="terminal-setting-title">外观设置</h2>
<!-- 显示设置 -->
<h2 class="terminal-setting-title">显示设置</h2>
<!-- 显示偏好 -->
<terminal-display-block />
<!-- 顶部工具栏 -->
<terminal-action-bar-block />
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'TerminalViewSetting'
name: 'TerminalDisplaySetting'
};
</script>
<script lang="ts" setup>
import TerminalDisplayBlock from './terminal-display-block.vue';
import TerminalActionBarBlock from './terminal-action-bar-block.vue';
</script>

View File

@@ -17,19 +17,21 @@
</div>
<!-- 右侧操作 -->
<div class="terminal-header-right">
<!-- 代码输入框 -->
<!-- 命令输入框 -->
<a-textarea class="command-input mr8"
v-if="preference.actionBarSetting.commandInput !== false"
v-model="commandInput"
:auto-size="{ minRows: 1, maxRows: 1 }"
placeholder="F8 发送命令"
allow-clear
@keyup="writeCommandInput" />
<!-- 操作按钮 -->
<icon-actions class="terminal-header-right-icon-actions"
<icon-actions class="terminal-header-right-action-bar"
:actions="rightActions"
position="bottom" />
<!-- 状态 -->
<a-badge class="status-bridge"
<!-- 连接状态 -->
<a-badge v-if="preference.actionBarSetting.connectStatus !== false"
class="status-bridge"
:status="getDictValue(connectStatusKey, session ? session.status : 0, 'status')"
:text="getDictValue(connectStatusKey, session ? session.status : 0)" />
</div>
@@ -59,12 +61,11 @@
</script>
<script lang="ts" setup>
import type { SidebarAction } from '../../types/terminal.type';
import type { ITerminalSession, TerminalTabItem } from '../../types/terminal.type';
import type { ITerminalSession, TerminalTabItem, SidebarAction } from '../../types/terminal.type';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useDictStore, useTerminalStore } from '@/store';
import useCopy from '@/hooks/copy';
import { connectStatusKey } from '../../types/terminal.const';
import { ActionBarItems, connectStatusKey } from '../../types/terminal.const';
import IconActions from '../layout/icon-actions.vue';
import ShellEditorModal from '@/components/view/shell-editor/shell-editor-modal.vue';
@@ -74,7 +75,7 @@
const { copy, readText } = useCopy();
const { getDictValue } = useDictStore();
const { preference, sessionManager } = useTerminalStore();
const { preference, tabManager, sessionManager } = useTerminalStore();
const modal = ref();
const commandInput = ref();
@@ -127,104 +128,80 @@
session.value?.focus();
};
// 右侧操作
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();
}
}, {
icon: 'icon-copy',
content: '复制选中部分',
click: () => {
copy(session.value?.getSelection(), '已复制');
}
}, {
icon: 'icon-paste',
content: '粘贴',
disabled: session.value?.canWrite,
click: async () => {
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: () => {
modal.value.open('', '');
}
}, {
icon: 'icon-search',
content: '搜索',
click: () => {
}
}, {
icon: 'icon-zoom-in',
content: '增大字号',
click: () => {
if (session.value) {
session.value.setOption('fontSize', session.value.getOption('fontSize') + 1);
if (session.value.connected) {
session.value.fit();
session.value.focus();
}
// 操作禁用状态
const actionsDisableStatus = computed<Record<string, boolean | undefined>>(() => {
return {
paste: session.value?.canWrite,
interrupt: session.value?.canWrite,
enter: session.value?.canWrite,
commandEditor: session.value?.canWrite,
disconnect: session.value?.connected,
};
});
// 操作点击逻辑
const actionsClickHandler: Record<string, () => void> = {
// 去顶部
toTop: () => session.value?.toTop(),
// 去底部
toBottom: () => session.value?.toBottom(),
// 全选
checkAll: () => session.value?.selectAll(),
// 复制选中部分
copy: () => copy(session.value?.getSelection(), '已复制'),
// 粘贴
paste: async () => session.value?.paste(await readText()),
// ctrl + c
interrupt: () => session.value?.paste(String.fromCharCode(3)),
// 回车
enter: () => session.value?.paste(String.fromCharCode(13)),
// 命令编辑器
commandEditor: () => modal.value.open('', ''),
// 搜索
search: () => {
},
// 增大字号
fontSizePlus: () => {
if (session.value) {
session.value.setOption('fontSize', session.value.getOption('fontSize') + 1);
if (session.value.connected) {
session.value.fit();
session.value.focus();
}
}
}, {
icon: 'icon-zoom-out',
content: '减小字号',
click: () => {
if (session.value) {
session.value.setOption('fontSize', session.value.getOption('fontSize') - 1);
if (session.value.connected) {
session.value.fit();
session.value.focus();
}
}
}
}, {
icon: 'icon-delete',
content: '清空',
click: () => {
session.value?.clear();
}
}, {
icon: 'icon-poweroff',
content: '关闭',
disabled: session.value?.connected,
click: () => {
session.value?.logout();
}
},
]);
// 减小字号
fontSizeSubtract: () => {
if (session.value) {
session.value.setOption('fontSize', session.value.getOption('fontSize') - 1);
if (session.value.connected) {
session.value.fit();
session.value.focus();
}
}
},
// 清空
clear: () => session.value?.clear(),
// 断开连接
disconnect: () => session.value?.disconnect(),
// 关闭
close: () => tabManager.deleteTab(props.tab.key),
};
// 右侧操作
const rightActions = computed<Array<SidebarAction>>(() => {
return ActionBarItems.map(s => {
return {
icon: s.icon,
content: s.content,
visible: preference.actionBarSetting[s.item] !== false,
disabled: actionsDisableStatus.value[s.item] !== false,
click: () => {
actionsClickHandler[s.item] && actionsClickHandler[s.item]();
}
};
});
});
// 初始化会话
onMounted(async () => {
@@ -297,7 +274,7 @@
}
}
&-right-icon-actions {
&-right-action-bar {
display: flex;
:deep(.terminal-sidebar-icon-wrapper) {
@@ -315,6 +292,10 @@
.status-bridge {
margin: 0 2px 0 8px;
user-select: none;
:deep(.arco-badge-status-text) {
width: 36px;
}
}
}

View File

@@ -60,8 +60,8 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
if (!session) {
return;
}
// 登出
session.logout();
// 关闭连接
session.disconnect();
// 关闭 session
session.close();
// 移除 session

View File

@@ -165,8 +165,8 @@ export default class TerminalSession implements ITerminalSession {
this.inst.options[option as keyof ITerminalOptions] = value;
}
// 登出
logout(): void {
// 断开连接
disconnect(): void {
// 发送关闭消息
this.channel.send(InputProtocol.CLOSE, {
sessionId: this.sessionId

View File

@@ -94,7 +94,7 @@
&-header {
width: 100%;
height: 44px;
height: var(--header-height);
background: var(--color-bg-header);
position: relative;
z-index: 9999;

View File

@@ -68,6 +68,67 @@ export const TerminalStatus = {
CLOSED: 2
};
// 终端操作栏-操作项
export const ActionBarItems = [
{
item: 'toTop',
icon: 'icon-up',
content: '去顶部',
}, {
item: 'toBottom',
icon: 'icon-down',
content: '去底部',
}, {
item: 'checkAll',
icon: 'icon-expand',
content: '全选',
}, {
item: 'copy',
icon: 'icon-copy',
content: '复制选中部分',
}, {
item: 'paste',
icon: 'icon-paste',
content: '粘贴',
}, {
item: 'interrupt',
icon: 'icon-formula',
content: 'ctrl + c',
}, {
item: 'enter',
icon: 'icon-play-arrow-fill',
content: '回车',
}, {
item: 'commandEditor',
icon: 'icon-code-square',
content: '命令编辑器',
}, {
item: 'search',
icon: 'icon-search',
content: '搜索',
}, {
item: 'fontSizePlus',
icon: 'icon-zoom-in',
content: '增大字号',
}, {
item: 'fontSizeSubtract',
icon: 'icon-zoom-out',
content: '减小字号',
}, {
item: 'clear',
icon: 'icon-delete',
content: '清空',
}, {
item: 'disconnect',
icon: 'icon-poweroff',
content: '断开连接',
}, {
item: 'close',
icon: 'icon-close',
content: '关闭',
}
];
// 获取会话id
export const nextSessionId = (): string => {
return getUUID().replaceAll('-', '').substring(0, 10);

View File

@@ -21,6 +21,7 @@ export interface SidebarAction {
content: string;
visible?: boolean;
disabled?: boolean;
checked?: boolean;
iconStyle?: CSSProperties;
click: () => void;
}
@@ -161,8 +162,8 @@ export interface ITerminalSession {
getOption: (option: string) => any;
// 设置配置
setOption: (option: string, value: any) => void;
// 登出
logout: () => void;
// 断开连接
disconnect: () => void;
// 关闭
close: () => void;
}