feat. 终端主题设置.

This commit is contained in:
lijiahang
2023-12-08 14:33:24 +08:00
parent cfb4895c3a
commit f68745b80d
19 changed files with 5671 additions and 15289 deletions

View File

@@ -1,5 +1,5 @@
// 亮色主题配色常量
body[terminal-theme='light'] {
body {
--color-bg-header: #232323;
--color-bg-sidebar: #F2F3F5;
--color-bg-content: #FEFEFE;
@@ -7,6 +7,9 @@ body[terminal-theme='light'] {
--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);
}
// 暗色主题配色常量
@@ -18,6 +21,9 @@ body[terminal-theme='dark'] {
--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);
}
// 布局常量
@@ -29,42 +35,160 @@ body[terminal-theme='dark'] {
--sidebar-icon-font-size: 22px;
--color-bg-header-icon-1: #434343;
--color-header-tabs-sp: #4B4B4B;
--color-header-tabs-bg: var(--color-bg-header);
--color-header-tabs-bg-hover: #434343;
--color-header-text-1: rgba(255, 255, 255, .9);
--color-header-text-2: rgba(255, 255, 255, .75);
--color-gradient-start: rgba(46, 46, 46, 1);
--color-gradient-end: rgba(46, 46, 46, 0);
--color-gradient-hover-start: rgba(88, 88, 88, 1);
--color-gradient-hover-end: rgba(88, 88, 88, 0);
--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);
}
// 侧栏图标 wrapper
// 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;
.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);
&:hover {
background: var(--color-sidebar-icon-bg);
}
}
}
@@ -78,3 +202,28 @@ body[terminal-theme='dark'] {
.terminal-tooltip-arrow {
display: none;
}
// 终端设置容器
@setting-container-width: 1180px;
.terminal-setting-container {
padding: 32px 16px;
width: @setting-container-width;
margin: auto;
display: flex;
flex-direction: column;
.terminal-setting-title {
margin: 0 0 24px 0;
color: var(--color-content-text-3);
}
.terminal-setting-block {
color: var(--color-content-text-2);
margin-bottom: 18px;
}
.terminal-setting-subtitle {
margin: 0 0 16px 0;
color: var(--color-content-text-3);
}
}

View File

@@ -0,0 +1,94 @@
<template>
<div class="terminal-content">
<!-- 内容 tabs -->
<a-tabs v-model:active-key="activeKey">
<a-tab-pane v-for="tab in tabs"
:key="tab.key"
:title="tab.title">
<!-- 设置 -->
<template v-if="tab.type === TabType.SETTING">
<!-- 主题设置 -->
<terminal-theme-setting v-if="tab.key === InnerTabs.THEME_SETTING.key"
@emitter="dispatchEmitter" />
<span v-else>
{{ tab.title }}
</span>
</template>
<!-- 终端 -->
<template v-else-if="tab.type === TabType.TERMINAL">
终端 {{ 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 type { PropType } from 'vue';
import type { TabItem } from '../../types/terminal.type';
import { computed } from 'vue';
import { TabType, InnerTabs } from '../../types/terminal.type';
import TerminalThemeSetting from '../terminal-theme-setting.vue';
import useEmitter from '@/hooks/emitter';
const props = defineProps({
modelValue: {
type: String,
required: true
},
tabs: {
type: Array as PropType<Array<TabItem>>,
required: true
}
});
const emits = defineEmits(['changeDarkTheme']);
const { dispatchEmitter } = useEmitter(emits);
const activeKey = computed<String>({
get() {
return props.modelValue;
},
set() {
}
});
</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>

View File

@@ -6,7 +6,16 @@
</div>
<!-- 左侧 tabs -->
<div class="terminal-header-tabs">
<slot />
<a-tabs v-model:active-key="activeKey"
:editable="true"
:hide-content="true"
:auto-switch="true"
@tab-click="e => emits('clickTab', e)"
@delete="e => emits('deleteTab', e)">
<a-tab-pane v-for="tab in tabs"
:key="tab.key"
:title="tab.title" />
</a-tabs>
</div>
<!-- 右侧操作 -->
<div class="terminal-header-right">
@@ -37,13 +46,26 @@
</script>
<script lang="ts" setup>
import type { SidebarAction } from '../../types/terminal.type';
import type { SidebarAction, TabItem } from '../../types/terminal.type';
import type { PropType } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { computed } from 'vue';
import IconActions from '@/views/host-ops/terminal/components/layout/icon-actions.vue';
import IconActions from '../layout/icon-actions.vue';
const props = defineProps({
modelValue: {
type: String,
required: true
},
tabs: {
type: Array as PropType<Array<TabItem>>,
required: true
}
});
const emits = defineEmits(['update:modelValue', 'clickTab', 'deleteTab', 'split', 'share']);
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const emits = defineEmits(['split', 'share']);
// 顶部操作
const actions = computed<Array<SidebarAction>>(() => [
@@ -66,6 +88,19 @@
},
]);
const activeKey = computed<String>({
get() {
return props.modelValue;
},
set(e) {
if (e) {
emits('update:modelValue', e);
} else {
emits('update:modelValue', null);
}
}
});
</script>
<style lang="less" scoped>
@@ -77,7 +112,7 @@
.terminal-header {
height: 100%;
color: var(--color-header-text-1);
color: var(--color-header-text-2);
display: flex;
user-select: none;
@@ -118,11 +153,11 @@
}
}
&-icon {
color: var(--color-header-text-1);
:deep(&-icon) {
color: var(--color-header-text-2) !important;
&:hover {
background: var(--color-bg-header-icon-1);
background: var(--color-bg-header-icon-1) !important;
}
}
}
@@ -143,7 +178,7 @@
}
&-button .arco-icon-hover:hover {
color: var(--color-header-text-1);
color: var(--color-header-text-2);
&::before {
background: var(--color-bg-header-icon-1);
@@ -159,16 +194,12 @@
height: 100%;
margin: 0;
padding: 0;
color: var(--color-header-text-2);
color: var(--color-header-text-1);
background: var(--color-header-tabs-bg);
position: relative;
&:not(:first-child) {
border-left: 1px solid var(--color-header-tabs-sp);
}
&:hover {
color: var(--color-header-text-1);
color: var(--color-header-text-2);
transition: .2s;
}
@@ -212,7 +243,7 @@
right: 0;
z-index: 4;
display: none;
color: var(--color-header-text-1);
color: var(--color-header-text-2);
&:hover {
transition: .2s;
@@ -227,14 +258,14 @@
:deep(.arco-tabs-tab-active) {
background: var(--color-header-tabs-bg-hover);
color: var(--color-header-text-1) !important;
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-hover-start) 45%, var(--color-gradient-hover-end) 120%);
background: linear-gradient(270deg, var(--color-gradient-start) 45%, var(--color-gradient-end) 120%);
}
}

View File

@@ -18,17 +18,18 @@
</script>
<script lang="ts" setup>
import { SidebarAction } from '../../types/terminal.type';
import type { SidebarAction, } from '../../types/terminal.type';
import { InnerTabs } from '../../types/terminal.type';
import IconActions from './icon-actions.vue';
const emits = defineEmits(['openAdd', 'copyAddress', 'openShortcutSetting', 'openViewSetting', 'openThemeSetting']);
const emits = defineEmits(['switchTab', 'copyAddress']);
// 顶部操作
const topActions: Array<SidebarAction> = [
{
icon: 'icon-plus',
content: '新建连接',
click: () => emits('openAdd')
click: () => emits('switchTab', InnerTabs.HOST_LIST)
},
{
icon: 'icon-copy',
@@ -42,17 +43,17 @@
{
icon: 'icon-command',
content: '快捷键设置',
click: () => emits('openShortcutSetting')
click: () => emits('switchTab', InnerTabs.SHORTCUT_SETTING)
},
{
icon: 'icon-palette',
content: '主题设置',
click: () => emits('openThemeSetting')
click: () => emits('switchTab', InnerTabs.THEME_SETTING)
},
{
icon: 'icon-tool',
content: '显示设置',
click: () => emits('openViewSetting')
click: () => emits('switchTab', InnerTabs.VIEW_SETTING)
},
];

View File

@@ -1,8 +1,12 @@
<template>
<div class="terminal-right-sidebar">
<!-- 操作按钮 -->
<icon-actions class="terminal-actions"
:actions="actions"
<!-- 顶部操作按钮 -->
<icon-actions class="top-actions"
:actions="topActions"
position="left" />
<!-- 底部操作按钮 -->
<icon-actions class="bottom-actions"
:actions="bottomActions"
position="left" />
</div>
</template>
@@ -14,13 +18,13 @@
</script>
<script lang="ts" setup>
import { SidebarAction } from '../../types/terminal.type';
import type { SidebarAction } from '../../types/terminal.type';
import IconActions from './icon-actions.vue';
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'openHistory']);
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'openHistory', 'screenshot']);
// 操作
const actions: Array<SidebarAction> = [
// 顶部操作
const topActions: Array<SidebarAction> = [
{
icon: 'icon-code-block',
content: '打开命令片段',
@@ -49,10 +53,23 @@
},
];
// 底部操作
const bottomActions: Array<SidebarAction> = [
{
icon: 'icon-camera',
content: '截图',
style: {},
click: () => emits('screenshot')
},
];
</script>
<style lang="less" scoped>
.terminal-right-sidebar {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>

View File

@@ -1,23 +0,0 @@
<template>
<div class="terminal-content">
<slot />
</div>
</template>
<script lang="ts">
export default {
name: 'TerminalContent'
};
</script>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
.terminal-content {
width: 100%;
height: 100%;
position: relative;
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div id="" class="terminal-example" ref="terminal"></div>
</template>
<script lang="ts">
export default {
name: 'terminalExample'
};
</script>
<script lang="ts" setup>
import { Terminal } from '@xterm/xterm';
import { onMounted, ref } from 'vue';
import { TerminalTheme } from '../types/terminal.theme';
const props = defineProps<{
theme: TerminalTheme
}>();
const terminal = ref();
onMounted(() => {
const term = new Terminal({
theme: props.theme,
cols: 47,
rows: 6,
fontSize: 15,
convertEol: true,
cursorBlink: false,
cursorInactiveStyle: 'none'
});
term.open(terminal.value);
term.write(
'[root@OrionServer usr]#\n' +
'dr-xr-xr-x. 2 root root bin\n' +
'dr-xr-xr-x. 2 root root sbin\n' +
'dr-xr-xr-x. 43 root root lib\n' +
'dr-xr-xr-x. 62 root root lib64\n' +
'lrwxrwxrwx. 1 root root tmp'
);
});
</script>
<style lang="less" scoped>
.terminal-example {
padding: 16px;
width: 100%;
height: 100%;
}
:deep(.xterm-viewport) {
overflow: hidden !important;
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<div class="terminal-setting-container theme-setting-container">
<div class="theme-setting-wrapper">
<!-- 大标题 -->
<h2 class="terminal-setting-title">
主题设置
</h2>
<!-- 切换主题 -->
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="theme-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
主题选择
</h3>
<a-radio-group v-model="userDarkTheme"
size="mini"
type="button"
@change="changeDarkTheme">
<a-radio v-for="theme in DarkTheme" :key="theme.value" :value="theme.value">
{{ theme.label }}
</a-radio>
</a-radio-group>
</div>
<!-- 内容区域 -->
<div class="theme-list">
<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 === userTerminalTheme.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)'
}"
@click="checkTheme(theme)">
<!-- 样例 -->
<terminal-example :theme="theme" />
<icon-check class="theme-check-icon" :style="{
display: theme.name === userTerminalTheme.name ? 'flex': 'none'
}" />
</a-card>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'TerminalThemeSetting'
};
</script>
<script lang="ts" setup>
import type { TerminalTheme } from '../types/terminal.theme';
import { DarkTheme } from '../types/terminal.type';
import ThemeSchema, { FRAPPE } from '../types/terminal.theme';
import useEmitter from '@/hooks/emitter';
import { onBeforeMount, ref } from 'vue';
import TerminalExample from './terminal-example.vue';
import { useDebounceFn } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
defineProps();
const emits = defineEmits(['emitter']);
const { bubblesEmitter } = useEmitter(emits);
interface TerminalPreference {
darkTheme: string,
terminalTheme: TerminalTheme
}
const userDarkTheme = ref(DarkTheme.DARK.value);
const userTerminalTheme = ref<TerminalTheme>(FRAPPE);
// 修改暗色主题
const changeDarkTheme = (value: string) => {
if (value === DarkTheme.DARK.value) {
// 暗色
bubblesEmitter('changeDarkTheme', true);
} else if (value === DarkTheme.LIGHT.value) {
// 亮色
bubblesEmitter('changeDarkTheme', false);
} else if (value === DarkTheme.AUTO.value) {
// 自动配色
bubblesEmitter('changeDarkTheme', userTerminalTheme.value.dark ? DarkTheme.DARK.value : DarkTheme.LIGHT.value);
}
sync();
};
// 选择终端主题
const checkTheme = (theme: TerminalTheme) => {
userTerminalTheme.value = theme;
// 切换主题配色
if (userDarkTheme.value === DarkTheme.AUTO.value) {
changeDarkTheme(theme.dark ? DarkTheme.DARK.value : DarkTheme.LIGHT.value);
} else {
sync();
}
};
// 同步用户偏好
const syncUserPreference = async () => {
try {
// FIXME 同步用户配置
Message.success('同步成功');
} catch (e) {
Message.error('同步失败');
}
};
// 同步用户偏好防抖
const sync = useDebounceFn(syncUserPreference, 1500);
onBeforeMount(() => {
// FIXME 加载用户配置
});
</script>
<style lang="less" scoped>
@terminal-width: 458px;
@terminal-height: 182px;
@wrapper-width: @terminal-width * 2 + 16;
.theme-setting-wrapper {
width: @wrapper-width;
user-select: none;
}
.theme-subtitle-wrapper {
display: flex;
justify-content: space-between;
align-items: flex-start;
.terminal-setting-subtitle {
margin: 0;
}
}
.theme-list {
margin-top: 16px;
.theme-row {
display: flex;
margin-bottom: 16px;
}
}
.terminal-theme-card {
width: @terminal-width;
height: @terminal-height;
border: 2px solid var(--color-border);
cursor: pointer;
:deep(.arco-card-header) {
padding: 4px 16px;
height: 40px;
border-bottom: .5px solid #DEDEDE;
&-title {
color: unset;
font-size: 16px;
font-weight: 600;
}
}
:deep(.arco-card-body) {
height: calc(@terminal-height - 44px);
padding: 0;
display: flex;
position: relative;
.theme-check-icon {
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>

View File

@@ -2,33 +2,22 @@
<div class="host-layout">
<!-- 头部区域 -->
<header class="host-layout-header">
<terminal-header>
<!-- 主机 tabs -->
<a-tabs :editable="true"
:hide-content="true"
@tab-click="clickTab"
@delete="deleteTab">
<a-tab-pane v-for="i in 30"
:key="i"
:title="'主机主机主机'+i+''" />
</a-tabs>
</terminal-header>
<terminal-header v-model="activeKey"
:tabs="tabs"
@click-tab="clickTab"
@deleteTab="deleteTab" />
</header>
<!-- 主体区域 -->
<main class="host-layout-main">
<!-- 左侧操作栏 -->
<div class="host-layout-left">
<terminal-left-sidebar />
<terminal-left-sidebar @switch-tab="switchTab" />
</div>
<!-- 内容区域 -->
<div class="host-layout-content">
<terminal-content>
<div class="my16 mx16">
<a-button @click="changeTheme">
{{ darkTheme }}
</a-button>
</div>
</terminal-content>
<terminal-content v-model="activeKey"
:tabs="tabs"
@change-dark-theme="changeLayoutTheme" />
</div>
<!-- 右侧操作栏 -->
<div class="host-layout-right">
@@ -45,36 +34,72 @@
</script>
<script lang="ts" setup>
import type { TabItem } from './types/terminal.type';
import { ref } from 'vue';
import { useDark } from '@vueuse/core';
import { TabType, InnerTabs, DarkTheme } from './types/terminal.type';
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/terminal-content.vue';
import { useDark } from '@vueuse/core';
import TerminalContent from './components/layout/terminal-content.vue';
import './assets/styles/layout.less';
import '@xterm/xterm/css/xterm.css';
import { onBeforeMount } from 'vue/dist/vue';
// 主题
// 系统主题
const darkTheme = useDark({
selector: 'body',
attribute: 'terminal-theme',
valueDark: 'dark',
valueLight: 'light',
initialValue: 'dark',
valueDark: DarkTheme.DARK.value,
valueLight: DarkTheme.LIGHT.value,
initialValue: DarkTheme.DARK.value as any,
storageKey: null
});
const changeTheme = () => {
console.log('current', darkTheme.value);
darkTheme.value = !darkTheme.value;
const activeKey = ref(InnerTabs.THEME_SETTING.key);
const tabs = ref<Array<TabItem>>([InnerTabs.THEME_SETTING]);
for (let i = 0; i < 3; i++) {
tabs.value.push({
key: `host${i}`,
title: `主机name ${i}`,
type: TabType.TERMINAL
});
}
// 切换系统主题
const changeLayoutTheme = (dark: boolean) => {
darkTheme.value = dark;
};
changeLayoutTheme(false);
// 点击 tab
const clickTab = (key: string) => {
activeKey.value = key;
};
const clickTab = (v: any) => {
console.log('click', v);
// 删除 tab
const deleteTab = (key: string) => {
const tabIndex = tabs.value.findIndex(s => s.key === key);
tabs.value.splice(tabIndex, 1);
if (key === activeKey.value && tabs.value.length !== 0) {
// 切换为前一个 tab
activeKey.value = tabs.value[Math.max(tabIndex - 1, 0)].key;
}
};
const deleteTab = (v: any) => {
console.log('delete', v);
// 切换 tab
const switchTab = (tab: TabItem) => {
// 不存在则创建tab
if (!tabs.value.find(s => s.key === tab.key)) {
tabs.value.push(tab);
}
activeKey.value = tab.key;
};
onBeforeMount(() => {
// FIXME 加载用户配置
});
</script>
<style lang="less" scoped>
@@ -92,7 +117,6 @@
&-main {
width: 100%;
height: calc(100% - var(--sidebar-width));
overflow: hidden;
position: relative;
display: flex;
justify-content: space-between;
@@ -103,12 +127,15 @@
height: 100%;
background: var(--color-bg-sidebar);
border-top: 1px solid var(--color-bg-content);
overflow: hidden;
}
&-content {
width: 100%;
height: 100%;
background: var(--color-bg-content);
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,280 @@
// 主题
export interface TerminalTheme {
name: string;
dark: boolean;
background: string;
foreground: string;
cursor: string;
cursorAccent?: string;
selectionInactiveBackground?: string;
selectionBackground?: string;
selectionForeground?: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
[key: string]: unknown;
}
// 默认配色
export const FRAPPE = {
name: 'frappe',
dark: true,
background: '#303446',
foreground: '#C6D0F5',
cursor: '#F2D5CF',
cursorAccent: '#232634',
// selectionInactiveBackground: 'rgba(98, 104, 128, 0.30078125)',
selectionBackground: '#C9DDF0',
selectionForeground: '#303446',
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 [
FRAPPE,
{
name: 'latte',
dark: false,
background: '#EFF1F5',
foreground: '#4C4F69',
cursor: '#DC8A78',
cursorAccent: '#EFF1F5',
// selectionInactiveBackground: 'rgba(172, 176, 190, 0.30078125)',
selectionForeground: '#EFF1F5',
selectionBackground: '#6C6F85',
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',
// selectionInactiveBackground: 'rgba(91, 96, 120, 0.30078125)',
selectionForeground: '#24273A',
selectionBackground: '#A5ADCB',
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',
// selectionInactiveBackground: 'rgba(88, 91, 112, 0.30078125)',
selectionForeground: '#1E1E2E',
selectionBackground: '#A6ADC8',
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: 'AtomOneLight',
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: 'OneHalfDark',
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',
selectionForeground: '#44475A',
selectionBackground: '#50FA7B',
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<TerminalTheme>;

View File

@@ -1,5 +1,21 @@
import type { CSSProperties } from 'vue';
// 暗色主题
export const DarkTheme = {
DARK: {
value: 'dark',
label: '暗色'
},
LIGHT: {
value: 'light',
label: '亮色'
},
AUTO: {
value: 'auto',
label: '自动'
}
};
// sidebar 操作类型
export interface SidebarAction {
icon: string;
@@ -8,3 +24,42 @@ export interface SidebarAction {
visible?: boolean;
click: () => void;
}
// tab 类型
export const TabType = {
SETTING: 'setting',
TERMINAL: 'terminal',
};
// 内置 tab
export const InnerTabs = {
HOST_LIST: {
key: 'hostList',
title: '新建连接',
type: TabType.SETTING
},
SHORTCUT_SETTING: {
key: 'shortcutSetting',
title: '快捷键设置',
type: TabType.SETTING
},
THEME_SETTING: {
key: 'themeSetting',
title: '主题设置',
type: TabType.SETTING
},
VIEW_SETTING: {
key: 'viewSetting',
title: '显示设置',
type: TabType.SETTING
},
};
// tab 元素
export interface TabItem {
key: string;
title: string;
type: string;
[key: string]: unknown;
}