🔨 命令发送.

This commit is contained in:
lijiahangmax
2024-12-16 20:54:35 +08:00
parent 6d74b4379e
commit 1227ed1770
16 changed files with 317 additions and 167 deletions

View File

@@ -1,18 +1,18 @@
spring:
datasource:
druid:
url: jdbc:mysql://116.62.194.246:3306/orion_visor?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true
url: jdbc:mysql://127.0.0.1:3306/orion_visor?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true
username: root
password: Orionsec@0379
password: Data@123456
initial-size: 0
min-idle: 1
max-active: 5
stat-view-servlet:
enabled: false
redis:
host: 116.62.194.246
host: 127.0.0.1
port: 6379
password: Orionsec@0379
password: Data@123456
redisson:
threads: 2
netty-threads: 2

View File

@@ -259,11 +259,6 @@ public class TerminalPreferenceModel implements GenericsDataModel {
@AllArgsConstructor
public static class ActionBarSettingModel implements IJsonObject {
/**
* 命令输入框
*/
private Boolean commandInput;
/**
* 连接状态
*/

View File

@@ -115,7 +115,6 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
.toJsonString();
// 操作栏设置
String actionBarSetting = TerminalPreferenceModel.ActionBarSettingModel.builder()
.commandInput(false)
.connectStatus(true)
.toTop(false)
.toBottom(false)

View File

@@ -65,6 +65,7 @@ export default defineStore('terminal', {
keys: []
} as TerminalShortcutSetting,
},
commandBarVisible: false,
hosts: {} as AuthorizedHostQueryResponse,
tabManager: new TerminalTabManager(),
panelManager: new TerminalPanelManager(),
@@ -124,6 +125,11 @@ export default defineStore('terminal', {
}
},
// 修改命令发送显示
setCommandBarVisible(visible: boolean) {
this.commandBarVisible = visible;
},
// 加载主机列表
async loadHosts() {
if (this.hosts.hostList?.length) {

View File

@@ -4,6 +4,7 @@ import type { TerminalTheme } from '@/api/asset/terminal';
export interface TerminalState {
preference: TerminalPreference;
commandBarVisible: boolean;
hosts: AuthorizedHostQueryResponse;
tabManager: ITerminalTabManager;
panelManager: ITerminalPanelManager;
@@ -38,7 +39,6 @@ export interface TerminalDisplaySetting {
// 操作栏设置
export interface TerminalActionBarSetting {
commandInput?: boolean;
connectStatus?: boolean;
[key: string]: unknown;

View File

@@ -0,0 +1,113 @@
<template>
<div class="command-bar">
<div class="command-header">
<!-- 左侧按钮 -->
<div class="command-header-left">
<!-- 粘贴 -->
<a-button size="mini"
class="mr8"
@click="paste">
粘贴
<template #icon>
<icon-send />
</template>
</a-button>
<!-- 清空 -->
<a-button size="mini" @click="clear">
清空
<template #icon>
<icon-delete />
</template>
</a-button>
</div>
<!-- 右侧按钮 -->
<div class="command-header-right">
<!-- 隐藏 -->
<a-button size="mini" @click="setCommandBarVisible(false)">
<template #icon>
<icon-down />
</template>
</a-button>
</div>
</div>
<div class="command-body">
<!-- 命令框 -->
<div class="command-input">
<a-textarea v-model="text"
placeholder="输入命令, F8 发送"
:auto-size="{ minRows: 3, maxRows: 3 }"
@keyup="checkCommandKey" />
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'commandBar'
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import { useTerminalStore } from '@/store';
const { setCommandBarVisible, appendCommandToCurrentSession } = useTerminalStore();
const text = ref('');
// 粘贴
const paste = () => {
appendCommandToCurrentSession(text.value);
text.value = '';
};
// 清空
const clear = () => {
text.value = '';
};
// 检查命令快捷键
const checkCommandKey = async (e: KeyboardEvent) => {
if (text.value && e.code === 'F8') {
paste();
}
};
</script>
<style lang="less" scoped>
.command-bar {
height: 122px;
}
.command-header {
border-top: 1px var(--color-bg-sidebar) solid;
width: 100%;
height: 36px;
padding: 0 12px;
display: flex;
justify-content: space-between;
&-left, &-right {
display: flex;
align-items: center;
}
}
.command-body {
width: 100%;
height: 92px;
padding: 0 12px 12px 12px;
display: flex;
.command-input {
width: 100%;
:deep(textarea) {
border-radius: 4px;
}
}
}
</style>

View File

@@ -23,6 +23,8 @@
</a-tabs>
<!-- 承载页推荐 -->
<empty-recommend v-else />
<!-- 底部发送命令 -->
<command-bar v-if="commandBarVisible" />
</div>
</template>
@@ -45,10 +47,11 @@
import TerminalThemeSetting from '../setting/theme/terminal-theme-setting.vue';
import TerminalGeneralSetting from '../setting/general/terminal-general-setting.vue';
import TerminalShortcutSetting from '../setting/shortcut/terminal-shortcut-setting.vue';
import CommandBar from '../command-bar/index.vue';
const emits = defineEmits(['openCommandSnippet', 'openPathBookmark', 'openTransferList', 'screenshot']);
const emits = defineEmits(['openCommandSnippet', 'openPathBookmark', 'openTransferList', 'openCommandBar', 'screenshot']);
const { preference, tabManager, getCurrentSession } = useTerminalStore();
const { commandBarVisible, preference, tabManager, getCurrentSession } = useTerminalStore();
// 监听 tab 切换
watch(() => tabManager.active, (active, before) => {
@@ -111,6 +114,10 @@
// 打开文件传输列表
emits('openTransferList');
break;
case TerminalShortcutKeys.OPEN_COMMAND_BAR:
// 打开发送命令
emits('openCommandBar');
break;
case TerminalShortcutKeys.SCREENSHOT:
// 截图
emits('screenshot');

View File

@@ -21,7 +21,7 @@
import type { SidebarAction } from '../../types/define';
import IconActions from './icon-actions.vue';
const emits = defineEmits(['openCommandSnippet', 'openPathBookmark', 'openTransferList', 'screenshot']);
const emits = defineEmits(['openCommandSnippet', 'openPathBookmark', 'openTransferList', 'openCommandBar', 'screenshot']);
// 顶部操作
const topActions = [
@@ -46,6 +46,10 @@
// 底部操作
const bottomActions: Array<SidebarAction> = [
{
icon: 'icon-send',
content: '发送命令',
click: () => emits('openCommandBar')
}, {
icon: 'icon-camera',
content: '截图',
click: () => emits('screenshot')

View File

@@ -24,12 +24,6 @@
:actions="actions"
position="bottom" />
</a-form-item>
<!-- 命令输入框 -->
<a-form-item field="commandInput" label="命令输入框">
<a-switch v-model="formModel.commandInput"
:default-checked="true"
type="round" />
</a-form-item>
<!-- 终端连接状态 -->
<a-form-item field="showStatus" label="终端连接状态">
<a-switch v-model="formModel.connectStatus"

View File

@@ -152,6 +152,7 @@
// 非初始化则修改终端样式
if (before) {
Object.values(sessionManager.sessions)
.filter(Boolean)
.filter(s => s.type === PanelSessionType.SSH.type)
.map(s => s as ISshSession)
.forEach(s => {

View File

@@ -77,6 +77,7 @@
document.body.setAttribute('terminal-theme', theme.dark ? 'dark' : 'light');
// 修改终端主题
Object.values(sessionManager.sessions)
.filter(Boolean)
.filter(s => s.type === PanelSessionType.SSH.type)
.map(s => s as ISshSession)
.forEach(s => {

View File

@@ -12,7 +12,7 @@
<a-doption v-for="(action, index) in actions"
:key="index"
:disabled="!session.handler.enabledStatus(action.item)"
@click="emits('click', action.item)">
@click="emits('handle', action.item)">
<!-- 图标 -->
<div class="terminal-context-menu-icon">
<component :is="action.icon" />
@@ -39,7 +39,7 @@
session?: ISshSession;
}>();
const emits = defineEmits(['click']);
const emits = defineEmits(['handle']);
const { preference } = useTerminalStore();

View File

@@ -0,0 +1,152 @@
<template>
<!-- 头部 -->
<div class="ssh-header">
<!-- 左侧操作 -->
<div class="ssh-header-left">
<!-- 主机地址 -->
<span class="address-wrapper">
<span class="text-copy"
:title="address"
@click="copy(address as string, true)">
{{ address }}
</span>
</span>
</div>
<!-- 右侧操作 -->
<div class="ssh-header-right">
<!-- 操作按钮 -->
<icon-actions class="ssh-header-right-action-bar"
wrapper-class="ssh-header-icon-wrapper"
icon-class="ssh-header-icon"
:actions="rightActions"
position="bottom" />
<!-- 连接状态 -->
<a-badge v-if="preference.actionBarSetting.connectStatus !== false"
class="status-bridge"
:status="getDictValue(sessionStatusKey, session ? session.status : 0, 'status')"
:text="getDictValue(sessionStatusKey, session ? session.status : 0)" />
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'sshHeader'
};
</script>
<script lang="ts" setup>
import type { ISshSession, SidebarAction } from '../../types/define';
import { computed } from 'vue';
import { useDictStore, useTerminalStore } from '@/store';
import { ActionBarItems, sessionStatusKey } from '../../types/const';
import { copy } from '@/hooks/copy';
import IconActions from '../layout/icon-actions.vue';
const emits = defineEmits(['handle']);
const props = defineProps<{
address: string;
session?: ISshSession;
}>();
const { getDictValue } = useDictStore();
const { preference } = useTerminalStore();
// 右侧操作
const rightActions = computed<Array<SidebarAction>>(() => {
return ActionBarItems.map(s => {
return {
icon: s.icon,
content: s.content,
visible: preference.actionBarSetting[s.item] !== false,
disabled: props.session?.handler.enabledStatus(s.item) === false,
click: () => emits('handle', s.item)
};
});
});
</script>
<style lang="less" scoped>
@ssh-header-height: 36px;
.ssh-header {
width: 100%;
height: @ssh-header-height;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--color-bg-panel-bar);
&-left, &-right {
display: flex;
align-items: center;
height: 100%;
}
&-left {
width: 25%;
.address-wrapper {
height: 100%;
display: inline-flex;
align-items: center;
user-select: none;
&:before {
content: 'IP:';
padding-right: 4px;
}
}
}
&-right {
width: 75%;
justify-content: flex-end;
.command-input {
width: 36%;
user-select: none;
}
}
&-right-action-bar {
display: flex;
:deep(.ssh-header-icon-wrapper) {
width: 28px;
height: 28px;
margin: 0 2px;
}
:deep(.ssh-header-icon) {
width: 28px;
height: 28px;
font-size: 20px;
}
}
.status-bridge {
height: 100%;
margin: 0 2px 0 8px;
display: flex;
align-items: center;
user-select: none;
:deep(.arco-badge-status-text) {
width: 36px;
}
&::before {
content: "";
height: 56%;
margin: 0 12px 0 6px;
border-left: 2px solid var(--color-fill-4);
border-radius: 2px;
}
}
}
</style>

View File

@@ -1,44 +1,12 @@
<template>
<div class="ssh-container">
<!-- 头部 -->
<div class="ssh-header">
<!-- 左侧操作 -->
<div class="ssh-header-left">
<!-- 主机地址 -->
<span class="address-wrapper">
<span class="text-copy"
:title="tab.address"
@click="copy(tab.address as string, true)">
{{ tab.address }}
</span>
</span>
</div>
<!-- 右侧操作 -->
<div class="ssh-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="ssh-header-right-action-bar"
wrapper-class="ssh-header-icon-wrapper"
icon-class="ssh-header-icon"
:actions="rightActions"
position="bottom" />
<!-- 连接状态 -->
<a-badge v-if="preference.actionBarSetting.connectStatus !== false"
class="status-bridge"
:status="getDictValue(sessionStatusKey, session ? session.status : 0, 'status')"
:text="getDictValue(sessionStatusKey, session ? session.status : 0)" />
</div>
</div>
<ssh-header :address="tab.address"
:session="session"
@handle="doTerminalHandle" />
<!-- 终端右键菜单 -->
<ssh-context-menu :session="session"
@click="doTerminalHandle">
@handle="doTerminalHandle">
<!-- 终端容器 -->
<div class="ssh-wrapper"
:style="{ background: preference.theme.schema.background }">
@@ -49,10 +17,11 @@
class="search-modal"
@find="findWords"
@close="focus" />
<!-- 上传文件模态框 -->
<sftp-upload-modal ref="uploadModal" @closed="focus" />
</div>
</ssh-context-menu>
<!-- 上传文件模态框 -->
<sftp-upload-modal ref="uploadModal" @closed="focus" />
<!-- 命令编辑器 -->
<shell-editor-modal ref="editorModal"
:closable="false"
@@ -71,13 +40,11 @@
</script>
<script lang="ts" setup>
import type { ISshSession, TerminalPanelTabItem, SidebarAction } from '../../types/define';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import type { ISshSession, TerminalPanelTabItem } from '../../types/define';
import { onMounted, onUnmounted, ref } from 'vue';
import { useDictStore, useTerminalStore } from '@/store';
import { copy } from '@/hooks/copy';
import { ActionBarItems, sessionStatusKey } from '../../types/const';
import SshHeader from './ssh-header.vue';
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
import IconActions from '../layout/icon-actions.vue';
import SshContextMenu from './ssh-context-menu.vue';
import SftpUploadModal from '../sftp/sftp-upload-modal.vue';
import XtermSearchModal from '@/components/xterm/search-modal/index.vue';
@@ -92,19 +59,9 @@
const editorModal = ref();
const searchModal = ref();
const uploadModal = ref();
const commandInput = ref();
const terminalRef = ref();
const session = ref<ISshSession>();
// 发送命令
const writeCommandInput = async (e: KeyboardEvent) => {
const value = commandInput.value;
if (value && e.code === 'F8') {
writeCommand(value);
commandInput.value = undefined;
}
};
// 发送命令
const writeCommand = (value: string) => {
if (session.value?.canWrite) {
@@ -127,19 +84,6 @@
session.value?.handler.invokeHandle.call(session.value?.handler, handle);
};
// 右侧操作
const rightActions = computed<Array<SidebarAction>>(() => {
return ActionBarItems.map(s => {
return {
icon: s.icon,
content: s.content,
visible: preference.actionBarSetting[s.item] !== false,
disabled: session.value?.handler.enabledStatus(s.item) === false,
click: () => doTerminalHandle(s.item)
};
});
});
// 初始化会话
onMounted(async () => {
// 创建终端处理器
@@ -167,84 +111,6 @@
position: relative;
}
.ssh-header {
width: 100%;
height: @ssh-header-height;
padding: 0 8px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--color-bg-panel-bar);
&-left, &-right {
display: flex;
align-items: center;
height: 100%;
}
&-left {
width: 25%;
.address-wrapper {
height: 100%;
display: inline-flex;
align-items: center;
user-select: none;
&:before {
content: 'IP:';
padding-right: 4px;
}
}
}
&-right {
width: 75%;
justify-content: flex-end;
.command-input {
width: 36%;
user-select: none;
}
}
&-right-action-bar {
display: flex;
:deep(.ssh-header-icon-wrapper) {
width: 28px;
height: 28px;
margin: 0 2px;
}
:deep(.ssh-header-icon) {
width: 28px;
height: 28px;
font-size: 20px;
}
}
.status-bridge {
height: 100%;
margin: 0 2px 0 8px;
display: flex;
align-items: center;
user-select: none;
:deep(.arco-badge-status-text) {
width: 36px;
}
&::before {
content: "";
height: 56%;
margin: 0 12px 0 6px;
border-left: 2px solid var(--color-fill-4);
border-radius: 2px;
}
}
}
.ssh-wrapper {
width: 100%;
height: calc(100% - @ssh-header-height);

View File

@@ -21,6 +21,7 @@
@open-command-snippet="() => snippetRef.open()"
@open-path-bookmark="() => pathRef.open()"
@open-transfer-list="() => transferRef.open()"
@open-command-bar="setCommandBarVisible(true)"
@screenshot="screenshot" />
</main>
<!-- 右侧操作栏 -->
@@ -28,6 +29,7 @@
<right-sidebar @open-command-snippet="() => snippetRef.open()"
@open-path-bookmark="() => pathRef.open()"
@open-transfer-list="() => transferRef.open()"
@open-command-bar="setCommandBarVisible(true)"
@screenshot="screenshot" />
</div>
</main>
@@ -75,7 +77,11 @@
import '@/assets/style/host-terminal-layout.less';
import '@xterm/xterm/css/xterm.css';
const { fetchPreference, getCurrentSession, openSession, preference, loadHosts, hosts, tabManager } = useTerminalStore();
const {
fetchPreference, getCurrentSession, openSession,
preference, loadHosts, hosts, tabManager,
setCommandBarVisible
} = useTerminalStore();
const { loading, setLoading } = useLoading(true);
const { enter: enterFull, exit: exitFull } = useFullscreen();
const route = useRoute();

View File

@@ -211,6 +211,8 @@ export const TerminalShortcutKeys = {
OPEN_PATH_BOOKMARK: 'openPathBookmark',
// 打开文件传输列表
OPEN_TRANSFER_LIST: 'openTransferList',
// 打开发送命令
OPEN_COMMAND_BAR: 'openCommandBar',
// 截图
SCREENSHOT: 'screenshot',
// 打开新建连接弹框
@@ -257,6 +259,10 @@ export const TerminalShortcutItems: Array<ShortcutKeyItem> = [
item: TerminalShortcutKeys.OPEN_TRANSFER_LIST,
content: '打开文件传输列表',
type: TerminalShortcutType.GLOBAL
}, {
item: TerminalShortcutKeys.OPEN_COMMAND_BAR,
content: '打开发送命令',
type: TerminalShortcutType.GLOBAL
}, {
item: TerminalShortcutKeys.SCREENSHOT,
content: '截图',