支持终端主题与背景图设置

This commit is contained in:
chaos-zhu 2024-08-12 17:10:30 +08:00
parent 4434a6d350
commit 268fa61b04
14 changed files with 253 additions and 42 deletions

View File

@ -39,7 +39,8 @@
"socket.io-client": "^4.7.5",
"vue": "^3.4.31",
"vue-codemirror": "^6.1.1",
"vue-router": "^4.4.0"
"vue-router": "^4.4.0",
"xterm-theme": "^1.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",

BIN
web/public/terminal/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
web/public/terminal/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
web/public/terminal/03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

BIN
web/public/terminal/04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
web/public/terminal/05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
web/public/terminal/06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
web/public/terminal/07.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
web/public/terminal/08.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
web/public/terminal/09.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@ -0,0 +1,148 @@
<template>
<el-dialog
v-model="visible"
width="600px"
top="120px"
title="终端设置"
:append-to-body="false"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
label-suffix=""
label-width="60px"
:show-message="false"
>
<el-form-item label="主题" prop="theme">
<el-select
v-model="theme"
placeholder=""
style="width: 100%;"
>
<el-option
v-for="(value, key) in themeList"
:key="key"
:label="key"
:value="key"
/>
</el-select>
</el-form-item>
<el-form-item label="背景" prop="backgroundImage">
<ul class="background_list">
<li :class="background ? '' : 'active'" @click="changeBackground('')">
<el-image class="image">
<template #error>
<div class="theme_background_text">
主题背景
</div>
</template>
</el-image>
</li>
<li
v-for="url in backgroundImages"
:key="url"
:class="background === url ? 'active' : ''"
@click="changeBackground(url)"
>
<el-image class="image" :src="url" />
</li>
</ul>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog_footer">
<el-button @click="visible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed } from 'vue'
import themeList from 'xterm-theme'
const props = defineProps({
show: {
required: true,
type: Boolean
},
themeName: {
required: true,
type: String
},
background: {
required: true,
type: String
}
})
const emit = defineEmits(['update:show', 'update:themeName', 'update:background',])
const backgroundImages = ref([
'/terminal/03.png',
'/terminal/04.png',
'/terminal/01.png',
'/terminal/02.png',
'/terminal/05.png',
'/terminal/06.png',
'/terminal/07.jpg',
'/terminal/08.jpg',
'/terminal/09.png',
])
const visible = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const theme = computed({
get: () => props.themeName,
set: (newVal) => emit('update:themeName', newVal)
})
const backgroundUrl = computed({
get: () => props.background,
set: (newVal) => emit('update:background', newVal)
})
const changeBackground = (url) => {
backgroundUrl.value = url || ''
}
</script>
<style lang="scss" scoped>
.background_list {
display: flex;
flex-wrap: wrap;
li {
width: 130px;
height: 75px;
box-sizing: border-box;
border-radius: 3px;
margin: 0 5px 5px 0;
display: flex;
align-items: center;
background: var(--el-fill-color-light);
color: var(--el-text-color-placeholder);
&:hover {
cursor: pointer;
box-shadow: 0 0 5px #1890ff;
}
&.active {
box-shadow: 0 0 5px #1890ff;
border: 1px solid #1890ff;
}
.image {
width: 100%;
height: 100%;
}
.theme_background_text {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.dialog_footer {
display: flex;
justify-content: center;
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<div ref="terminalRefs" class="terminal_tab_container" />
<div ref="terminalRef" class="terminal_tab_container" />
</template>
<script setup>
import { ref, onMounted, computed, onBeforeUnmount, getCurrentInstance } from 'vue'
import { ref, onMounted, computed, onBeforeUnmount, getCurrentInstance, watch, nextTick } from 'vue'
import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css'
import { FitAddon } from '@xterm/addon-fit'
@ -19,6 +19,14 @@ const props = defineProps({
host: {
required: true,
type: String
},
theme: {
required: true,
type: Object
},
background: {
required: true,
type: String
}
})
@ -31,9 +39,34 @@ const timer = ref(null)
const fitAddon = ref(null)
const searchBar = ref(null)
const isManual = ref(false)
const terminalRefs = ref(null)
const terminal = ref(null)
const terminalRef = ref(null)
const token = computed(() => $store.token)
const theme = computed(() => props.theme)
const background = computed(() => props.background)
watch(theme, () => {
nextTick(() => {
if (!background.value) terminal.value.options.theme = theme.value
else terminal.value.options.theme = { ...theme.value, background: '#00000080' }
})
})
watch(background, (newVal) => {
nextTick(() => {
if (newVal) {
// terminal.value.options.theme.background = '#00000080'
terminal.value.options.theme = { ...theme.value, background: '#00000080' }
terminalRef.value.style.backgroundImage = `url(${ background.value })`
terminalRef.value.style.backgroundImage = `url(${ background.value })`
// terminalRef.value.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)), url(${ background.value })`
} else {
terminal.value.options.theme.background = theme.value.background
terminalRef.value.style.backgroundImage = null
}
})
}, { immediate: true })
const getCommand = async () => {
let { data } = await $api.getCommand(props.host)
@ -117,7 +150,7 @@ const reConnect = () => {
}
const createLocalTerminal = () => {
let terminal = new Terminal({
let terminalInstance = new Terminal({
rendererType: 'dom',
bellStyle: 'sound',
convertEol: true,
@ -126,20 +159,22 @@ const createLocalTerminal = () => {
fontSize: 18,
minimumContrastRatio: 7,
allowTransparency: true,
theme: {
foreground: '#ECECEC',
background: '#000000', // 'transparent',
// cursor: 'help',
selection: '#ff9900',
lineHeight: 20
}
theme: theme.value
// {
// foreground: '#ECECEC',
// background: '#000000', // 'transparent',
// // cursor: 'help',
// selection: '#ff9900',
// lineHeight: 20
// }
})
term.value = terminal
terminal.open(terminalRefs.value)
terminal.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
terminal.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
terminal.focus()
term.value = terminalInstance
terminalInstance.open(terminalRef.value)
terminalInstance.writeln('\x1b[1;32mWelcome to EasyNode terminal\x1b[0m.')
terminalInstance.writeln('\x1b[1;32mAn experimental Web-SSH Terminal\x1b[0m.')
terminalInstance.focus()
onSelectionChange()
terminal.value = terminalInstance
}
const onResize = () => {
@ -315,9 +350,8 @@ defineExpose({
.terminal_tab_container {
min-height: 200px;
// background-image: url('@/assets/bg.jpg');
// background-size: cover;
// background-repeat: no-repeat;
background-size: 100% 100%;
background-repeat: no-repeat;
:deep(.xterm) {
height: 100%;

View File

@ -12,17 +12,6 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-dropdown trigger="click">
<span class="link_text">会话同步<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleSyncSession">
<el-icon v-show="isSyncAllSession"><Select class="action_icon" /></el-icon>
<span>同步键盘输入到所有会话</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
<el-dropdown
trigger="click"
max-height="50vh"
@ -38,6 +27,16 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-dropdown trigger="click" max-height="50vh">
<span class="link_text">主题<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(value, key) in themeList" :key="key" @click="handleChangeTheme(key)">
<span :style="{color: key === themeName ? 'var(--el-menu-active-color)' : ''}">{{ key }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
<el-dropdown trigger="click">
<span class="link_text">设置<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
@ -45,8 +44,8 @@
<el-dropdown-item @click="handleFullScreen">
<span>开启全屏</span>
</el-dropdown-item>
<el-dropdown-item disabled @click="handleFullScreen">
<span>终端设置(开发中)</span>
<el-dropdown-item @click="showSetting = true">
<span>终端设置</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -124,6 +123,8 @@
<TerminalTab
ref="terminalRefs"
:host="item.host"
:theme="themeObj"
:background="terminalBackground"
@input-command="terminalInput"
@cd-command="cdCommand"
/>
@ -144,6 +145,12 @@
@update-list="handleUpdateList"
@closed="updateHostData = null"
/>
<TerminalSetting
v-model:show="showSetting"
v-model:themeName="themeName"
v-model:background="terminalBackground"
@closed="showSetting = false"
/>
</div>
</template>
@ -155,7 +162,9 @@ import InfoSide from './info-side.vue'
import Sftp from './sftp.vue'
import InputCommand from '@/components/input-command/index.vue'
import HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
// import { randomStr } from '@utils/index.js'
import themeList from 'xterm-theme'
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
@ -172,19 +181,33 @@ const showInputCommand = ref(false)
const infoSideRef = ref(null)
const terminalRefs = ref([])
const sftpRefs = ref([])
let activeTabIndex = ref(0)
let visible = ref(true)
let showSftp = ref(localStorage.getItem('showSftp') === 'true')
let mainHeight = ref('')
let isSyncAllSession = ref(false)
let hostFormVisible = ref(false)
let updateHostData = ref(null)
const activeTabIndex = ref(0)
const visible = ref(true)
const showSftp = ref(localStorage.getItem('showSftp') === 'true')
const mainHeight = ref('')
const isSyncAllSession = ref(false)
const hostFormVisible = ref(false)
const updateHostData = ref(null)
const showSetting = ref(false)
const themeName = ref(localStorage.getItem('themeName') || 'Afterglow')
let localTerminalBackground = localStorage.getItem('terminalBackground')
const terminalBackground = ref(localTerminalBackground === undefined ? '/01.png' : localTerminalBackground)
const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
let hostList = computed(() => $store.hostList)
const hostList = computed(() => $store.hostList)
const curHost = computed(() => hostList.value.find(item => item.host === terminalTabs.value[activeTabIndex.value]?.host))
let scriptList = computed(() => $store.scriptList)
const scriptList = computed(() => $store.scriptList)
const themeObj = computed(() => themeList[themeName.value])
watch(themeName, (newVal) => {
console.log('update theme:', newVal)
localStorage.setItem('themeName', newVal)
})
watch(terminalBackground, (newVal) => {
console.log('update terminalBackground:', newVal)
localStorage.setItem('terminalBackground', newVal)
})
onMounted(() => {
handleResizeTerminalSftp()

View File

@ -4731,6 +4731,11 @@ xmlhttprequest-ssl@~2.0.0:
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
xterm-theme@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/xterm-theme/-/xterm-theme-1.1.0.tgz#2e499c4dd6d2cf592c90389022095e1ce509bbbe"
integrity sha512-n2GocBEbqcz4vEl4OYkU93hEVia8GWdnqchiz/0nQ/olRUyhulGf4wfha23x/D2m0imWaIavRZtt8c6kWZXdsA==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"