mirror of
https://gitee.com/goploy/goploy.git
synced 2024-12-03 12:39:44 +08:00
A terminal
This commit is contained in:
parent
a0c6b06e01
commit
8b5b1e66ea
1
web/src/icons/svg/terminal.svg
Normal file
1
web/src/icons/svg/terminal.svg
Normal 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 |
158
web/src/views/server/manage/TheXtermDialog.vue
Normal file
158
web/src/views/server/manage/TheXtermDialog.vue
Normal 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>
|
@ -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>
|
@ -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) {
|
||||
|
39
web/src/views/server/manage/xterm.ts
Normal file
39
web/src/views/server/manage/xterm.ts
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user