2024-08-18 18:10:59 +08:00

529 lines
14 KiB
Vue

<template>
<div class="terminal_wrap">
<div class="terminal_top">
<div class="left_menu">
<el-dropdown trigger="click">
<span class="link_text">新建连接<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item, index) in hostList" :key="index" @click="handleCommandHost(item)">
{{ item.name }} {{ item.host }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown
trigger="click"
max-height="50vh"
:teleported="false"
class="scripts_menu"
>
<span class="link_text">脚本库<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in scriptList" :key="item.id" @click="handleExecScript(item)">
<span>{{ item.name }}</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>
<el-dropdown-menu>
<el-dropdown-item @click="handleFullScreen">
<span>双屏</span>
</el-dropdown-item>
<el-dropdown-item @click="handleFullScreen">
<span>三屏</span>
</el-dropdown-item>
<el-dropdown-item @click="handleFullScreen">
<span>四屏</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>
<el-dropdown-menu>
<el-dropdown-item @click="handleFullScreen">
<span>开启全屏</span>
</el-dropdown-item>
<el-dropdown-item @click="showSetting = true">
<span>终端设置</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="right_overview">
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="开启后同步键盘输入到所有会话"
placement="top"
>
<el-switch
v-model="isSyncAllSession"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="同步"
inactive-text="同步"
/>
</el-tooltip>
</div>
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="SFTP文件传输"
placement="top"
>
<el-switch
v-model="showSftp"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="SFTP"
inactive-text="SFTP"
/>
</el-tooltip>
</div>
<!-- <el-icon class="full_icon">
<FullScreen class="icon" @click="handleFullScreen" />
</el-icon> -->
</div>
</div>
<div class="info_box">
<InfoSide
ref="infoSideRef"
v-model:show-input-command="showInputCommand"
:host-info="curHost"
:visible="visible"
@click-input-command="clickInputCommand"
/>
</div>
<div class="terminals_sftp_wrap">
<el-tabs
v-model="activeTabIndex"
type="border-card"
tab-position="top"
@tab-remove="removeTab"
@tab-change="tabChange"
>
<el-tab-pane
v-for="(item, index) in terminalTabs"
:key="item.key"
:label="item.name"
:name="index"
:closable="true"
class="el_tab_pane"
>
<template #label>
<div class="tab_label">
<span class="tab_status" :style="{ background: getStatusColor(item.status) }" />
<span>{{ item.name }}</span>
</div>
</template>
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
<TerminalTab
ref="terminalRefs"
:host-obj="item"
:theme="themeObj"
:background="terminalBackground"
:font-size="terminalFontSize"
@input-command="terminalInput"
@cd-command="cdCommand"
/>
<Sftp
v-if="showSftp"
ref="sftpRefs"
:host="item.host"
@resize="resizeTerminal"
/>
</div>
</el-tab-pane>
</el-tabs>
</div>
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
<HostForm
v-model:show="hostFormVisible"
:default-data="updateHostData"
@update-list="handleUpdateList"
@closed="updateHostData = null"
/>
<TerminalSetting
v-model:show="showSetting"
v-model:themeName="themeName"
v-model:background="terminalBackground"
v-model:font-size="terminalFontSize"
@closed="showSetting = false"
/>
</div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
import TerminalTab from './terminal-tab.vue'
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 themeList from 'xterm-theme'
import { terminalStatusList } from '@/utils/enum'
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
const props = defineProps({
terminalTabs: {
type: Array,
required: true
}
})
const emit = defineEmits(['closed', 'removeTab', 'add-host',])
const showInputCommand = ref(false)
const infoSideRef = ref(null)
const terminalRefs = ref([])
const sftpRefs = ref([])
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 || '/terminal/01.png')
let localTerminalFontSize = localStorage.getItem('terminalFontSize')
const terminalFontSize = ref(Number(localTerminalFontSize) || 18)
const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
const hostList = computed(() => $store.hostList)
const curHost = computed(() => hostList.value.find(item => item.host === terminalTabs.value[activeTabIndex.value]?.host))
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)
})
watch(terminalFontSize, (newVal) => {
console.log('update terminalFontSize:', newVal)
localStorage.setItem('terminalFontSize', newVal)
})
onMounted(() => {
handleResizeTerminalSftp()
window.addEventListener('resize', handleResizeTerminalSftp)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResizeTerminalSftp)
})
const getStatusColor = (status) => {
return terminalStatusList.find(item => item.value === status)?.color || 'gray'
}
const handleUpdateList = async ({ host }) => {
try {
await $store.getHostList()
let targetHost = hostList.value.find(item => item.host === host)
if (targetHost) emit('add-host', targetHost)
} catch (err) {
$message.error('获取实例列表失败')
console.error('获取实例列表失败: ', err)
}
}
const handleResizeTerminalSftp = () => {
$nextTick(() => {
mainHeight.value = document.querySelector('.terminals_sftp_wrap')?.offsetHeight - 45 // 45 is tab-header height+15
})
}
const handleCommandHost = (host) => {
if (!host.isConfig) {
$message.warning('请先配置SSH连接信息')
hostFormVisible.value = true
updateHostData.value = { ...host }
return
}
emit('add-host', host)
}
const handleExecScript = (scriptObj) => {
let { command } = scriptObj
command += '\n'
if (!isSyncAllSession.value) return handleInputCommand(command)
terminalRefs.value.forEach(terminalRef => {
terminalRef.inputCommand(command)
})
}
const terminalInput = (command) => {
if (!isSyncAllSession.value) return
let filterTerminalRefs = terminalRefs.value.filter((host, index) => {
return index !== activeTabIndex.value
})
filterTerminalRefs.forEach(hostRef => {
hostRef.inputCommand(command)
})
}
const cdCommand = (path) => {
// console.log('cdCommand:', path)
if (!showSftp.value) return
if (isSyncAllSession.value) {
sftpRefs.value.forEach(sftpRef => {
sftpRef.openDir(path)
})
} else {
sftpRefs.value[activeTabIndex.value].openDir(path, false)
}
}
const tabChange = async (index) => {
await $nextTick()
const curTerminalRef = terminalRefs.value[index]
curTerminalRef?.focusTab()
}
watch(terminalTabsLen, () => {
let len = terminalTabsLen.value
// console.log('add tab:', len)
if (len > 0) {
activeTabIndex.value = len - 1
// registryDbClick()
tabChange(activeTabIndex.value)
}
}, {
immediate: true,
deep: false
})
watch(showSftp, () => {
localStorage.setItem('showSftp', showSftp.value)
nextTick(() => {
resizeTerminal()
})
})
// const windowBeforeUnload = () => {
// window.onbeforeunload = () => {
// return ''
// }
// }
const clickInputCommand = () => {
showInputCommand.value = true
}
const removeTab = (index) => {
emit('removeTab', index)
if (index === activeTabIndex.value) {
nextTick(() => {
activeTabIndex.value = 0
})
}
}
const handleFullScreen = () => {
document.getElementsByClassName('terminals_sftp_wrap')[0].requestFullscreen()
}
// const registryDbClick = () => {
// $nextTick(() => {
// let tabItems = Array.from(document.getElementsByClassName('el-tabs__item'))
// tabItems.forEach(item => {
// item.removeEventListener('dblclick', handleDblclick)
// item.addEventListener('dblclick', handleDblclick)
// })
// })
// }
// const handleDblclick = (e) => {
// let key = e.target.id.substring(4)
// removeTab(key)
// }
const resizeTerminal = () => {
for (let terminalTabRef of terminalRefs.value) {
const { handleResize } = terminalTabRef || {}
handleResize && handleResize()
}
}
const handleInputCommand = async (command) => {
const curTerminalRef = terminalRefs.value[activeTabIndex.value]
await $nextTick()
curTerminalRef?.focusTab()
curTerminalRef.inputCommand(`${ command }`) // \n
showInputCommand.value = false
}
</script>
<style lang="scss" scoped>
.terminal_wrap {
display: flex;
flex-wrap: wrap;
height: 100%;
:deep(.el-tabs__content) {
// width: 100%;
padding: 0 0 5px 0;
}
:deep(.el-tabs--border-card) {
border: none;
}
:deep(.el-tabs__nav-wrap.is-scrollable.is-top) {
display: flex;
align-items: center;
}
$terminalTopHeight: 30px;
.terminal_top {
width: 100%;
height: $terminalTopHeight;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
position: sticky;
top: 0;
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
z-index: 3;
user-select: none;
// :deep(.el-dropdown) {
// margin-top: -2px;
// }
.scripts_menu {
:deep(.el-dropdown-menu) {
min-width: 120px;
max-width: 300px;
}
}
.link_text {
font-size: var(--el-font-size-base);
color: var(--el-text-color-regular);
// color: var(--el-color-primary);
cursor: pointer;
margin-right: 15px;
.hidden_icon {
opacity: 0;
}
}
.left_menu {
display: flex;
align-items: center;
}
.right_overview {
display: flex;
align-items: center;
.switch_wrap {
display: flex;
align-items: center;
margin-right: 5px;
}
.full_icon {
cursor: pointer;
&:hover .icon {
color: var(--el-color-primary);
}
}
}
}
.info_box {
height: calc(100% - $terminalTopHeight);
overflow: auto;
display: flex;
flex-direction: column;
border: var(--el-descriptions-table-border);
}
.terminals_sftp_wrap {
height: calc(100% - $terminalTopHeight);
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
position: relative;
.tab_label {
display: flex;
align-items: center;
justify-content: center;
.tab_status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
transition: all 0.5s;
// background-color: var(--el-color-primary);
}
}
.tab_content_wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
:deep(.terminal_tab_container) {
flex: 1;
}
:deep(.sftp_tab_container) {
height: 300px;
}
}
.full-screen-button {
position: absolute;
right: 10px;
top: 4px;
z-index: 99999;
}
}
.visible {
position: absolute;
z-index: 999999;
top: 13px;
left: 5px;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
}
</style>
<style>
.action_icon {
color: var(--el-color-primary);
}
</style>