前端业务拆分.
This commit is contained in:
248
orion-ops-ui/src/views/host/terminal/assets/styles/layout.less
Normal file
248
orion-ops-ui/src/views/host/terminal/assets/styles/layout.less
Normal file
@@ -0,0 +1,248 @@
|
||||
// 亮色主题配色常量
|
||||
body {
|
||||
--color-bg-header: #232323;
|
||||
--color-bg-sidebar: #F2F3F4;
|
||||
--color-bg-content: #FEFEFE;
|
||||
--color-sidebar-icon: #737070;
|
||||
--color-sidebar-icon-bg: #D7D8DB;
|
||||
--color-sidebar-tooltip-text: rgba(255, 255, 255, .9);
|
||||
--color-sidebar-tooltip-bg: rgb(29, 33, 41);
|
||||
--color-content-text-1: rgba(0, 0, 0, .8);
|
||||
--color-content-text-2: rgba(0, 0, 0, .85);
|
||||
--color-content-text-3: rgba(0, 0, 0, .95);
|
||||
}
|
||||
|
||||
// 暗色主题配色常量
|
||||
body[terminal-theme='dark'] {
|
||||
--color-bg-header: #232323;
|
||||
--color-bg-sidebar: #2C2E31;
|
||||
--color-bg-content: #1A1B1C;
|
||||
--color-sidebar-icon: #C3C8CE;
|
||||
--color-sidebar-icon-bg: #43444C;
|
||||
--color-sidebar-tooltip-text: rgba(255, 255, 255, .9);
|
||||
--color-sidebar-tooltip-bg: var(--color-sidebar-icon-bg);
|
||||
--color-content-text-1: rgba(255, 255, 255, .8);
|
||||
--color-content-text-2: rgba(255, 255, 255, .85);
|
||||
--color-content-text-3: rgba(255, 255, 255, .95);
|
||||
}
|
||||
|
||||
// 布局常量
|
||||
.host-layout {
|
||||
--header-height: 44px;
|
||||
--sidebar-width: 44px;
|
||||
--sidebar-icon-wrapper-size: var(--header-height);
|
||||
--sidebar-icon-size: 32px;
|
||||
--sidebar-icon-font-size: 22px;
|
||||
|
||||
--color-bg-header-icon-1: #434343;
|
||||
--color-header-tabs-bg: var(--color-bg-header);
|
||||
--color-header-tabs-bg-hover: #434343;
|
||||
--color-header-text-1: rgba(255, 255, 255, .75);
|
||||
--color-header-text-2: rgba(255, 255, 255, .9);
|
||||
--color-gradient-start: rgba(38, 38, 38, 1);
|
||||
--color-gradient-end: rgba(38, 38, 38, 0);
|
||||
}
|
||||
|
||||
// arco 亮色配色
|
||||
body .host-layout {
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
--color-border: rgb(var(--gray-3));
|
||||
--color-bg-popup: var(--color-bg-5);
|
||||
--color-bg-1: #fff;
|
||||
--color-bg-2: #fff;
|
||||
--color-bg-3: #fff;
|
||||
--color-bg-4: #fff;
|
||||
--color-bg-5: #fff;
|
||||
--color-bg-white: #fff;
|
||||
--color-neutral-1: rgb(var(--gray-1));
|
||||
--color-neutral-2: rgb(var(--gray-2));
|
||||
--color-neutral-3: rgb(var(--gray-3));
|
||||
--color-neutral-4: rgb(var(--gray-4));
|
||||
--color-neutral-5: rgb(var(--gray-5));
|
||||
--color-neutral-6: rgb(var(--gray-6));
|
||||
--color-neutral-7: rgb(var(--gray-7));
|
||||
--color-neutral-8: rgb(var(--gray-8));
|
||||
--color-neutral-9: rgb(var(--gray-9));
|
||||
--color-neutral-10: rgb(var(--gray-10));
|
||||
--color-text-1: var(--color-neutral-10);
|
||||
--color-text-2: var(--color-neutral-8);
|
||||
--color-text-3: var(--color-neutral-6);
|
||||
--color-text-4: var(--color-neutral-4);
|
||||
--color-border-1: var(--color-neutral-2);
|
||||
--color-border-2: var(--color-neutral-3);
|
||||
--color-border-3: var(--color-neutral-4);
|
||||
--color-border-4: var(--color-neutral-6);
|
||||
--color-fill-1: var(--color-neutral-1);
|
||||
--color-fill-2: var(--color-neutral-2);
|
||||
--color-fill-3: var(--color-neutral-3);
|
||||
--color-fill-4: var(--color-neutral-4);
|
||||
--color-primary-light-1: rgb(var(--primary-1));
|
||||
--color-primary-light-2: rgb(var(--primary-2));
|
||||
--color-primary-light-3: rgb(var(--primary-3));
|
||||
--color-primary-light-4: rgb(var(--primary-4));
|
||||
--color-link-light-1: rgb(var(--link-1));
|
||||
--color-link-light-2: rgb(var(--link-2));
|
||||
--color-link-light-3: rgb(var(--link-3));
|
||||
--color-link-light-4: rgb(var(--link-4));
|
||||
--color-secondary: var(--color-neutral-2);
|
||||
--color-secondary-hover: var(--color-neutral-3);
|
||||
--color-secondary-active: var(--color-neutral-4);
|
||||
--color-secondary-disabled: var(--color-neutral-1);
|
||||
--color-danger-light-1: rgb(var(--danger-1));
|
||||
--color-danger-light-2: rgb(var(--danger-2));
|
||||
--color-danger-light-3: rgb(var(--danger-3));
|
||||
--color-danger-light-4: rgb(var(--danger-4));
|
||||
--color-success-light-1: rgb(var(--success-1));
|
||||
--color-success-light-2: rgb(var(--success-2));
|
||||
--color-success-light-3: rgb(var(--success-3));
|
||||
--color-success-light-4: rgb(var(--success-4));
|
||||
--color-warning-light-1: rgb(var(--warning-1));
|
||||
--color-warning-light-2: rgb(var(--warning-2));
|
||||
--color-warning-light-3: rgb(var(--warning-3));
|
||||
--color-warning-light-4: rgb(var(--warning-4));
|
||||
--border-radius-none: 0;
|
||||
--border-radius-small: 2px;
|
||||
--border-radius-medium: 4px;
|
||||
--border-radius-large: 8px;
|
||||
--border-radius-circle: 50%;
|
||||
--color-tooltip-bg: rgb(var(--gray-10));
|
||||
--color-spin-layer-bg: rgba(255, 255, 255, 0.6);
|
||||
--color-menu-dark-bg: #232324;
|
||||
--color-menu-light-bg: #ffffff;
|
||||
--color-menu-dark-hover: rgba(255, 255, 255, 0.04);
|
||||
--color-mask-bg: rgba(29, 33, 41, 0.6);
|
||||
}
|
||||
|
||||
// arco 暗色配色
|
||||
body[terminal-theme='dark'] .host-layout {
|
||||
--color-white: rgba(255, 255, 255, 0.9);
|
||||
--color-black: #000000;
|
||||
--color-border: #333335;
|
||||
--color-bg-1: #17171a;
|
||||
--color-bg-2: #232324;
|
||||
--color-bg-3: #2a2a2b;
|
||||
--color-bg-4: #313132;
|
||||
--color-bg-5: #373739;
|
||||
--color-bg-white: #f6f6f6;
|
||||
--color-text-1: rgba(255, 255, 255, 0.9);
|
||||
--color-text-2: rgba(255, 255, 255, 0.7);
|
||||
--color-text-3: rgba(255, 255, 255, 0.5);
|
||||
--color-text-4: rgba(255, 255, 255, 0.3);
|
||||
--color-fill-1: rgba(255, 255, 255, 0.04);
|
||||
--color-fill-2: rgba(255, 255, 255, 0.08);
|
||||
--color-fill-3: rgba(255, 255, 255, 0.12);
|
||||
--color-fill-4: rgba(255, 255, 255, 0.16);
|
||||
--color-primary-light-1: rgba(var(--primary-6), 0.2);
|
||||
--color-primary-light-2: rgba(var(--primary-6), 0.35);
|
||||
--color-primary-light-3: rgba(var(--primary-6), 0.5);
|
||||
--color-primary-light-4: rgba(var(--primary-6), 0.65);
|
||||
--color-secondary: rgba(var(--gray-9), 0.08);
|
||||
--color-secondary-hover: rgba(var(--gray-8), 0.16);
|
||||
--color-secondary-active: rgba(var(--gray-7), 0.24);
|
||||
--color-secondary-disabled: rgba(var(--gray-9), 0.08);
|
||||
--color-danger-light-1: rgba(var(--danger-6), 0.2);
|
||||
--color-danger-light-2: rgba(var(--danger-6), 0.35);
|
||||
--color-danger-light-3: rgba(var(--danger-6), 0.5);
|
||||
--color-danger-light-4: rgba(var(--danger-6), 0.65);
|
||||
--color-success-light-1: rgb(var(--success-6), 0.2);
|
||||
--color-success-light-2: rgb(var(--success-6), 0.35);
|
||||
--color-success-light-3: rgb(var(--success-6), 0.5);
|
||||
--color-success-light-4: rgb(var(--success-6), 0.65);
|
||||
--color-warning-light-1: rgb(var(--warning-6), 0.2);
|
||||
--color-warning-light-2: rgb(var(--warning-6), 0.35);
|
||||
--color-warning-light-3: rgb(var(--warning-6), 0.5);
|
||||
--color-warning-light-4: rgb(var(--warning-6), 0.65);
|
||||
--color-link-light-1: rgb(var(--link-6), 0.2);
|
||||
--color-link-light-2: rgb(var(--link-6), 0.35);
|
||||
--color-link-light-3: rgb(var(--link-6), 0.5);
|
||||
--color-link-light-4: rgb(var(--link-6), 0.65);
|
||||
--color-tooltip-bg: #373739;
|
||||
--color-spin-layer-bg: rgba(51, 51, 51, 0.6);
|
||||
--color-menu-dark-bg: #232324;
|
||||
--color-menu-light-bg: #232324;
|
||||
--color-menu-dark-hover: var(--color-fill-2);
|
||||
--color-mask-bg: rgba(23, 23, 26, 0.6);
|
||||
}
|
||||
|
||||
// 侧栏图标
|
||||
.terminal-sidebar-icon-wrapper {
|
||||
width: var(--sidebar-icon-wrapper-size);
|
||||
height: var(--sidebar-icon-wrapper-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.terminal-sidebar-icon {
|
||||
width: var(--sidebar-icon-size);
|
||||
height: var(--sidebar-icon-size);
|
||||
font-size: var(--sidebar-icon-font-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-sidebar-icon);
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.1s cubic-bezier(0, 0, 1, 1);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-sidebar-icon-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tooltip 内容
|
||||
.terminal-tooltip-content {
|
||||
color: var(--color-sidebar-tooltip-text);
|
||||
background: var(--color-sidebar-tooltip-bg);
|
||||
}
|
||||
|
||||
// tooltip 箭头
|
||||
.terminal-tooltip-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 终端设置容器
|
||||
.terminal-setting-container {
|
||||
padding: 32px 16px 16px 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.terminal-setting-wrapper {
|
||||
min-width: 932px;
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal-setting-title {
|
||||
margin: 0 0 24px 0;
|
||||
user-select: none;
|
||||
font-size: 1.65em;
|
||||
color: var(--color-content-text-3);
|
||||
}
|
||||
|
||||
.terminal-setting-block {
|
||||
color: var(--color-content-text-2);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.terminal-setting-subtitle-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.terminal-setting-subtitle {
|
||||
margin: 0 0 16px 0;
|
||||
user-select: none;
|
||||
color: var(--color-content-text-3);
|
||||
}
|
||||
|
||||
.terminal-setting-body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-tooltip v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
:position="position as any"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-arrow"
|
||||
:content="action.content">
|
||||
<div class="terminal-sidebar-icon-wrapper">
|
||||
<div class="terminal-sidebar-icon"
|
||||
:class="iconClass"
|
||||
@click="action.click">
|
||||
<component :is="action.icon" :style="action?.iconStyle" />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'icon-actions'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../types/terminal.const';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
actions: Array as PropType<Array<SidebarAction>>,
|
||||
position: String,
|
||||
iconClass: String,
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="terminal-content">
|
||||
<!-- 内容 tabs -->
|
||||
<a-tabs v-model:active-key="terminalStore.tabs.active">
|
||||
<a-tab-pane v-for="tab in terminalStore.tabs.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title">
|
||||
<!-- 设置 -->
|
||||
<template v-if="tab.type === TabType.SETTING">
|
||||
<!-- 新建连接 -->
|
||||
<new-connection-view v-if="tab.key === InnerTabs.NEW_CONNECTION.key" />
|
||||
<!-- 显示设置 -->
|
||||
<terminal-view-setting v-else-if="tab.key === InnerTabs.VIEW_SETTING.key" />
|
||||
</template>
|
||||
<!-- 终端 -->
|
||||
<template v-else-if="tab.type === TabType.TERMINAL">
|
||||
<terminal-view>
|
||||
|
||||
</terminal-view>
|
||||
终端 {{ tab.key }}
|
||||
<div v-for="i in 1000" :key="i">
|
||||
{{ tab.title }}
|
||||
</div>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalContent'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TabType, InnerTabs } from '../../types/terminal.const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import TerminalViewSetting from '../view-setting/terminal-view-setting.vue';
|
||||
import NewConnectionView from '../new-connection/new-connection-view.vue';
|
||||
import TerminalView from '../xterm/terminal-view.vue';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.arco-tabs-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-tabs-content {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<div class="terminal-header">
|
||||
<!-- 左侧 logo -->
|
||||
<div class="terminal-header-left">
|
||||
<img alt="logo"
|
||||
class="terminal-header-logo-img"
|
||||
draggable="false"
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image" />
|
||||
<h5 class="terminal-header-logo-text">Orion Ops Pro</h5>
|
||||
</div>
|
||||
<!-- 左侧 tabs -->
|
||||
<div class="terminal-header-tabs">
|
||||
<a-tabs v-model:active-key="terminalStore.tabs.active"
|
||||
:editable="true"
|
||||
:hide-content="true"
|
||||
:auto-switch="true"
|
||||
@tab-click="terminalStore.clickTab"
|
||||
@delete="terminalStore.deleteTab">
|
||||
<a-tab-pane v-for="tab in terminalStore.tabs.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="terminal-header-right">
|
||||
<!-- 操作按钮 -->
|
||||
<icon-actions class="terminal-header-right-actions"
|
||||
:actions="actions"
|
||||
position="br"
|
||||
icon-class="terminal-header-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalHeader'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../types/terminal.const';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import IconActions from '../layout/icon-actions.vue';
|
||||
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
const terminalStore = useTerminalStore();
|
||||
|
||||
// 顶部操作
|
||||
const actions = computed<Array<SidebarAction>>(() => [
|
||||
{
|
||||
icon: isFullscreen.value ? 'icon-fullscreen-exit' : 'icon-fullscreen',
|
||||
content: isFullscreen.value ? '点击退出全屏模式' : '点击切换全屏模式',
|
||||
click: toggleFullScreen
|
||||
},
|
||||
]);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-header {
|
||||
--logo-width: 168px;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
height: 100%;
|
||||
color: var(--color-header-text-2);
|
||||
display: flex;
|
||||
user-select: none;
|
||||
|
||||
&-left {
|
||||
width: var(--logo-width);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&-logo-img {
|
||||
padding-left: 6px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
&-logo-text {
|
||||
height: var(--header-height);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&-tabs {
|
||||
width: calc(100% - var(--logo-width) - var(--sidebar-icon-wrapper-size));
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: var(--sidebar-icon-wrapper-size);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
&-actions {
|
||||
width: var(--sidebar-icon-wrapper-size);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(&-icon) {
|
||||
color: var(--color-header-text-2) !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-header-icon-1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
height: 100%;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-tab {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-ink {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-button .arco-icon-hover:hover {
|
||||
color: var(--color-header-text-2);
|
||||
|
||||
&::before {
|
||||
background: var(--color-bg-header-icon-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-type-line .arco-tabs-tab:hover .arco-tabs-tab-title::before) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab) {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--color-header-text-1);
|
||||
background: var(--color-header-tabs-bg);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-header-text-2);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 54px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: linear-gradient(270deg, var(--color-gradient-start) 45%, var(--color-gradient-end) 120%);
|
||||
}
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
padding: 11px 18px;
|
||||
background: var(--color-header-tabs-bg);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .arco-tabs-tab-close-btn {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
&-close-btn {
|
||||
margin: 0 8px 0 0;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
display: none;
|
||||
color: var(--color-header-text-2);
|
||||
|
||||
&:hover {
|
||||
transition: .2s;
|
||||
background: var(--color-bg-header-icon-1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab-active) {
|
||||
background: var(--color-header-tabs-bg-hover);
|
||||
color: var(--color-header-text-2) !important;
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
background: var(--color-header-tabs-bg-hover);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: linear-gradient(270deg, var(--color-gradient-start) 45%, var(--color-gradient-end) 120%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="terminal-left-sidebar">
|
||||
<!-- 顶部操作按钮 -->
|
||||
<icon-actions class="top-actions"
|
||||
:actions="topActions"
|
||||
position="left" />
|
||||
<!-- 底部操作按钮 -->
|
||||
<icon-actions class="bottom-actions"
|
||||
:actions="bottomActions"
|
||||
position="right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalLeftSidebar'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../types/terminal.const';
|
||||
import { InnerTabs } from '../../types/terminal.const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import IconActions from './icon-actions.vue';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
|
||||
// 顶部操作
|
||||
const topActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: 'icon-plus',
|
||||
content: '新建连接',
|
||||
click: () => terminalStore.switchTab(InnerTabs.NEW_CONNECTION)
|
||||
},
|
||||
];
|
||||
|
||||
// 底部操作
|
||||
const bottomActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: 'icon-command',
|
||||
content: '快捷键设置',
|
||||
click: () => terminalStore.switchTab(InnerTabs.SHORTCUT_SETTING)
|
||||
},
|
||||
{
|
||||
icon: 'icon-palette',
|
||||
content: '外观设置',
|
||||
click: () => terminalStore.switchTab(InnerTabs.VIEW_SETTING)
|
||||
},
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-left-sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="terminal-right-sidebar">
|
||||
<!-- 顶部操作按钮 -->
|
||||
<icon-actions class="top-actions"
|
||||
:actions="topActions"
|
||||
position="left" />
|
||||
<!-- 底部操作按钮 -->
|
||||
<icon-actions class="bottom-actions"
|
||||
:actions="bottomActions"
|
||||
position="left" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalRightSidebar'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../types/terminal.const';
|
||||
import IconActions from './icon-actions.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { DarkTheme } from '@/store/modules/terminal';
|
||||
|
||||
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'screenshot']);
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
|
||||
// 顶部操作
|
||||
const topActions = computed<Array<SidebarAction>>(() => [
|
||||
{
|
||||
icon: 'icon-code-block',
|
||||
content: '打开命令片段',
|
||||
click: () => emits('openSnippet')
|
||||
},
|
||||
{
|
||||
icon: 'icon-folder',
|
||||
content: '打开 SFTP',
|
||||
click: () => emits('openSftp')
|
||||
},
|
||||
{
|
||||
icon: 'icon-swap',
|
||||
content: '文件传输列表',
|
||||
iconStyle: {
|
||||
transform: 'rotate(90deg)'
|
||||
},
|
||||
click: () => emits('openTransfer')
|
||||
},
|
||||
{
|
||||
icon: terminalStore.isDarkTheme ? 'icon-sun-fill' : 'icon-moon-fill',
|
||||
content: terminalStore.isDarkTheme ? '点击切换为亮色模式' : '点击切换为暗色模式',
|
||||
click: () => terminalStore.changeDarkTheme(terminalStore.isDarkTheme ? DarkTheme.LIGHT : DarkTheme.DARK)
|
||||
},
|
||||
]);
|
||||
|
||||
// 底部操作
|
||||
const bottomActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: 'icon-camera',
|
||||
content: '截图',
|
||||
click: () => emits('screenshot')
|
||||
},
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-right-sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="group-view-container">
|
||||
<!-- 主机分组 -->
|
||||
<div class="host-group-container">
|
||||
<a-scrollbar>
|
||||
<a-tree v-model:selected-keys="selectedGroup"
|
||||
class="host-tree block-tree"
|
||||
:data="groupTree"
|
||||
:blockNode="true">
|
||||
<!-- 组内数量 -->
|
||||
<template #extra="node">
|
||||
<span class="node-host-count span-blue">{{ treeNodes[node.key]?.length || 0 }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
<!-- 主机列表 -->
|
||||
<host-list-view class="host-list"
|
||||
:hostList="hostList"
|
||||
empty-value="当前分组内无授权主机!" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostGroupView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { HostQueryResponse } from '@/api/asset/host';
|
||||
import { HostGroupQueryResponse } from '@/api/asset/host-group';
|
||||
import HostListView from './host-list-view.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number,
|
||||
groupTree: Array<HostGroupQueryResponse>;
|
||||
hostList: Array<HostQueryResponse>;
|
||||
treeNodes: Record<string, Array<number>>;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
|
||||
const selectedGroup = computed({
|
||||
get() {
|
||||
return [props.modelValue];
|
||||
},
|
||||
set(e) {
|
||||
emits('update:modelValue', e[0]);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@tree-width: 298px;
|
||||
@tree-gap: 32px;
|
||||
|
||||
.group-view-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.host-group-container {
|
||||
:deep(.arco-scrollbar) {
|
||||
width: @tree-width;
|
||||
height: 100%;
|
||||
margin-right: @tree-gap;
|
||||
border-radius: 4px;
|
||||
|
||||
&-container {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.host-tree {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
.node-host-count {
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-list {
|
||||
width: calc(100% - @tree-width - @tree-gap);
|
||||
border-radius: 4px;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="hosts-list-container">
|
||||
<a-list size="large"
|
||||
max-height="100%"
|
||||
:hoverable="true"
|
||||
:data="hostList">
|
||||
<!-- 空数据 -->
|
||||
<template #empty>
|
||||
<a-empty>
|
||||
<template #image>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
{{ emptyValue }}
|
||||
</a-empty>
|
||||
</template>
|
||||
<!-- 数据 -->
|
||||
<template #item="{ item }">
|
||||
<a-list-item class="host-item-wrapper">
|
||||
<div class="host-item">
|
||||
<!-- 左侧图标-名称 -->
|
||||
<div class="flex-center host-item-left">
|
||||
<!-- 图标 -->
|
||||
<span class="host-item-left-icon" @click="terminalStore.openTerminal(item)">
|
||||
<icon-desktop />
|
||||
</span>
|
||||
<!-- 名称 -->
|
||||
<span class="host-item-left-name">
|
||||
<!-- 名称文本 -->
|
||||
<template v-if="!item.editable">
|
||||
<!-- 文本 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
:content="item.alias || `${item.name} (${item.code})`">
|
||||
<span class="host-item-text host-item-left-name-text">
|
||||
<template v-if="item.alias">
|
||||
{{ item.alias }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ `${item.name} (${item.code})` }}
|
||||
</template>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<!-- 修改别名 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="修改别名">
|
||||
<icon-edit class="host-item-left-name-edit"
|
||||
@click="clickEditAlias(item)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<!-- 名称输入框 -->
|
||||
<template v-else>
|
||||
<a-input v-model="item.alias"
|
||||
ref="aliasNameInput"
|
||||
class="host-item-left-name-input"
|
||||
:max-length="32"
|
||||
:disabled="item.loading"
|
||||
size="mini"
|
||||
:placeholder="`${item.name} (${item.code})`"
|
||||
@blur="saveAlias(item)"
|
||||
@pressEnter="saveAlias(item)"
|
||||
@change="saveAlias(item)">
|
||||
<template #suffix>
|
||||
<!-- 加载中 -->
|
||||
<icon-loading v-if="item.loading" />
|
||||
<!-- 保存 -->
|
||||
<icon-check v-else
|
||||
class="pointer"
|
||||
title="保存"
|
||||
@click="saveAlias(item)" />
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 中间ip -->
|
||||
<div class="flex-center host-item-center">
|
||||
<!-- ip -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
:content="item.address">
|
||||
<span class="host-item-text host-item-center-address">
|
||||
{{ item.address }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<!-- 右侧tag-操作 -->
|
||||
<div class="flex-center host-item-right">
|
||||
<!-- tags -->
|
||||
<div class="host-item-right-tags">
|
||||
<template v-if="item.tags?.length">
|
||||
<a-tag v-for="(tag, i) in item.tags"
|
||||
class="host-item-text"
|
||||
:key="tag.id"
|
||||
:style="{
|
||||
maxWidth: `calc(${100 / item.tags.length}% - ${i !== item.tags.length - 1 ? '8px' : '0px'})`,
|
||||
marginRight: `${i !== item.tags.length - 1 ? '8px' : '0'}`,
|
||||
}"
|
||||
:color="dataColor(tag.name, tagColor)">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<div class="host-item-right-actions">
|
||||
<!-- 连接主机 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="连接主机">
|
||||
<div class="terminal-sidebar-icon-wrapper">
|
||||
<div class="terminal-sidebar-icon" @click="terminalStore.openTerminal(item)">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<!-- 连接设置 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="连接设置">
|
||||
<div class="terminal-sidebar-icon-wrapper">
|
||||
<div class="terminal-sidebar-icon" @click="openSetting(item)">
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<!-- 收藏 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="收藏">
|
||||
<div class="terminal-sidebar-icon-wrapper">
|
||||
<div class="terminal-sidebar-icon" @click="setFavorite(item)">
|
||||
<icon-star-fill class="favorite" v-if="item.favorite" />
|
||||
<icon-star v-else />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostListView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import { ref, nextTick, inject } from 'vue';
|
||||
import useFavorite from '@/hooks/favorite';
|
||||
import { dataColor } from '@/utils';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { updateHostAlias } from '@/api/asset/host-extra';
|
||||
import { openSshModalKey } from '../../types/terminal.const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
|
||||
const props = defineProps<{
|
||||
hostList: Array<HostQueryResponse>,
|
||||
emptyValue: string
|
||||
}>();
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
||||
|
||||
const aliasNameInput = ref();
|
||||
|
||||
// 点击修改别名
|
||||
const clickEditAlias = (item: HostQueryResponse) => {
|
||||
item.editable = true;
|
||||
if (!item.alias) {
|
||||
item.alias = '';
|
||||
}
|
||||
nextTick(() => {
|
||||
aliasNameInput.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存别名
|
||||
const saveAlias = async (item: HostQueryResponse) => {
|
||||
item.loading = true;
|
||||
item.modCount = (item.modCount || 0) + 1;
|
||||
if (item.modCount != 1) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 修改别名
|
||||
await updateHostAlias({
|
||||
id: item.id,
|
||||
name: item.alias || ''
|
||||
});
|
||||
item.editable = false;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
item.loading = false;
|
||||
item.modCount = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开配置
|
||||
const openSetting = inject<(record: HostQueryResponse) => void>(openSshModalKey);
|
||||
|
||||
// 设置收藏
|
||||
const setFavorite = async (item: HostQueryResponse) => {
|
||||
if (favoriteLoading.value) {
|
||||
return;
|
||||
}
|
||||
await toggleFavorite(item, item.id);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@host-item-height: 56px;
|
||||
|
||||
:deep(.arco-list-bordered) {
|
||||
border: 1px solid var(--color-fill-3);
|
||||
|
||||
.arco-empty {
|
||||
padding: 16px 0;
|
||||
flex-direction: column;
|
||||
|
||||
.arco-empty-image {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-list-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-fill-3);
|
||||
}
|
||||
|
||||
.arco-list-item:hover {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-wrapper {
|
||||
padding: 0 !important;
|
||||
height: @host-item-height;
|
||||
font-size: 12px;
|
||||
color: var(--color-content-text-2);
|
||||
|
||||
.host-item {
|
||||
width: 100%;
|
||||
height: @host-item-height;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&-text {
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.host-item-left-name-edit {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.host-item-right-tags {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.host-item-right-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-left {
|
||||
width: 35%;
|
||||
height: 100%;
|
||||
padding-left: 18px;
|
||||
position: absolute;
|
||||
|
||||
&-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-3);
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
|
||||
&-name {
|
||||
// 100% - icon-width - icon-margin-right
|
||||
width: calc(100% - 42px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
// 100% - edit-margin-left - edit-font-size
|
||||
max-width: calc(100% - 18px);
|
||||
}
|
||||
|
||||
&-edit {
|
||||
display: none;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
color: rgb(var(--blue-6));
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-center {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
left: 35%;
|
||||
padding: 0 8px;
|
||||
position: absolute;
|
||||
|
||||
&-address {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-right {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
left: 60%;
|
||||
padding-right: 18px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
|
||||
&-tags {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: none;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favorite {
|
||||
color: rgb(var(--yellow-6));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 分组视图列表 -->
|
||||
<host-group-view v-if="NewConnectionType.GROUP === newConnectionType"
|
||||
v-model="selectedGroup"
|
||||
:group-tree="hosts.groupTree"
|
||||
:tree-nodes="treeNodes"
|
||||
:host-list="hostList"
|
||||
:filter-value="filterValue" />
|
||||
<!-- 列表视图 -->
|
||||
<host-list-view v-if="NewConnectionType.LIST === newConnectionType"
|
||||
:hostList="hostList"
|
||||
empty-value="无授权主机!" />
|
||||
<!-- 我的收藏 -->
|
||||
<host-list-view v-if="NewConnectionType.FAVORITE === newConnectionType"
|
||||
class="list-view-container"
|
||||
:hostList="hostList"
|
||||
empty-value="无收藏记录, 快去点击主机右侧的⭐进行收藏吧!" />
|
||||
<!-- 最近连接 -->
|
||||
<host-list-view v-if="NewConnectionType.LATEST === newConnectionType"
|
||||
class="list-view-container"
|
||||
:hostList="hostList"
|
||||
empty-value="暂无连接记录, 快去体验吧!" />
|
||||
<!-- 修改主机设置模态框 -->
|
||||
<ssh-extra-modal ref="sshModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostsView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref, watch } from 'vue';
|
||||
import { NewConnectionType, openSshModalKey } from '../../types/terminal.const';
|
||||
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import { HostQueryResponse } from '@/api/asset/host';
|
||||
import HostGroupView from './host-group-view.vue';
|
||||
import HostListView from './host-list-view.vue';
|
||||
import SshExtraModal from './ssh-extra-modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
hosts: AuthorizedHostQueryResponse,
|
||||
filterValue: string,
|
||||
newConnectionType: string
|
||||
}>();
|
||||
|
||||
const hostList = ref<Array<HostQueryResponse>>([]);
|
||||
const treeNodes = ref<Record<string, Array<number>>>({});
|
||||
const selectedGroup = ref(
|
||||
props.hosts?.groupTree?.length
|
||||
? props.hosts.groupTree[0].key
|
||||
: 0
|
||||
);
|
||||
const sshModal = ref();
|
||||
|
||||
// 暴露打开 ssh 配置模态框
|
||||
provide(openSshModalKey, (record: any) => {
|
||||
sshModal.value?.open(record);
|
||||
});
|
||||
|
||||
// 主机数据处理
|
||||
const shuffleHosts = () => {
|
||||
let list = [...props.hosts?.hostList];
|
||||
// 过滤
|
||||
const filterVal = props.filterValue.toLowerCase();
|
||||
if (filterVal) {
|
||||
list = filterVal.startsWith('@')
|
||||
// tag 过滤
|
||||
? list.filter(item => item.tags.some(tag => (tag.name as string).toLowerCase().startsWith(filterVal.substring(1, filterVal.length))))
|
||||
// 名称/编码/地址 过滤
|
||||
: list.filter(item => {
|
||||
return (item.name as string)?.toLowerCase().indexOf(filterVal) > -1
|
||||
|| (item.code as string)?.toLowerCase().indexOf(filterVal) > -1
|
||||
|| (item.alias as string)?.toLowerCase().indexOf(filterVal) > -1
|
||||
|| (item.address as string)?.toLowerCase().indexOf(filterVal) > -1;
|
||||
});
|
||||
}
|
||||
// 判断类型
|
||||
if (NewConnectionType.GROUP === props.newConnectionType) {
|
||||
// 过滤-分组
|
||||
const groupNodes = { ...props.hosts.treeNodes };
|
||||
Object.keys(groupNodes).forEach(k => {
|
||||
groupNodes[k] = (groupNodes[k] || []).filter(item => list.some(host => host.id === item));
|
||||
});
|
||||
treeNodes.value = groupNodes;
|
||||
// 当前组内数据
|
||||
list = list.filter(item => groupNodes[selectedGroup.value]?.some(id => id === item.id));
|
||||
} else if (NewConnectionType.FAVORITE === props.newConnectionType) {
|
||||
// 过滤-个人收藏
|
||||
list = list.filter(item => item.favorite);
|
||||
}
|
||||
// 排序
|
||||
hostList.value = list?.sort((o1, o2) => {
|
||||
if (o1.favorite || o2.favorite) {
|
||||
if (o1.favorite && o2.favorite) {
|
||||
return o2.id < o1.id ? 1 : -1;
|
||||
}
|
||||
return o2.favorite ? 1 : -1;
|
||||
} else {
|
||||
return o2.id < o1.id ? 1 : -1;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听搜索值变化
|
||||
watch(() => props.filterValue, shuffleHosts);
|
||||
|
||||
// 监听类型变化
|
||||
watch(() => props.newConnectionType, shuffleHosts);
|
||||
|
||||
// 监听分组变化
|
||||
watch(selectedGroup, shuffleHosts);
|
||||
|
||||
// 初始化 加载主机
|
||||
onMounted(shuffleHosts);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.list-view-container {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="terminal-setting-container">
|
||||
<div class="terminal-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="terminal-setting-title">新建连接</h2>
|
||||
<!-- 操作栏 -->
|
||||
<div class="terminal-setting-block header-actions">
|
||||
<!-- 视图类型 -->
|
||||
<a-radio-group v-model="newConnectionType"
|
||||
type="button"
|
||||
class="usn"
|
||||
:options="toOptions(NewConnectionTypeKey)"
|
||||
@change="changeNewConnectionType" />
|
||||
<!-- 过滤 -->
|
||||
<a-auto-complete v-model="filterValue"
|
||||
class="host-filter"
|
||||
placeholder="别名/名称/编码/IP @标签"
|
||||
:allow-clear="true"
|
||||
:data="filterOptions"
|
||||
:filter-option="searchFilter">
|
||||
<template #option="{ data: { raw: { label, isTag} } }">
|
||||
<!-- tag -->
|
||||
<a-tag v-if="isTag" :color="dataColor(label, tagColor)">
|
||||
{{ label }}
|
||||
</a-tag>
|
||||
<!-- 文本 -->
|
||||
<template v-else>
|
||||
{{ label }}
|
||||
</template>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
</div>
|
||||
<!-- 授权主机 -->
|
||||
<div class="terminal-setting-block" style="margin: 0;">
|
||||
<!-- 顶部 -->
|
||||
<div class="terminal-setting-subtitle-wrapper">
|
||||
<h3 class="terminal-setting-subtitle">
|
||||
授权主机
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="terminal-setting-body body-container">
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="loading"
|
||||
class="hosts-skeleton"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="6"
|
||||
:line-height="40"
|
||||
:line-spacing="20" />
|
||||
</a-skeleton>
|
||||
<!-- 无数据 -->
|
||||
<a-empty v-else-if="!hosts.hostList?.length">
|
||||
<template #image>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
Oops! 无授权主机 请联系管理员授权后重试!
|
||||
</a-empty>
|
||||
<!-- 主机列表 -->
|
||||
<hosts-view v-else
|
||||
class="host-view-container"
|
||||
:hosts="hosts"
|
||||
:filter-value="filterValue"
|
||||
:new-connection-type="newConnectionType" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'newConnectionView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { NewConnectionType, NewConnectionTypeKey } from '../../types/terminal.const';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import { dataColor } from '@/utils';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import HostsView from './hosts-view.vue';
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions } = useDictStore();
|
||||
const { preference, changeNewConnectionType } = useTerminalStore();
|
||||
|
||||
const newConnectionType = ref(preference.newConnectionType || NewConnectionType.GROUP);
|
||||
const filterValue = ref('');
|
||||
const filterOptions = ref<Array<SelectOptionData>>([]);
|
||||
const hosts = ref<AuthorizedHostQueryResponse>({} as AuthorizedHostQueryResponse);
|
||||
|
||||
// 过滤输入
|
||||
const searchFilter = (searchValue: string, option: SelectOptionData) => {
|
||||
if (searchValue.startsWith('@')) {
|
||||
// tag 过滤
|
||||
return option.isTag && (option.label as string).toLowerCase().startsWith(searchValue.substring(1, searchValue.length).toLowerCase());
|
||||
} else {
|
||||
// 文本过滤
|
||||
return !option.isTag && (option.label as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化过滤器项
|
||||
const initFilterOptions = () => {
|
||||
// 添加 tags
|
||||
const tagNames = hosts.value.hostList?.map(s => s.tags)
|
||||
.filter(s => s?.length)
|
||||
.flat(1)
|
||||
.sort((o1, o2) => o1.id - o2.id)
|
||||
.map(s => s.name);
|
||||
[...new Set(tagNames)].map(value => {
|
||||
return { label: value, value: `@${value}`, isTag: true };
|
||||
}).forEach(s => filterOptions.value.push(s));
|
||||
// 添加主机信息
|
||||
const hostMeta = hosts.value.hostList?.map(s => {
|
||||
return [s.name, s.code, s.address, s.alias];
|
||||
}).filter(Boolean).flat(1);
|
||||
[...new Set(hostMeta)].map(value => {
|
||||
return { label: value, value };
|
||||
}).forEach(s => filterOptions.value.push(s));
|
||||
};
|
||||
|
||||
// 初始化
|
||||
const init = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getCurrentAuthorizedHost();
|
||||
hosts.value = data;
|
||||
// 初始化过滤项
|
||||
initFilterOptions();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载主机信息
|
||||
onBeforeMount(init);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
:deep(.host-filter) {
|
||||
width: 36%;
|
||||
}
|
||||
}
|
||||
|
||||
.body-container {
|
||||
justify-content: space-between;
|
||||
|
||||
.hosts-skeleton {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.host-view-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 240px);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form"
|
||||
title-align="start"
|
||||
:title="title"
|
||||
:top="80"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="保存"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:style="{ width: '460px' }"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
:rules="{}">
|
||||
<!-- 验证方式 -->
|
||||
<a-form-item field="authType" label="验证方式">
|
||||
<a-radio-group type="button"
|
||||
v-model="formModel.authType"
|
||||
:options="toOptions(extraSshAuthTypeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
|
||||
field="username"
|
||||
label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<!-- 主机秘钥 -->
|
||||
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
|
||||
field="keyId"
|
||||
label="主机秘钥"
|
||||
:rules="{ required: true, message: '请选择主机秘钥' }">
|
||||
<host-key-selector v-model="formModel.keyId"
|
||||
:authorized="true" />
|
||||
</a-form-item>
|
||||
<!-- 主机身份 -->
|
||||
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_IDENTITY"
|
||||
field="identityId"
|
||||
label="主机身份"
|
||||
:rules="{ required: true, message: '请选择主机身份' }">
|
||||
<host-identity-selector v-model="formModel.identityId"
|
||||
:authorized="true" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ssh-extra-modal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { SshExtraModel } from '../../types/terminal.const';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getHostExtraItem, updateHostExtra } from '@/api/asset/host-extra';
|
||||
import { ExtraSshAuthType, extraSshAuthTypeKey } from '../../types/terminal.const';
|
||||
import { useDictStore } from '@/store';
|
||||
import HostIdentitySelector from '@/components/asset/host-identity/host-identity-selector.vue';
|
||||
import HostKeySelector from '@/components/asset/host-key/host-key-selector.vue';
|
||||
|
||||
const { toOptions } = useDictStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const hostId = ref<number>();
|
||||
const formRef = ref();
|
||||
const formModel = ref<SshExtraModel>({});
|
||||
|
||||
// 打开配置
|
||||
const open = (record: HostQueryResponse) => {
|
||||
hostId.value = record.id;
|
||||
title.value = record.alias || `${record.name} (${record.code})`;
|
||||
renderForm();
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostExtraItem<SshExtraModel>({ hostId: hostId.value, item: 'ssh' });
|
||||
formModel.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 修改
|
||||
await updateHostExtra({
|
||||
hostId: hostId.value,
|
||||
item: 'ssh',
|
||||
extra: JSON.stringify(formModel.value)
|
||||
});
|
||||
Message.success('保存成功');
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="terminal-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="terminal-setting-subtitle-wrapper">
|
||||
<h3 class="terminal-setting-subtitle">
|
||||
显示设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<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="toOptions(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-example">
|
||||
<span class="terminal-example-label">预览效果</span>
|
||||
<div class="terminal-example-wrapper"
|
||||
:style="{ background: preference.themeSchema?.background }">
|
||||
<terminal-example :theme="preference.themeSchema"
|
||||
ref="previewTerminal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalFontBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalDisplaySetting } from '@/store/modules/terminal/types';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import { fontFamilyKey, fontSizeKey, fontWeightKey, fontFamilySuffix, cursorStyleKey } from '../../types/terminal.const';
|
||||
import { labelFilter } from '@/types/form';
|
||||
import TerminalExample from '../view-setting/terminal-example.vue';
|
||||
|
||||
const { toOptions } = useDictStore();
|
||||
const { preference, changeDisplaySetting } = useTerminalStore();
|
||||
|
||||
const previewTerminal = ref();
|
||||
const formModel = ref<TerminalDisplaySetting>({ ...preference.displaySetting });
|
||||
|
||||
// 监听主题变化 动态修改预览样式
|
||||
watch(() => preference.themeSchema, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
const options = previewTerminal.value?.term?.options;
|
||||
options && (options.theme = v);
|
||||
});
|
||||
|
||||
// 监听内容变化
|
||||
watch(formModel, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
const options = previewTerminal.value?.term?.options;
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
// 修改预览终端配置
|
||||
Object.keys(v).forEach(key => {
|
||||
if (key === 'fontFamily') {
|
||||
options[key] = (formModel.value as any)[key] + fontFamilySuffix;
|
||||
} else {
|
||||
options[key] = (formModel.value as any)[key];
|
||||
}
|
||||
});
|
||||
// 同步
|
||||
changeDisplaySetting(formModel.value);
|
||||
// 聚焦
|
||||
previewTerminal.value.term.focus();
|
||||
}, { deep: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@terminal-width: 458px;
|
||||
|
||||
.terminal-setting-body {
|
||||
height: 248px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-fill-4);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.arco-form) {
|
||||
.arco-form-item-label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-item-font-family {
|
||||
width: 158px;
|
||||
}
|
||||
|
||||
.form-item-font-size {
|
||||
width: 148px;
|
||||
}
|
||||
|
||||
.form-item-line-height {
|
||||
width: 114px;
|
||||
}
|
||||
|
||||
.form-item-font-weight, .form-item-font-bold-weight {
|
||||
width: 178px;
|
||||
}
|
||||
|
||||
.form-item-font-weight {
|
||||
margin-right: 70px;
|
||||
}
|
||||
|
||||
.form-item-cursor-style {
|
||||
margin-right: 90px;
|
||||
|
||||
.arco-radio-button-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-example {
|
||||
height: 100%;
|
||||
|
||||
&-label {
|
||||
color: var(--color-text-2);
|
||||
display: block;
|
||||
height: 16px;
|
||||
margin-bottom: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
border-radius: 4px;
|
||||
width: calc(@terminal-width - 16px);
|
||||
height: calc(100% - 16px - 12px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="terminal-example" ref="terminal"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalExample'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalThemeSchema } from '@/store/modules/terminal/types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
theme: TerminalThemeSchema | Record<string, any>
|
||||
}>();
|
||||
|
||||
const terminal = ref();
|
||||
const term = ref();
|
||||
|
||||
onMounted(() => {
|
||||
term.value = new Terminal({
|
||||
theme: props.theme,
|
||||
cols: 42,
|
||||
rows: 6,
|
||||
fontSize: 15,
|
||||
cursorInactiveStyle: 'none',
|
||||
});
|
||||
term.value.open(terminal.value);
|
||||
term.value.write(
|
||||
'[1;94m[root[0m@[1;96mOrionServer usr]#[0m\r\n' +
|
||||
'[92mdr-xr-xr-x.[0m 2 root root [96mbin[0m\r\n' +
|
||||
'[92mdr-xr-xr-x.[0m 2 root root [96msbin[0m\r\n' +
|
||||
'[92mdr-xr-xr-x.[0m 43 root root [96mlib[0m\r\n' +
|
||||
'[92mdr-xr-xr-x.[0m 62 root root [96mlib64[0m\r\n' +
|
||||
'[92mlrwxrwxrwx.[0m 1 root root [90;102mtmp[0m'
|
||||
);
|
||||
});
|
||||
|
||||
defineExpose({ term });
|
||||
|
||||
onUnmounted(() => {
|
||||
term.value?.dispose();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-example {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.xterm-viewport) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="terminal-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="terminal-setting-subtitle-wrapper">
|
||||
<h3 class="terminal-setting-subtitle">
|
||||
主题设置
|
||||
</h3>
|
||||
<!-- 暗色选择 -->
|
||||
<a-radio-group v-model="preference.darkTheme"
|
||||
class="usn"
|
||||
size="mini"
|
||||
type="button"
|
||||
:options="toOptions(darkThemeKey)"
|
||||
@change="changeDarkTheme">
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="terminal-setting-body terminal-theme-container">
|
||||
<div class="theme-row"
|
||||
v-for="index in ThemeSchema.length / 2"
|
||||
:key="index">
|
||||
<a-card v-for="(theme, index) in [ThemeSchema[(index - 1) * 2], ThemeSchema[(index - 1) * 2 + 1]]"
|
||||
:key="theme.name"
|
||||
class="terminal-theme-card simple-card"
|
||||
:class="{
|
||||
'terminal-theme-card-check': theme.name === preference.themeSchema.name
|
||||
}"
|
||||
:title="theme.name"
|
||||
:style="{
|
||||
background: theme.background,
|
||||
marginRight: index === 0 ? '16px' : 0
|
||||
}"
|
||||
:header-style="{
|
||||
color: theme.dark ? 'rgba(255, 255, 255, .8)' : 'rgba(0, 0, 0, .8)',
|
||||
userSelect: 'none'
|
||||
}"
|
||||
@click="changeThemeSchema(theme)">
|
||||
<!-- 样例 -->
|
||||
<terminal-example :theme="{ ...theme, cursor: theme.background }" />
|
||||
<!-- 选中按钮 -->
|
||||
<icon-check class="theme-check-icon"
|
||||
v-show="theme.name === preference.themeSchema.name" />
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalThemeBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { darkThemeKey } from '../../types/terminal.const';
|
||||
import ThemeSchema from '../../types/terminal.theme';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import TerminalExample from './terminal-example.vue';
|
||||
|
||||
const { changeThemeSchema, changeDarkTheme, preference } = useTerminalStore();
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@terminal-width: 458px;
|
||||
@terminal-height: 138px;
|
||||
|
||||
.terminal-theme-container {
|
||||
flex-direction: column;
|
||||
|
||||
.theme-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-theme-card {
|
||||
width: @terminal-width;
|
||||
height: calc(@terminal-height + 44px);
|
||||
border: 2px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
padding: 4px 16px;
|
||||
height: 40px;
|
||||
border-bottom: .5px solid var(--color-border-2);
|
||||
|
||||
&-title {
|
||||
color: unset;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
height: @terminal-height;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.theme-check-icon {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
color: #FFF;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
&-check, &:hover {
|
||||
border: 2px solid rgb(var(--blue-6));
|
||||
}
|
||||
|
||||
&-check::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 28px solid rgb(var(--blue-6));
|
||||
border-left: 28px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="terminal-setting-container">
|
||||
<div class="terminal-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="terminal-setting-title">外观设置</h2>
|
||||
<!-- 显示设置 -->
|
||||
<terminal-display-block />
|
||||
<!-- 主题设置 -->
|
||||
<terminal-theme-block />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalViewSetting'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TerminalDisplayBlock from './terminal-display-block.vue';
|
||||
import TerminalThemeBlock from './terminal-theme-block.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
105
orion-ops-ui/src/views/host/terminal/index.vue
Normal file
105
orion-ops-ui/src/views/host/terminal/index.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="host-layout" v-if="render">
|
||||
<!-- 头部区域 -->
|
||||
<header class="host-layout-header">
|
||||
<terminal-header />
|
||||
</header>
|
||||
<!-- 主体区域 -->
|
||||
<main class="host-layout-main">
|
||||
<!-- 左侧操作栏 -->
|
||||
<div class="host-layout-left">
|
||||
<terminal-left-sidebar />
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-layout-content">
|
||||
<terminal-content />
|
||||
</div>
|
||||
<!-- 右侧操作栏 -->
|
||||
<div class="host-layout-right">
|
||||
<terminal-right-sidebar />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostTerminal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount, onUnmounted } from 'vue';
|
||||
import { dictKeys } from './types/terminal.const';
|
||||
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
|
||||
import TerminalHeader from './components/layout/terminal-header.vue';
|
||||
import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
|
||||
import TerminalRightSidebar from './components/layout/terminal-right-sidebar.vue';
|
||||
import TerminalContent from './components/layout/terminal-content.vue';
|
||||
import './assets/styles/layout.less';
|
||||
import 'xterm/css/xterm.css';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const dictStore = useDictStore();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const render = ref(false);
|
||||
|
||||
// 加载用户终端偏好
|
||||
onBeforeMount(async () => {
|
||||
await terminalStore.fetchPreference();
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
// 加载字典值
|
||||
onBeforeMount(async () => {
|
||||
await dictStore.loadKeys([...dictKeys]);
|
||||
});
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('authorizedHostKeys', 'authorizedHostIdentities');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.host-layout {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
color: var(--color-content-text-2);
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: var(--color-bg-header);
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 100%;
|
||||
height: calc(100% - var(--sidebar-width));
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-left, &-right {
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
background: var(--color-bg-sidebar);
|
||||
border-top: 1px solid var(--color-bg-content);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-content {
|
||||
width: calc(100% - var(--sidebar-width) * 2);
|
||||
height: 100%;
|
||||
background: var(--color-bg-content);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
96
orion-ops-ui/src/views/host/terminal/types/terminal.const.ts
Normal file
96
orion-ops-ui/src/views/host/terminal/types/terminal.const.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
import type { TabItem } from '@/store/modules/terminal/types';
|
||||
|
||||
// sidebar 操作类型
|
||||
export interface SidebarAction {
|
||||
icon: string;
|
||||
content: string;
|
||||
iconStyle?: CSSProperties;
|
||||
click: () => void;
|
||||
}
|
||||
|
||||
// tab 类型
|
||||
export const TabType = {
|
||||
SETTING: 'setting',
|
||||
TERMINAL: 'terminal',
|
||||
};
|
||||
|
||||
// 内置 tab
|
||||
export const InnerTabs: Record<string, TabItem> = {
|
||||
NEW_CONNECTION: {
|
||||
key: 'newConnection',
|
||||
title: '新建连接',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
SHORTCUT_SETTING: {
|
||||
key: 'shortcutSetting',
|
||||
title: '快捷键设置',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
VIEW_SETTING: {
|
||||
key: 'viewSetting',
|
||||
title: '外观设置',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
};
|
||||
|
||||
// 新建连接类型
|
||||
export const NewConnectionType = {
|
||||
GROUP: 'group',
|
||||
LIST: 'list',
|
||||
FAVORITE: 'favorite',
|
||||
LATEST: 'latest'
|
||||
};
|
||||
|
||||
// ssh 额外配置
|
||||
export interface SshExtraModel {
|
||||
authType?: string;
|
||||
username?: string;
|
||||
keyId?: number;
|
||||
identityId?: number;
|
||||
}
|
||||
|
||||
// 主机额外配置 ssh 认证方式
|
||||
export const ExtraSshAuthType = {
|
||||
// 使用默认认证方式
|
||||
DEFAULT: 'DEFAULT',
|
||||
// 自定义秘钥
|
||||
CUSTOM_KEY: 'CUSTOM_KEY',
|
||||
// 自定义身份
|
||||
CUSTOM_IDENTITY: 'CUSTOM_IDENTITY',
|
||||
};
|
||||
|
||||
// 打开 sshModal key
|
||||
export const openSshModalKey = Symbol();
|
||||
|
||||
// 字体后缀 兜底
|
||||
export const fontFamilySuffix = ',courier-new, courier, monospace';
|
||||
|
||||
// 终端暗色模式 字典项
|
||||
export const darkThemeKey = 'terminalDarkTheme';
|
||||
|
||||
// 终端字体样式
|
||||
export const fontFamilyKey = 'terminalFontFamily';
|
||||
|
||||
// 终端字体大小
|
||||
export const fontSizeKey = 'terminalFontSize';
|
||||
|
||||
// 终端字体字重
|
||||
export const fontWeightKey = 'terminalFontWeight';
|
||||
|
||||
// 终端光标样式
|
||||
export const cursorStyleKey = 'terminalCursorStyle';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const NewConnectionTypeKey = 'terminalNewConnectionType';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const extraSshAuthTypeKey = 'hostExtraSshAuthType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [
|
||||
darkThemeKey, fontFamilyKey,
|
||||
fontSizeKey, fontWeightKey,
|
||||
cursorStyleKey, NewConnectionTypeKey,
|
||||
extraSshAuthTypeKey
|
||||
];
|
||||
120
orion-ops-ui/src/views/host/terminal/types/terminal.protocol.ts
Normal file
120
orion-ops-ui/src/views/host/terminal/types/terminal.protocol.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// 终端协议
|
||||
export interface Protocol {
|
||||
type: string;
|
||||
template: string[];
|
||||
}
|
||||
|
||||
// 终端内容
|
||||
export interface Payload {
|
||||
type?: string;
|
||||
session: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 输入协议
|
||||
export const InputProtocol: Record<string, Protocol> = {
|
||||
// 主机连接检查
|
||||
CHECK: {
|
||||
type: 'ck',
|
||||
template: ['type', 'session', 'hostId']
|
||||
},
|
||||
// 连接主机
|
||||
CONNECT: {
|
||||
type: 'co',
|
||||
template: ['type', 'session', 'cols', 'rows']
|
||||
},
|
||||
// 关闭连接
|
||||
CLOSE: {
|
||||
type: 'cl',
|
||||
template: ['type', 'session']
|
||||
},
|
||||
// ping
|
||||
PING: {
|
||||
type: 'p',
|
||||
template: ['type']
|
||||
},
|
||||
// 修改大小
|
||||
RESIZE: {
|
||||
type: 'rs',
|
||||
template: ['type', 'session', 'cols', 'rows']
|
||||
},
|
||||
// 执行
|
||||
EXEC: {
|
||||
type: 'e',
|
||||
template: ['type', 'session', 'command']
|
||||
},
|
||||
// 输入
|
||||
INPUT: {
|
||||
type: 'i',
|
||||
template: ['type', 'session', 'command']
|
||||
}
|
||||
};
|
||||
|
||||
// 输出协议
|
||||
export const OutputProtocol: Record<string, Protocol> = {
|
||||
// 主机连接检查
|
||||
CHECK: {
|
||||
type: 'ck',
|
||||
template: ['type', 'session', 'result', 'errorMessage']
|
||||
},
|
||||
// 主机连接
|
||||
CONNECT: {
|
||||
type: 'co',
|
||||
template: ['type', 'session', 'result', 'errorMessage']
|
||||
},
|
||||
// pong
|
||||
PONG: {
|
||||
type: 'p',
|
||||
template: ['type']
|
||||
},
|
||||
// 输出
|
||||
OUTPUT: {
|
||||
type: 'o',
|
||||
template: ['type', 'session', 'body']
|
||||
},
|
||||
};
|
||||
|
||||
// 分隔符
|
||||
export const SEPARATOR = '|';
|
||||
|
||||
// 解析参数
|
||||
export const parse: Record<string, any> = (payload: string) => {
|
||||
const protocols = Object.values(OutputProtocol);
|
||||
const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload);
|
||||
if (!useProtocol) {
|
||||
return undefined;
|
||||
}
|
||||
const template = useProtocol.template;
|
||||
const res: Record<string, any> = {};
|
||||
let curr = 0;
|
||||
let len = payload.length;
|
||||
for (let i = 0, pl = template.length; i < pl; i++) {
|
||||
if (i == pl - 1) {
|
||||
// 最后一次
|
||||
res[template[i]] = payload.substring(curr, len);
|
||||
} else {
|
||||
// 非最后一次
|
||||
let tmp = '';
|
||||
for (; curr < len; curr++) {
|
||||
const c = payload.charAt(curr);
|
||||
if (c == SEPARATOR) {
|
||||
res[template[i]] = tmp;
|
||||
curr++;
|
||||
break;
|
||||
} else {
|
||||
tmp += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
// 格式化参数
|
||||
export const format = (payload: Payload, protocol: Protocol) => {
|
||||
payload.type = protocol.type;
|
||||
return protocol.template
|
||||
.map(i => payload[i] || '')
|
||||
.join(SEPARATOR);
|
||||
};
|
||||
251
orion-ops-ui/src/views/host/terminal/types/terminal.theme.ts
Normal file
251
orion-ops-ui/src/views/host/terminal/types/terminal.theme.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import type { TerminalThemeSchema } from '@/store/modules/terminal/types';
|
||||
|
||||
// 默认配色
|
||||
export const DEFAULT_SCHEMA = {
|
||||
name: 'Frappe',
|
||||
dark: true,
|
||||
background: '#303446',
|
||||
foreground: '#C6D0F5',
|
||||
cursor: '#F2D5CF',
|
||||
cursorAccent: '#232634',
|
||||
selectionBackground: '#C9DDF0',
|
||||
selectionForeground: '#303446',
|
||||
// selectionInactiveBackground: 'rgba(98, 104, 128, 0.30078125)',
|
||||
black: '#51576D',
|
||||
red: '#E78284',
|
||||
green: '#A6D189',
|
||||
yellow: '#E5C890',
|
||||
blue: '#8CAAEE',
|
||||
magenta: '#F4B8E4',
|
||||
cyan: '#81C8BE',
|
||||
white: '#B5BFE2',
|
||||
brightBlack: '#626880',
|
||||
brightRed: '#E78284',
|
||||
brightGreen: '#A6D189',
|
||||
brightYellow: '#E5C890',
|
||||
brightBlue: '#8CAAEE',
|
||||
brightMagenta: '#F4B8E4',
|
||||
brightCyan: '#81C8BE',
|
||||
brightWhite: '#A5ADCE'
|
||||
};
|
||||
|
||||
export default [
|
||||
DEFAULT_SCHEMA,
|
||||
{
|
||||
name: 'Latte',
|
||||
dark: false,
|
||||
background: '#EFF1F5',
|
||||
foreground: '#4C4F69',
|
||||
cursor: '#DC8A78',
|
||||
cursorAccent: '#EFF1F5',
|
||||
selectionBackground: '#6C6F85',
|
||||
selectionForeground: '#EFF1F5',
|
||||
// selectionInactiveBackground: 'rgba(172, 176, 190, 0.30078125)',
|
||||
black: '#5C5F77',
|
||||
red: '#D20F39',
|
||||
green: '#40A02B',
|
||||
yellow: '#DF8E1D',
|
||||
blue: '#1E66F5',
|
||||
magenta: '#EA76CB',
|
||||
cyan: '#179299',
|
||||
white: '#ACB0BE',
|
||||
brightBlack: '#6C6F85',
|
||||
brightRed: '#D20F39',
|
||||
brightGreen: '#40A02B',
|
||||
brightYellow: '#DF8E1D',
|
||||
brightBlue: '#1E66F5',
|
||||
brightMagenta: '#EA76CB',
|
||||
brightCyan: '#179299',
|
||||
brightWhite: '#BCC0CC'
|
||||
},
|
||||
{
|
||||
name: 'Macchiato',
|
||||
dark: true,
|
||||
background: '#24273A',
|
||||
foreground: '#CAD3F5',
|
||||
cursor: '#F4DBD6',
|
||||
cursorAccent: '#181926',
|
||||
selectionBackground: '#A5ADCB',
|
||||
selectionForeground: '#24273A',
|
||||
// selectionInactiveBackground: 'rgba(91, 96, 120, 0.30078125)',
|
||||
black: '#494D64',
|
||||
red: '#ED8796',
|
||||
green: '#A6DA95',
|
||||
yellow: '#EED49F',
|
||||
blue: '#8AADF4',
|
||||
magenta: '#F5BDE6',
|
||||
cyan: '#8BD5CA',
|
||||
white: '#B8C0E0',
|
||||
brightBlack: '#5B6078',
|
||||
brightRed: '#ED8796',
|
||||
brightGreen: '#A6DA95',
|
||||
brightYellow: '#EED49F',
|
||||
brightBlue: '#8AADF4',
|
||||
brightMagenta: '#F5BDE6',
|
||||
brightCyan: '#8BD5CA',
|
||||
brightWhite: '#A5ADCB'
|
||||
},
|
||||
{
|
||||
name: 'Mocha',
|
||||
dark: true,
|
||||
background: '#1E1E2E',
|
||||
foreground: '#CDD6F4',
|
||||
cursor: '#F5E0DC',
|
||||
cursorAccent: '#11111B',
|
||||
selectionBackground: '#A6ADC8',
|
||||
selectionForeground: '#1E1E2E',
|
||||
// selectionInactiveBackground: 'rgba(88, 91, 112, 0.30078125)',
|
||||
black: '#45475A',
|
||||
red: '#F38BA8',
|
||||
green: '#A6E3A1',
|
||||
yellow: '#F9E2AF',
|
||||
blue: '#89B4FA',
|
||||
magenta: '#F5C2E7',
|
||||
cyan: '#94E2D5',
|
||||
white: '#BAC2DE',
|
||||
brightBlack: '#585B70',
|
||||
brightRed: '#F38BA8',
|
||||
brightGreen: '#A6E3A1',
|
||||
brightYellow: '#F9E2AF',
|
||||
brightBlue: '#89B4FA',
|
||||
brightMagenta: '#F5C2E7',
|
||||
brightCyan: '#94E2D5',
|
||||
brightWhite: '#A6ADC8'
|
||||
},
|
||||
{
|
||||
name: 'Atom One Light',
|
||||
dark: false,
|
||||
background: '#F9F9F9',
|
||||
foreground: '#2A2C33',
|
||||
cursor: '#BBBBBB',
|
||||
selectionBackground: '#EDEDED',
|
||||
black: '#000000',
|
||||
red: '#DE3E35',
|
||||
green: '#3F953A',
|
||||
yellow: '#D2B67C',
|
||||
blue: '#2F5AF3',
|
||||
cyan: '#3F953A',
|
||||
white: '#BBBBBB',
|
||||
brightBlack: '#000000',
|
||||
brightRed: '#DE3E35',
|
||||
brightGreen: '#3F953A',
|
||||
brightYellow: '#D2B67C',
|
||||
brightBlue: '#2F5AF3',
|
||||
brightCyan: '#3F953A',
|
||||
brightWhite: '#FFFFFF'
|
||||
},
|
||||
{
|
||||
name: 'One Half Dark',
|
||||
dark: true,
|
||||
background: '#282C34',
|
||||
foreground: '#DCDFE4',
|
||||
cursor: '#A3B3CC',
|
||||
selectionBackground: '#474E5D',
|
||||
black: '#282C34',
|
||||
red: '#E06C75',
|
||||
green: '#98C379',
|
||||
yellow: '#E5C07B',
|
||||
blue: '#61AFEF',
|
||||
cyan: '#56B6C2',
|
||||
white: '#DCDFE4',
|
||||
brightBlack: '#282C34',
|
||||
brightRed: '#E06C75',
|
||||
brightGreen: '#98C379',
|
||||
brightYellow: '#E5C07B',
|
||||
brightBlue: '#61AFEF',
|
||||
brightCyan: '#56B6C2',
|
||||
brightWhite: '#DCDFE4'
|
||||
},
|
||||
{
|
||||
name: 'Dracula',
|
||||
dark: true,
|
||||
background: '#282A36',
|
||||
foreground: '#F8F8F2',
|
||||
cursor: '#F8F8F2',
|
||||
cursorAccent: '#282A36',
|
||||
selectionBackground: '#50FA7B',
|
||||
selectionForeground: '#44475A',
|
||||
black: '#21222C',
|
||||
red: '#FF5555',
|
||||
green: '#50FA7B',
|
||||
yellow: '#F1FA8C',
|
||||
blue: '#BD93F9',
|
||||
magenta: '#FF79C6',
|
||||
cyan: '#8BE9FD',
|
||||
white: '#F8F8F2',
|
||||
brightBlack: '#6272A4',
|
||||
brightRed: '#FF6E6E',
|
||||
brightGreen: '#69FF94',
|
||||
brightYellow: '#FFFFA5',
|
||||
brightBlue: '#D6ACFF',
|
||||
brightMagenta: '#FF92DF',
|
||||
brightCyan: '#A4FFFF',
|
||||
brightWhite: '#FFFFFF'
|
||||
},
|
||||
{
|
||||
name: 'Solarized Light',
|
||||
dark: false,
|
||||
background: '#FDF6E3',
|
||||
foreground: '#657B83',
|
||||
cursor: '#657B83',
|
||||
selectionBackground: '#E6DDC3',
|
||||
black: '#073642',
|
||||
red: '#DC322F',
|
||||
green: '#859900',
|
||||
yellow: '#B58900',
|
||||
blue: '#268BD2',
|
||||
cyan: '#2AA198',
|
||||
white: '#EEE8D5',
|
||||
brightBlack: '#002B36',
|
||||
brightRed: '#CB4B16',
|
||||
brightGreen: '#586E75',
|
||||
brightYellow: '#657B83',
|
||||
brightBlue: '#839496',
|
||||
brightCyan: '#93A1A1',
|
||||
brightWhite: '#FDF6E3'
|
||||
},
|
||||
{
|
||||
name: 'Material Design',
|
||||
dark: true,
|
||||
background: '#1D262A',
|
||||
foreground: '#E7EBED',
|
||||
cursor: '#EAEAEA',
|
||||
selectionBackground: '#4E6A78',
|
||||
black: '#435B67',
|
||||
red: '#FC3841',
|
||||
green: '#5CF19E',
|
||||
yellow: '#FED032',
|
||||
blue: '#37B6FF',
|
||||
cyan: '#59FFD1',
|
||||
white: '#FFFFFF',
|
||||
brightBlack: '#A1B0B8',
|
||||
brightRed: '#FC746D',
|
||||
brightGreen: '#ADF7BE',
|
||||
brightYellow: '#FEE16C',
|
||||
brightBlue: '#70CFFF',
|
||||
brightCyan: '#9AFFE6',
|
||||
brightWhite: '#FFFFFF'
|
||||
},
|
||||
{
|
||||
name: 'Duotone Dark',
|
||||
dark: true,
|
||||
background: '#1F1D27',
|
||||
foreground: '#B7A1FF',
|
||||
cursor: '#FF9839',
|
||||
selectionBackground: '#353147',
|
||||
black: '#1F1D27',
|
||||
red: '#D9393E',
|
||||
green: '#2DCD73',
|
||||
yellow: '#D9B76E',
|
||||
blue: '#FFC284',
|
||||
cyan: '#2488FF',
|
||||
white: '#B7A1FF',
|
||||
brightBlack: '#353147',
|
||||
brightRed: '#D9393E',
|
||||
brightGreen: '#2DCD73',
|
||||
brightYellow: '#D9B76E',
|
||||
brightBlue: '#FFC284',
|
||||
brightCyan: '#2488FF',
|
||||
brightWhite: '#EAE5FF'
|
||||
}
|
||||
] as Array<TerminalThemeSchema>;
|
||||
Reference in New Issue
Block a user