A terminal

This commit is contained in:
zhenorzz 2021-06-19 17:18:03 +08:00
parent a0c6b06e01
commit 8b5b1e66ea
5 changed files with 205 additions and 112 deletions

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1624087271487" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2113" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M992 832h-960C14.933333 832 0 817.066667 0 800v-704C0 78.933333 14.933333 64 32 64h960c17.066667 0 32 14.933333 32 32v704c0 17.066667-14.933333 32-32 32zM64 768h896V128H64v640zM864 960h-704c-17.066667 0-32-14.933333-32-32S142.933333 896 160 896h704c17.066667 0 32 14.933333 32 32s-14.933333 32-32 32z" fill="#212121" p-id="2114"></path><path d="M661.333333 452.266667c-17.066667-6.4-23.466667 0-14.933333 14.933333L746.666667 686.933333c6.4 17.066667 17.066667 14.933333 19.2-2.133333l19.2-91.733333 91.733333-19.2c17.066667-4.266667 19.2-12.8 2.133333-19.2L661.333333 452.266667z" fill="#212121" p-id="2115"></path></svg>

After

Width:  |  Height:  |  Size: 999 B

View File

@ -0,0 +1,158 @@
<template>
<el-dialog
v-model="dialogVisible"
title="WebSSH"
:close-on-click-modal="false"
:fullscreen="$store.state.app.device === 'mobile'"
>
<el-select
v-model="serverId"
v-loading="serverLoading"
filterable
default-first-option
placeholder="please select server"
style="width: 100%; margin-bottom: 10px"
@change="handleSelectServer"
>
<el-option
v-for="item in serverOption"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-tabs v-model="activeName" closable @tab-remove="removeTab">
<el-tab-pane
v-for="item in tabList"
:key="item.name"
:label="item.label"
:name="item.name"
>
<div
:ref="
(el) => {
item.el = el
}
"
class="xterm"
:style="
$store.state.app.device === 'mobile'
? 'height: calc(100vh - 212px)'
: 'height:500px'
"
/>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<script lang="ts">
import 'xterm/css/xterm.css'
import { xterm } from './xterm'
import {
computed,
watch,
onBeforeUpdate,
defineComponent,
ref,
nextTick,
} from 'vue'
import { ServerOption } from '@/api/server'
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
},
})
const serverLoading = ref(false)
const serverOption = ref<ServerOption['datagram']['list']>([])
watch(dialogVisible, (val: boolean) => {
if (val === true) {
serverLoading.value = true
new ServerOption()
.request()
.then((response) => {
serverOption.value = response.data.list
})
.finally(() => {
serverLoading.value = false
})
}
})
const activeName = ref('')
const tabList = ref<{ label: string; name: string; el: HTMLDivElement }[]>(
[]
)
const serverId = ref('')
const xterms = ref<xterm[]>([])
onBeforeUpdate(() => {
xterms.value = []
})
const handleSelectServer = () => {
const selectedServer = serverOption.value.find(
(_) => _.id === Number(serverId.value)
)
if (!selectedServer) {
return
}
const tabLen = tabList.value.length
const item = {
label: selectedServer.ip,
name: '' + tabLen,
el: {} as HTMLDivElement,
}
tabList.value.push(item)
serverId.value = ''
nextTick(() => {
xterms.value[tabLen] = new xterm(item.el, selectedServer.id)
xterms.value[tabLen].connect()
})
}
const removeTab = (targetName: string) => {
if (activeName.value === targetName) {
tabList.value.forEach((tab, index) => {
if (tab.name === targetName) {
let nextTab = tabList.value[index + 1] || tabList.value[index - 1]
if (nextTab) {
activeName.value = nextTab.name
}
}
})
}
tabList.value = tabList.value.filter((tab) => tab.name !== targetName)
xterms.value[Number(targetName)].close()
}
return {
xterms,
dialogVisible,
serverLoading,
serverOption,
serverId,
tabList,
activeName,
handleSelectServer,
removeTab,
}
},
})
</script>
<style lang="scss" scoped>
.xterm {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,91 +0,0 @@
<template>
<el-drawer
ref="drawer"
v-model="drawerVisible"
:title="`${serverRow.name}(${serverRow.description})`"
:direction="$store.state.app.device === 'mobile' ? 'ttb' : 'rtl'"
:size="$store.state.app.device === 'mobile' ? 469 : '30%'"
@opened="connectTerminal"
@closed="closeTerminal"
>
<div v-if="drawerVisible" ref="xterm" class="xterm" />
</el-drawer>
</template>
<script lang="ts">
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { AttachAddon } from 'xterm-addon-attach'
import { computed, defineComponent, ref } from 'vue'
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false,
},
serverRow: {
type: Object,
required: true,
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const drawerVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
},
})
const terminal = ref<Terminal | null>()
const websocket = ref<WebSocket | null>()
// html dom
const xterm = ref()
const connectTerminal = () => {
const isWindows =
['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0
const term = new Terminal({
fontSize: 14,
cursorBlink: true,
windowsMode: isWindows,
})
const fitAddon = new FitAddon()
term.loadAddon(fitAddon)
term.open(xterm.value)
fitAddon.fit()
term.focus()
websocket.value = new WebSocket(
`${location.protocol.replace('http', 'ws')}//${
window.location.host + import.meta.env.VITE_APP_BASE_API
}/ws/xterm?serverId=${props.serverRow.id}&rows=${term.rows}&cols=${
term.cols
}`
)
const attachAddon = new AttachAddon(websocket.value)
term.loadAddon(attachAddon)
terminal.value = term
websocket.value.onerror = () => {
websocket.value = null
}
}
const closeTerminal = () => {
terminal.value = null
websocket.value?.close()
}
return {
xterm,
drawerVisible,
connectTerminal,
closeTerminal,
}
},
})
</script>
<style lang="scss" scoped>
.xterm {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,6 +1,9 @@
<template>
<el-row class="app-container">
<el-row class="app-bar" type="flex" justify="end">
<el-button @click="dialogTermVisible = true">
<svg-icon icon-class="terminal" />
</el-button>
<el-button type="primary" icon="el-icon-plus" @click="handleAdd" />
</el-row>
<el-table
@ -61,23 +64,11 @@
<el-table-column
prop="operation"
:label="$t('op')"
width="130"
width="80"
align="center"
:fixed="$store.state.app.device === 'mobile' ? false : 'right'"
>
<template #default="scope">
<el-tooltip
class="item"
effect="dark"
content="Connect Terminal"
placement="bottom"
>
<el-button
type="success"
icon="el-icon-connection"
@click="handleConnect(scope.row)"
/>
</el-tooltip>
<el-button
type="primary"
icon="el-icon-edit"
@ -191,7 +182,7 @@
</el-row>
</template>
</el-dialog>
<TheXtermDrawer v-model="dialogTermVisible" :server-row="selectedItem" />
<TheXtermDialog v-model="dialogTermVisible" />
</el-row>
</template>
<script lang="ts">
@ -206,7 +197,7 @@ import {
ServerToggle,
ServerData,
} from '@/api/server'
import TheXtermDrawer from './TheXtermDrawer.vue'
import TheXtermDialog from './TheXtermDialog.vue'
import Validator from 'async-validator'
import { defineComponent } from 'vue'
import { copy } from '@/utils'
@ -214,7 +205,7 @@ import { ElMessageBox, ElMessage } from 'element-plus'
export default defineComponent({
name: 'ServerIndex',
components: { TheXtermDrawer },
components: { TheXtermDialog },
data() {
return {
dialogTermVisible: false,
@ -363,11 +354,6 @@ export default defineComponent({
}
},
handleConnect(data: ServerData['datagram']) {
this.selectedItem = data
this.dialogTermVisible = true
},
check() {
;(this.$refs.form as Validator).validate((valid: boolean) => {
if (valid) {

View File

@ -0,0 +1,39 @@
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { AttachAddon } from 'xterm-addon-attach'
export class xterm {
private serverId: number
private element: HTMLDivElement
private websocket!: WebSocket
private terminal: Terminal | null = null
constructor(element: HTMLDivElement, serverId: number) {
this.element = element
this.serverId = serverId
}
public connect(): void {
const isWindows =
['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0
this.terminal = new Terminal({
fontSize: 14,
cursorBlink: true,
windowsMode: isWindows,
})
const fitAddon = new FitAddon()
this.terminal.loadAddon(fitAddon)
this.terminal.open(this.element)
fitAddon.fit()
this.websocket = new WebSocket(
`${location.protocol.replace('http', 'ws')}//${
window.location.host + import.meta.env.VITE_APP_BASE_API
}/ws/xterm?serverId=${this.serverId}&rows=${this.terminal.rows}&cols=${
this.terminal.cols
}`
)
const attachAddon = new AttachAddon(this.websocket)
this.terminal.loadAddon(attachAddon)
}
public close(): void {
this.terminal = null
this.websocket.close()
}
}