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