mirror of
https://gitee.com/goploy/goploy.git
synced 2024-12-04 04:59:58 +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>
|
<template>
|
||||||
<el-row class="app-container">
|
<el-row class="app-container">
|
||||||
<el-row class="app-bar" type="flex" justify="end">
|
<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-button type="primary" icon="el-icon-plus" @click="handleAdd" />
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-table
|
<el-table
|
||||||
@ -61,23 +64,11 @@
|
|||||||
<el-table-column
|
<el-table-column
|
||||||
prop="operation"
|
prop="operation"
|
||||||
:label="$t('op')"
|
:label="$t('op')"
|
||||||
width="130"
|
width="80"
|
||||||
align="center"
|
align="center"
|
||||||
:fixed="$store.state.app.device === 'mobile' ? false : 'right'"
|
:fixed="$store.state.app.device === 'mobile' ? false : 'right'"
|
||||||
>
|
>
|
||||||
<template #default="scope">
|
<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
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@ -191,7 +182,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<TheXtermDrawer v-model="dialogTermVisible" :server-row="selectedItem" />
|
<TheXtermDialog v-model="dialogTermVisible" />
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -206,7 +197,7 @@ import {
|
|||||||
ServerToggle,
|
ServerToggle,
|
||||||
ServerData,
|
ServerData,
|
||||||
} from '@/api/server'
|
} from '@/api/server'
|
||||||
import TheXtermDrawer from './TheXtermDrawer.vue'
|
import TheXtermDialog from './TheXtermDialog.vue'
|
||||||
import Validator from 'async-validator'
|
import Validator from 'async-validator'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { copy } from '@/utils'
|
import { copy } from '@/utils'
|
||||||
@ -214,7 +205,7 @@ import { ElMessageBox, ElMessage } from 'element-plus'
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ServerIndex',
|
name: 'ServerIndex',
|
||||||
components: { TheXtermDrawer },
|
components: { TheXtermDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialogTermVisible: false,
|
dialogTermVisible: false,
|
||||||
@ -363,11 +354,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleConnect(data: ServerData['datagram']) {
|
|
||||||
this.selectedItem = data
|
|
||||||
this.dialogTermVisible = true
|
|
||||||
},
|
|
||||||
|
|
||||||
check() {
|
check() {
|
||||||
;(this.$refs.form as Validator).validate((valid: boolean) => {
|
;(this.$refs.form as Validator).validate((valid: boolean) => {
|
||||||
if (valid) {
|
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