✨ 支持终端主题与背景图设置
@ -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
After Width: | Height: | Size: 14 KiB |
BIN
web/public/terminal/02.png
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
web/public/terminal/03.png
Normal file
After Width: | Height: | Size: 501 KiB |
BIN
web/public/terminal/04.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
web/public/terminal/05.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
web/public/terminal/06.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
web/public/terminal/07.jpg
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
web/public/terminal/08.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
web/public/terminal/09.png
Normal file
After Width: | Height: | Size: 177 KiB |
148
web/src/views/terminal/components/terminal-setting.vue
Normal 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>
|
@ -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%;
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|