✨ 新增终端连接状态展示
This commit is contained in:
parent
e0eb1446db
commit
1e783c0d90
@ -158,6 +158,7 @@ module.exports = (httpServer) => {
|
|||||||
// 初始化新的SSH客户端对象
|
// 初始化新的SSH客户端对象
|
||||||
sshClient = new SSHClient()
|
sshClient = new SSHClient()
|
||||||
stream = await createTerminal(ip, socket, sshClient)
|
stream = await createTerminal(ip, socket, sshClient)
|
||||||
|
socket.emit('reconnect_terminal_success')
|
||||||
socket.on('input', listenerInput)
|
socket.on('input', listenerInput)
|
||||||
socket.on('resize', resizeShell)
|
socket.on('resize', resizeShell)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
15
web/src/utils/enum.js
Normal file
15
web/src/utils/enum.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 终端连接状态
|
||||||
|
export const terminalStatus = {
|
||||||
|
CONNECTING: 'connecting',
|
||||||
|
RECONNECTING: 'reconnecting',
|
||||||
|
CONNECT_FAIL: 'connect_fail',
|
||||||
|
CONNECT_SUCCESS: 'connect_success'
|
||||||
|
}
|
||||||
|
export const terminalStatusList = [
|
||||||
|
{ value: terminalStatus.CONNECTING, label: '连接中', color: '#FFA500' },
|
||||||
|
{ value: terminalStatus.RECONNECTING, label: '重连中', color: '#FFA500' },
|
||||||
|
{ value: terminalStatus.CONNECT_FAIL, label: '连接失败', color: '#DC3545' },
|
||||||
|
{ value: terminalStatus.CONNECT_SUCCESS, label: '已连接', color: '#28A745' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// other...
|
@ -21,14 +21,17 @@ import { SearchAddon } from '@xterm/addon-search'
|
|||||||
// import { SearchBarAddon } from 'xterm-addon-search-bar'
|
// import { SearchBarAddon } from 'xterm-addon-search-bar'
|
||||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
|
import { terminalStatus } from '@/utils/enum'
|
||||||
|
|
||||||
|
const { CONNECTING, RECONNECTING, CONNECT_SUCCESS, CONNECT_FAIL } = terminalStatus
|
||||||
|
|
||||||
const { io } = socketIo
|
const { io } = socketIo
|
||||||
const { proxy: { $api, $store, $serviceURI, $notification, $router, $message } } = getCurrentInstance()
|
const { proxy: { $api, $store, $serviceURI, $notification, $router, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: {
|
hostObj: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: Object
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
required: false,
|
required: false,
|
||||||
@ -54,11 +57,13 @@ const command = ref('')
|
|||||||
const timer = ref(null)
|
const timer = ref(null)
|
||||||
const fitAddon = ref(null)
|
const fitAddon = ref(null)
|
||||||
const searchBar = ref(null)
|
const searchBar = ref(null)
|
||||||
const isManual = ref(false)
|
const hasRegisterEvent = ref(false)
|
||||||
const isConnectSuccess = ref(false)
|
|
||||||
const isConnectFail = ref(false)
|
// const isConnectSuccess = ref(false)
|
||||||
const isConnecting = ref(true)
|
// const isConnectFail = ref(false)
|
||||||
const isReConnect = ref(false)
|
// const isConnecting = ref(true)
|
||||||
|
// const isReConnect = ref(false)
|
||||||
|
const curStatus = ref(CONNECTING)
|
||||||
const terminal = ref(null)
|
const terminal = ref(null)
|
||||||
const terminalRef = ref(null)
|
const terminalRef = ref(null)
|
||||||
|
|
||||||
@ -66,6 +71,8 @@ const token = computed(() => $store.token)
|
|||||||
const theme = computed(() => props.theme)
|
const theme = computed(() => props.theme)
|
||||||
const fontSize = computed(() => props.fontSize)
|
const fontSize = computed(() => props.fontSize)
|
||||||
const background = computed(() => props.background)
|
const background = computed(() => props.background)
|
||||||
|
const hostObj = computed(() => props.hostObj)
|
||||||
|
const host = computed(() => hostObj.value.host)
|
||||||
|
|
||||||
watch(theme, () => {
|
watch(theme, () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@ -96,28 +103,28 @@ watch(background, (newVal) => {
|
|||||||
})
|
})
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(curStatus, () => {
|
||||||
|
console.warn(`status: ${ curStatus.value }`)
|
||||||
|
hostObj.value.status = curStatus.value
|
||||||
|
})
|
||||||
|
|
||||||
const getCommand = async () => {
|
const getCommand = async () => {
|
||||||
let { data } = await $api.getCommand(props.host)
|
let { data } = await $api.getCommand(host.value)
|
||||||
if (data) command.value = data
|
if (data) command.value = data
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectIO = () => {
|
const connectIO = () => {
|
||||||
const { host } = props
|
|
||||||
socket.value = io($serviceURI, {
|
socket.value = io($serviceURI, {
|
||||||
path: '/terminal',
|
path: '/terminal',
|
||||||
forceNew: false,
|
forceNew: false,
|
||||||
reconnectionAttempts: 1
|
reconnectionAttempts: 1
|
||||||
})
|
})
|
||||||
socket.value.on('connect', () => {
|
socket.value.on('connect', () => {
|
||||||
console.log('/terminal socket已连接:', host)
|
console.log('/terminal socket已连接:', host.value)
|
||||||
socket.value.emit('create', { host, token: token.value })
|
socket.value.emit('create', { host: host.value, token: token.value })
|
||||||
socket.value.on('connect_terminal_success', () => {
|
socket.value.on('connect_terminal_success', () => {
|
||||||
isConnectFail.value = false
|
if (hasRegisterEvent.value) return // 以下事件连接成功后仅可注册一次, 否则会多次触发. 除非socket重连
|
||||||
isConnecting.value = false
|
hasRegisterEvent.value = true
|
||||||
if (isReConnect.value) {
|
|
||||||
isReConnect.value = false
|
|
||||||
return // 重连不需要再注册监听事件
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.value.on('output', (str) => {
|
socket.value.on('output', (str) => {
|
||||||
term.value.write(str)
|
term.value.write(str)
|
||||||
@ -125,7 +132,7 @@ const connectIO = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.value.on('connect_shell_success', () => {
|
socket.value.on('connect_shell_success', () => {
|
||||||
isConnectSuccess.value = true
|
curStatus.value = CONNECT_SUCCESS
|
||||||
onResize()
|
onResize()
|
||||||
onFindText()
|
onFindText()
|
||||||
onWebLinks()
|
onWebLinks()
|
||||||
@ -144,28 +151,26 @@ const connectIO = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.value.on('connect_close', () => {
|
socket.value.on('connect_close', () => {
|
||||||
if (isConnectFail.value) return
|
if (curStatus.value === CONNECT_FAIL) return // 连接失败不需要自动重连
|
||||||
isReConnect.value = true // 重连状态标记为true
|
curStatus.value = RECONNECTING
|
||||||
isConnecting.value = true
|
console.warn('连接断开,3秒后自动重连: ', host.value)
|
||||||
isConnectSuccess.value = false
|
|
||||||
console.warn('连接断开,3秒后重连: ', host)
|
|
||||||
term.value.write('\r\n连接断开,3秒后自动重连...\r\n')
|
term.value.write('\r\n连接断开,3秒后自动重连...\r\n')
|
||||||
socket.value.emit('reconnect_terminal')
|
socket.value.emit('reconnect_terminal')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.value.on('reconnect_terminal_success', () => {
|
||||||
|
curStatus.value = CONNECT_SUCCESS
|
||||||
|
})
|
||||||
|
|
||||||
socket.value.on('create_fail', (message) => {
|
socket.value.on('create_fail', (message) => {
|
||||||
isConnectFail.value = true
|
curStatus.value = CONNECT_FAIL
|
||||||
isConnecting.value = false
|
console.error('n创建失败:', host.value, message)
|
||||||
isConnectSuccess.value = false
|
|
||||||
console.error('n创建失败:', host, message)
|
|
||||||
term.value.write(`\r\n创建失败: ${ message }\r\n`)
|
term.value.write(`\r\n创建失败: ${ message }\r\n`)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.value.on('connect_fail', (message) => {
|
socket.value.on('connect_fail', (message) => {
|
||||||
isConnectFail.value = true
|
curStatus.value = CONNECT_FAIL
|
||||||
isConnecting.value = false
|
console.error('连接失败:', host.value, message)
|
||||||
isConnectSuccess.value = false
|
|
||||||
console.error('连接失败:', host, message)
|
|
||||||
term.value.write(`\r\n连接失败: ${ message }\r\n`)
|
term.value.write(`\r\n连接失败: ${ message }\r\n`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -174,10 +179,8 @@ const connectIO = () => {
|
|||||||
console.warn('terminal websocket 连接断开')
|
console.warn('terminal websocket 连接断开')
|
||||||
socket.value.removeAllListeners() // 取消所有监听
|
socket.value.removeAllListeners() // 取消所有监听
|
||||||
// socket.value.off('output') // 取消output监听,取消onData输入监听,重新注册
|
// socket.value.off('output') // 取消output监听,取消onData输入监听,重新注册
|
||||||
isConnectFail.value = true
|
curStatus.value = CONNECT_FAIL
|
||||||
isConnecting.value = true
|
term.value.write('\r\nError: 与面板socket连接断开。请关闭此tab,并检查本地与面板连接是否稳定\r\n')
|
||||||
isConnectSuccess.value = false
|
|
||||||
if (!isManual.value) $notification({ title: '与面板socket连接断开', message: `${ props.host }-请检查socket服务是否稳定`, type: 'error' })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.value.on('connect_error', (err) => {
|
socket.value.on('connect_error', (err) => {
|
||||||
@ -316,13 +319,13 @@ const onData = () => {
|
|||||||
enterTimer.value = setTimeout(() => {
|
enterTimer.value = setTimeout(() => {
|
||||||
if (enterTimer.value) clearTimeout(enterTimer.value)
|
if (enterTimer.value) clearTimeout(enterTimer.value)
|
||||||
if (key === '\r') { // Enter
|
if (key === '\r') { // Enter
|
||||||
if (isConnectFail.value && !isConnecting.value) { // 连接失败&&未正在连接,按回车可触发重连
|
if (curStatus.value === CONNECT_FAIL) { // 连接失败&&未正在连接,按回车可触发重连
|
||||||
isConnecting.value = true
|
curStatus.value = CONNECTING
|
||||||
term.value.write('\r\n连接中...\r\n')
|
term.value.write('\r\n连接中...\r\n')
|
||||||
socket.value.emit('reconnect_terminal')
|
socket.value.emit('reconnect_terminal')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isConnectSuccess.value) {
|
if (curStatus.value === CONNECT_SUCCESS) {
|
||||||
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
|
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
|
||||||
const lines = cleanText.split('\n')
|
const lines = cleanText.split('\n')
|
||||||
// console.log('lines: ', lines)
|
// console.log('lines: ', lines)
|
||||||
@ -345,7 +348,7 @@ const onData = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (isConnectFail.value || isConnecting.value) return console.warn(`isConnectFail: ${ isConnectFail.value }, isConnecting: ${ isConnecting.value }`)
|
if (curStatus.value !== CONNECT_SUCCESS) return
|
||||||
emit('inputCommand', key)
|
emit('inputCommand', key)
|
||||||
socket.value.emit('input', key)
|
socket.value.emit('input', key)
|
||||||
})
|
})
|
||||||
@ -393,11 +396,11 @@ onMounted(async () => {
|
|||||||
createLocalTerminal()
|
createLocalTerminal()
|
||||||
await getCommand()
|
await getCommand()
|
||||||
connectIO()
|
connectIO()
|
||||||
|
await nextTick()
|
||||||
onData()
|
onData()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
isManual.value = true
|
|
||||||
socket.value?.close()
|
socket.value?.close()
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
})
|
})
|
||||||
|
@ -119,10 +119,16 @@
|
|||||||
:closable="true"
|
:closable="true"
|
||||||
class="el_tab_pane"
|
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' }">
|
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
|
||||||
<TerminalTab
|
<TerminalTab
|
||||||
ref="terminalRefs"
|
ref="terminalRefs"
|
||||||
:host="item.host"
|
:host-obj="item"
|
||||||
:theme="themeObj"
|
:theme="themeObj"
|
||||||
:background="terminalBackground"
|
:background="terminalBackground"
|
||||||
:font-size="terminalFontSize"
|
:font-size="terminalFontSize"
|
||||||
@ -165,8 +171,8 @@ import Sftp from './sftp.vue'
|
|||||||
import InputCommand from '@/components/input-command/index.vue'
|
import InputCommand from '@/components/input-command/index.vue'
|
||||||
import HostForm from '../../server/components/host-form.vue'
|
import HostForm from '../../server/components/host-form.vue'
|
||||||
import TerminalSetting from './terminal-setting.vue'
|
import TerminalSetting from './terminal-setting.vue'
|
||||||
// import { randomStr } from '@utils/index.js'
|
|
||||||
import themeList from 'xterm-theme'
|
import themeList from 'xterm-theme'
|
||||||
|
import { terminalStatusList } from '@/utils/enum'
|
||||||
|
|
||||||
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
|
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
@ -226,6 +232,10 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('resize', handleResizeTerminalSftp)
|
window.removeEventListener('resize', handleResizeTerminalSftp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
return terminalStatusList.find(item => item.value === status)?.color || 'gray'
|
||||||
|
}
|
||||||
|
|
||||||
const handleUpdateList = async ({ host }) => {
|
const handleUpdateList = async ({ host }) => {
|
||||||
try {
|
try {
|
||||||
await $store.getHostList()
|
await $store.getHostList()
|
||||||
@ -461,6 +471,19 @@ const handleInputCommand = async (command) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
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;
|
||||||
|
// background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
.tab_content_wrap {
|
.tab_content_wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -57,6 +57,8 @@ import { useRoute } from 'vue-router'
|
|||||||
import Terminal from './components/terminal.vue'
|
import Terminal from './components/terminal.vue'
|
||||||
import HostForm from '../server/components/host-form.vue'
|
import HostForm from '../server/components/host-form.vue'
|
||||||
import { randomStr } from '@utils/index.js'
|
import { randomStr } from '@utils/index.js'
|
||||||
|
import { terminalStatus } from '@/utils/enum'
|
||||||
|
const { CONNECTING } = terminalStatus
|
||||||
|
|
||||||
const { proxy: { $store, $message } } = getCurrentInstance()
|
const { proxy: { $store, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
@ -74,7 +76,7 @@ let isAllConfssh = computed(() => {
|
|||||||
|
|
||||||
function linkTerminal(row) {
|
function linkTerminal(row) {
|
||||||
const { name, host } = row
|
const { name, host } = row
|
||||||
terminalTabs.push({ key: randomStr(16), name, host })
|
terminalTabs.push({ key: randomStr(16), name, host, status: CONNECTING })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateHost(row) {
|
function handleUpdateHost(row) {
|
||||||
@ -103,7 +105,7 @@ onActivated(async () => {
|
|||||||
if (!host) return
|
if (!host) return
|
||||||
let targetHosts = hostList.value.filter(item => host.includes(item.host)).map(item => {
|
let targetHosts = hostList.value.filter(item => host.includes(item.host)).map(item => {
|
||||||
const { name, host } = item
|
const { name, host } = item
|
||||||
return { key: randomStr(16), name, host }
|
return { key: randomStr(16), name, host, status: CONNECTING }
|
||||||
})
|
})
|
||||||
if (!targetHosts || !targetHosts.length) return
|
if (!targetHosts || !targetHosts.length) return
|
||||||
terminalTabs.push(...targetHosts)
|
terminalTabs.push(...targetHosts)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user