feat: 终端设置入口优化

This commit is contained in:
chaoszhu
2026-08-29 22:26:03 +08:00
parent 989a25e443
commit 1982d1d0f6
6 changed files with 342 additions and 253 deletions
@@ -1,14 +1,5 @@
<template>
<el-drawer
v-model="visible"
title="菜单选项"
:direction="isMobileScreen ? 'ttb' : 'ltr'"
:close-on-click-modal="true"
:close-on-press-escape="true"
:modal="true"
modal-class="menu_options_drawer"
:size="isMobileScreen ? '80%' : '30%'"
>
<div class="menu_options_content">
<el-form
ref="formRef"
label-suffix=":"
@@ -49,34 +40,13 @@
</span>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog_footer">
<el-button @click="visible = false">关闭</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script setup>
import { computed, getCurrentInstance } from 'vue'
import useMobileWidth from '@/composables/useMobileWidth'
const { proxy: { $store } } = getCurrentInstance()
const { isMobileScreen } = useMobileWidth()
const props = defineProps({
show: {
required: true,
type: Boolean
}
})
const emit = defineEmits(['update:show'])
const visible = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const scriptLibrary = computed({
get: () => $store.menuSetting.scriptLibrary,
@@ -95,19 +65,7 @@ const hostGroupCascader = computed({
</script>
<style lang="scss" scoped>
.dialog_footer {
display: flex;
justify-content: center;
}
.script_library_switch {
margin-left: 20px;
}
</style>
<style lang="scss">
.menu_options_drawer {
.el-drawer__header {
margin-bottom: 0 !important;
}
}
</style>
@@ -1,14 +1,5 @@
<template>
<el-drawer
v-model="visible"
title="其他设置"
:direction="isMobileScreen ? 'ttb' : 'ltr'"
:close-on-click-modal="true"
:close-on-press-escape="true"
:modal="true"
modal-class="other_setting_drawer"
:size="isMobileScreen ? '80%' : '30%'"
>
<div class="other_settings_content">
<el-form
ref="formRef"
label-suffix=":"
@@ -60,34 +51,13 @@
</el-tooltip>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog_footer">
<el-button @click="visible = false">关闭</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script setup>
import { computed, getCurrentInstance } from 'vue'
import useMobileWidth from '@/composables/useMobileWidth'
const { proxy: { $store } } = getCurrentInstance()
const { isMobileScreen } = useMobileWidth()
const props = defineProps({
show: {
required: true,
type: Boolean
}
})
const emit = defineEmits(['update:show',])
const visible = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const autoReconnect = computed({
get: () => $store.terminalConfig.autoReconnect,
@@ -105,18 +75,3 @@ const autoShowContextMenu = computed({
})
</script>
<style lang="scss" scoped>
.dialog_footer {
display: flex;
justify-content: center;
}
</style>
<style lang="scss">
.other_setting_drawer {
.el-drawer__header {
margin-bottom: 0 !important;
}
}
</style>
@@ -1,14 +1,5 @@
<template>
<el-drawer
v-model="dialogVisible"
title="高亮设置"
:direction="isMobileScreen ? 'ttb' : 'ltr'"
:close-on-click-modal="true"
:close-on-press-escape="true"
:modal="true"
modal-class="local_setting_drawer"
:size="isMobileScreen ? '80%' : '35%'"
>
<div class="terminal_highlight_settings_content">
<el-form
ref="formRef"
label-suffix=":"
@@ -169,12 +160,6 @@
/>
</el-form>
<template #footer>
<span class="dialog_footer">
<el-button @click="handleCancel">关闭</el-button>
</span>
</template>
<!-- 规则编辑对话框 -->
<RuleEditDialog
v-model:show="showRuleEdit"
@@ -201,32 +186,16 @@
<el-button type="primary" @click="confirmImportRules">确认导入</el-button>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
<script setup>
import { ref, computed, watch, getCurrentInstance } from 'vue'
import { ref, computed, getCurrentInstance, onMounted } from 'vue'
import { HIGHLIGHT_RULES } from '@/utils/highlighter'
import useMobileWidth from '@/composables/useMobileWidth'
import RuleEditDialog from './rule-edit-dialog.vue'
import themeList from 'xterm-theme'
const { proxy: { $store, $message, $messageBox } } = getCurrentInstance()
const { isMobileScreen } = useMobileWidth()
const props = defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:show',])
const dialogVisible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
// 高亮配置
const highlightConfig = ref({
@@ -637,17 +606,7 @@ const handleConfigChange = async () => {
}
}
// 关闭对话框
const handleCancel = () => {
dialogVisible.value = false
}
// 监听对话框显示
watch(() => props.show, (show) => {
if (show) {
loadConfig()
}
})
onMounted(loadConfig)
</script>
<style lang="scss" scoped>
@@ -1,14 +1,5 @@
<template>
<el-drawer
v-model="visible"
title="基础设置"
:direction="isMobileScreen ? 'ttb' : 'ltr'"
:close-on-click-modal="true"
:close-on-press-escape="true"
:modal="true"
modal-class="terminal_setting_drawer"
:size="isMobileScreen ? '80%' : '30%'"
>
<div class="terminal_setting_content">
<el-form
ref="formRef"
label-suffix=":"
@@ -134,30 +125,14 @@
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog_footer">
<el-button @click="visible = false">关闭</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script setup>
import { computed, getCurrentInstance, ref } from 'vue'
import themeList from 'xterm-theme'
import useMobileWidth from '@/composables/useMobileWidth'
const { proxy: { $store } } = getCurrentInstance()
const { isMobileScreen } = useMobileWidth()
const props = defineProps({
show: {
required: true,
type: Boolean
}
})
const emit = defineEmits(['update:show',])
// 全平台通用字体列表
const commonFonts = [
@@ -189,11 +164,6 @@ const lastCustomCursorColor = ref('#00ff41')
const defaultBackgroundImages = computed(() => $store.defaultBackgroundImages)
const visible = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const theme = computed({
get: () => $store.terminalConfig.themeName,
set: (newVal) => $store.setTerminalSetting({ themeName: newVal })
@@ -407,16 +377,4 @@ const changeBackground = (item) => {
.custom_background {
width: 90%;
}
.dialog_footer {
display: flex;
justify-content: center;
}
</style>
<style lang="scss">
.terminal_setting_drawer {
.el-drawer__header {
margin-bottom: 0 !important;
}
}
</style>
@@ -0,0 +1,266 @@
<template>
<el-dialog
v-model="visible"
title="终端设置"
class="terminal_settings_dialog"
:width="dialogWidth"
:draggable="!isMobileScreen"
:close-on-click-modal="false"
destroy-on-close
>
<div class="terminal_settings_container">
<el-tabs
v-model="activeTab"
:tab-position="isMobileScreen ? 'top' : 'left'"
class="terminal_settings_tabs"
@tab-change="handleTabChange"
>
<el-tab-pane
v-for="tab in settingTabs"
:key="tab.name"
:label="tab.label"
:name="tab.name"
:lazy="tab.lazy"
>
<section class="terminal_setting_page">
<header class="terminal_setting_page_header">
<h2>{{ tab.title }}</h2>
<p>{{ tab.description }}</p>
</header>
<div class="terminal_setting_page_content">
<component :is="tab.component" />
</div>
</section>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import useMobileWidth from '@/composables/useMobileWidth'
import TerminalSetting from './terminal-setting.vue'
import TerminalHighlightSettings from './terminal-highlight-settings.vue'
import OtherSettings from './other-settings.vue'
import MenuOptions from './menu-options.vue'
const props = defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:show',])
const { isMobileScreen } = useMobileWidth()
const activeTab = ref('basic')
const settingTabs = [
{
label: '外观',
name: 'basic',
title: '终端外观',
description: '调整终端主题、字体、颜色与背景。',
component: TerminalSetting,
lazy: false
},
{
label: '高亮规则',
name: 'highlight',
title: '关键词高亮',
description: '配置关键词高亮规则,并预览终端中的显示效果。',
component: TerminalHighlightSettings,
lazy: true
},
{
label: '交互行为',
name: 'other',
title: '终端交互',
description: '设置自动重连、脚本执行和右键菜单行为。',
component: OtherSettings,
lazy: true
},
{
label: '菜单展示',
name: 'menu',
title: '顶部菜单',
description: '控制实例与脚本库菜单的显示和分组方式。',
component: MenuOptions,
lazy: true
},
]
const visible = computed({
get: () => props.show,
set: (value) => emit('update:show', value)
})
const dialogWidth = computed(() => isMobileScreen.value ? '96%' : '900px')
const handleTabChange = () => {
nextTick(() => {
document.querySelector('.terminal_settings_dialog .el-tabs__content')?.scrollTo({ top: 0 })
})
}
</script>
<style lang="scss" scoped>
.terminal_settings_container {
height: min(68vh, 680px);
min-height: 480px;
overflow: hidden;
}
.terminal_settings_tabs {
height: 100%;
> :deep(.el-tabs__header.is-left) {
width: 148px;
margin-right: 24px;
padding: 8px;
box-sizing: border-box;
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
background: var(--el-fill-color-extra-light);
}
> :deep(.el-tabs__header.is-left .el-tabs__nav),
> :deep(.el-tabs__header.is-left .el-tabs__item) {
width: 100%;
}
> :deep(.el-tabs__header.is-left .el-tabs__item) {
justify-content: flex-start;
height: 42px;
margin: 2px 0;
padding: 0 14px;
border-radius: 7px;
&:hover {
background: var(--el-fill-color-light);
}
&.is-active {
background: var(--el-color-primary-light-9);
}
}
> :deep(.el-tabs__header .el-tabs__nav-wrap::after),
> :deep(.el-tabs__header .el-tabs__active-bar) {
display: none;
}
> :deep(.el-tabs__content) {
height: 100%;
padding-right: 4px;
overflow: auto;
}
}
.terminal_setting_page {
padding: 2px 0 20px;
}
.terminal_setting_page_header {
margin-bottom: 18px;
h2 {
margin: 0 0 6px;
color: var(--el-text-color-primary);
font-size: 22px;
font-weight: 600;
line-height: 1.4;
}
p {
margin: 0;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.6;
}
}
.terminal_setting_page_content {
min-width: 0;
padding: 24px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
background: var(--el-bg-color);
box-shadow: var(--el-box-shadow-lighter);
:deep(.el-form) {
max-width: 100%;
}
}
@media (max-width: 968px) {
.terminal_settings_container {
height: 70vh;
min-height: 0;
}
.terminal_settings_tabs {
display: flex;
flex-direction: column;
> :deep(.el-tabs__header.is-top) {
flex-shrink: 0;
margin: 0 0 12px;
padding: 4px 8px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 9px;
background: var(--el-fill-color-extra-light);
}
> :deep(.el-tabs__header.is-top .el-tabs__item) {
height: 40px;
padding: 0 14px;
border-radius: 6px;
&.is-active {
background: var(--el-color-primary-light-9);
}
}
> :deep(.el-tabs__content) {
flex: 1;
padding-right: 0;
}
}
.terminal_setting_page_header h2 {
font-size: 20px;
}
.terminal_setting_page_content {
padding: 18px 16px;
border-radius: 8px;
}
}
</style>
<style lang="scss">
.terminal_settings_dialog {
max-width: calc(100vw - 24px);
.el-dialog__header {
margin-right: 0;
padding-bottom: 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
padding-top: 14px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
</style>
@@ -104,41 +104,17 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
trigger="click"
:teleported="isMobileScreen"
<button class="top_menu_action" type="button" @click="showSettings = true">
设置
</button>
<button
v-if="!isMobileScreen"
class="top_menu_action"
type="button"
@click="handleFullScreen"
>
<span class="link_text">终端设置<el-icon class="link_icon"><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showSetting = true">
<span>基础设置</span>
</el-dropdown-item>
<el-dropdown-item @click="showHighlightSettings = true">
<span>高亮设置</span>
</el-dropdown-item>
<el-dropdown-item @click="showOtherSettings = true">
<span>其他设置</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
trigger="click"
:teleported="isMobileScreen"
>
<span class="link_text">功能项<el-icon class="link_icon"><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleFullScreen">
<span>全屏</span>
</el-dropdown-item>
<el-dropdown-item @click="showMenuOptions = true">
<span>菜单选项</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
{{ isFullscreen ? '退出全屏' : '全屏' }}
</button>
</div>
<div class="right_overview">
<div class="switch_wrap">
@@ -481,10 +457,7 @@
@closed="updateHostData = null"
/>
<TerminalSetting v-model:show="showSetting" />
<TerminalHighlightSettings v-model:show="showHighlightSettings" />
<OtherSettings v-model:show="showOtherSettings" />
<MenuOptions v-model:show="showMenuOptions" />
<TerminalSettingsDialog v-model:show="showSettings" />
<TerminalSessionSetting v-model:show="showSessionSetting" />
</div>
</template>
@@ -508,10 +481,7 @@ import { useTerminalTabContextMenu } from '@/composables/useTerminalTabContextMe
import Terminal from './terminal.vue'
import ServerStatus from './server-status.vue'
import HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
import TerminalHighlightSettings from './terminal-highlight-settings.vue'
import OtherSettings from './other-settings.vue'
import MenuOptions from './menu-options.vue'
import TerminalSettingsDialog from './terminal-settings-dialog.vue'
import FooterBar from './footer-bar.vue'
import SftpV2 from './sftp-v2.vue'
import TerminalSingleWindow from './terminal-single-window.vue'
@@ -549,10 +519,8 @@ watch(layoutMode, (newMode) => {
})
const hostFormVisible = ref(false)
const updateHostData = ref(null)
const showSetting = ref(false)
const showHighlightSettings = ref(false)
const showOtherSettings = ref(false)
const showMenuOptions = ref(false)
const showSettings = ref(false)
const isFullscreen = ref(false)
const showSessionSetting = ref(false)
const showInfoSide = ref(isMobileScreen.value ? false : localStorage.getItem('showInfoSide') !== 'false')
const showTerminalAi = ref(isMobileScreen.value ? false : localStorage.getItem('showTerminalAi') === 'true')
@@ -1118,10 +1086,11 @@ const removeTab = (index) => {
const handleFullScreen = () => {
if (isMobileScreen.value) return
if (document.fullscreenElement) document?.exitFullscreen()
document
.getElementsByClassName('terminal_wrap')[0]
.requestFullscreen()
if (document.fullscreenElement) {
document.exitFullscreen()
return
}
document.querySelector('.terminal_wrap')?.requestFullscreen()
}
const handleHorizontalScreen = () => {
@@ -1308,6 +1277,7 @@ const handleSuspendTerminalSingleDone = (terminalKey) => {
}
const fullScreenCb = async() => {
isFullscreen.value = !!document.fullscreenElement
await $nextTick()
setTimeout(() => {
resizeTerminal()
@@ -1415,6 +1385,29 @@ onUnmounted(() => {
}
}
.top_menu_action {
margin: 0 10px 0 0;
padding: 0;
border: 0;
background: transparent;
color: var(--el-text-color-regular);
font: inherit;
font-size: var(--el-font-size-base);
line-height: 30px;
white-space: nowrap;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
&:focus-visible {
border-radius: 3px;
outline: 2px solid var(--el-color-primary-light-5);
outline-offset: 2px;
}
}
.left_menu {
display: flex;
align-items: center;