mirror of
https://github.com/acepanel/panel.git
synced 2026-02-07 18:13:13 +08:00
feat: 添加容器编排实时进度显示功能 (#1217)
* Initial plan * feat: 添加容器编排实时进度显示功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 修复代码审查问题,使用动态路径和安全的字体大小操作 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * docs: 完成容器编排实时进度显示功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 容器优化 * fix: 用户关闭命令窗口时杀死正在运行的进程 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * refactor: 改进 WebSocket 断开检测,支持用户输入转发到 PTY Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 启用终端用户输入支持,转发键盘输入到 PTY Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 容器优化 * fix: lint * feat: 添加 PTY 窗口大小调整支持 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 支持读取输入 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -13,6 +13,17 @@ export default {
|
||||
ws.onerror = (e) => reject(e)
|
||||
})
|
||||
},
|
||||
// PTY 命令执行
|
||||
pty: (command: string): Promise<WebSocket> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(`${base}/pty`)
|
||||
ws.onopen = () => {
|
||||
ws.send(command)
|
||||
resolve(ws)
|
||||
}
|
||||
ws.onerror = (e) => reject(e)
|
||||
})
|
||||
},
|
||||
// 连接SSH
|
||||
ssh: (id: number): Promise<WebSocket> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
303
web/src/components/common/PtyTerminalModal.vue
Normal file
303
web/src/components/common/PtyTerminalModal.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<script setup lang="ts">
|
||||
import '@fontsource-variable/jetbrains-mono/wght-italic.css'
|
||||
import '@fontsource-variable/jetbrains-mono/wght.css'
|
||||
import { ClipboardAddon } from '@xterm/addon-clipboard'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import ws from '@/api/ws'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
command: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'complete'): void
|
||||
(e: 'error', error: string): void
|
||||
}>()
|
||||
|
||||
const isRunning = ref(false)
|
||||
const terminalRef = ref<HTMLElement | null>(null)
|
||||
const term = ref<Terminal | null>(null)
|
||||
let ptyWs: WebSocket | null = null
|
||||
let fitAddon: FitAddon | null = null
|
||||
let webglAddon: WebglAddon | null = null
|
||||
|
||||
// 初始化终端
|
||||
const initTerminal = async () => {
|
||||
if (!terminalRef.value || !props.command) {
|
||||
return
|
||||
}
|
||||
|
||||
isRunning.value = true
|
||||
|
||||
try {
|
||||
ptyWs = await ws.pty(props.command)
|
||||
ptyWs.binaryType = 'arraybuffer'
|
||||
|
||||
term.value = new Terminal({
|
||||
allowProposedApi: true,
|
||||
lineHeight: 1.2,
|
||||
fontSize: 14,
|
||||
fontFamily: `'JetBrains Mono Variable', monospace`,
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'underline',
|
||||
tabStopWidth: 4,
|
||||
disableStdin: false,
|
||||
convertEol: true,
|
||||
theme: { background: '#111', foreground: '#fff' }
|
||||
})
|
||||
|
||||
fitAddon = new FitAddon()
|
||||
webglAddon = new WebglAddon()
|
||||
|
||||
term.value.loadAddon(fitAddon)
|
||||
term.value.loadAddon(new ClipboardAddon())
|
||||
term.value.loadAddon(new WebLinksAddon())
|
||||
term.value.loadAddon(new Unicode11Addon())
|
||||
term.value.unicode.activeVersion = '11'
|
||||
term.value.loadAddon(webglAddon)
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon?.dispose()
|
||||
})
|
||||
term.value.open(terminalRef.value)
|
||||
|
||||
ptyWs.onmessage = (ev) => {
|
||||
const data: ArrayBuffer | string = ev.data
|
||||
term.value?.write(typeof data === 'string' ? data : new Uint8Array(data))
|
||||
}
|
||||
|
||||
term.value?.onData((data) => {
|
||||
if (ptyWs?.readyState === WebSocket.OPEN) {
|
||||
ptyWs?.send(data)
|
||||
}
|
||||
})
|
||||
term.value?.onBinary((data) => {
|
||||
if (ptyWs?.readyState === WebSocket.OPEN) {
|
||||
const buffer = new Uint8Array(data.length)
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
buffer[i] = data.charCodeAt(i) & 255
|
||||
}
|
||||
ptyWs?.send(buffer)
|
||||
}
|
||||
})
|
||||
term.value.onResize(({ rows, cols }) => {
|
||||
if (ptyWs && ptyWs.readyState === WebSocket.OPEN) {
|
||||
ptyWs.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
fitAddon.fit()
|
||||
term.value.focus()
|
||||
window.addEventListener('resize', onTerminalResize, false)
|
||||
|
||||
ptyWs.onclose = () => {
|
||||
isRunning.value = false
|
||||
if (term.value) {
|
||||
term.value.write('\r\n' + $gettext('Connection closed.'))
|
||||
}
|
||||
window.removeEventListener('resize', onTerminalResize)
|
||||
emit('complete')
|
||||
}
|
||||
|
||||
ptyWs.onerror = (event) => {
|
||||
isRunning.value = false
|
||||
if (term.value) {
|
||||
term.value.write('\r\n' + $gettext('Connection error.'))
|
||||
}
|
||||
console.error(event)
|
||||
ptyWs?.close()
|
||||
emit('error', $gettext('Connection error'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to start PTY:', error)
|
||||
isRunning.value = false
|
||||
emit('error', $gettext('Failed to connect'))
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭终端
|
||||
const closeTerminal = () => {
|
||||
try {
|
||||
if (ptyWs) {
|
||||
ptyWs.close()
|
||||
ptyWs = null
|
||||
}
|
||||
if (term.value) {
|
||||
term.value.dispose()
|
||||
term.value = null
|
||||
}
|
||||
fitAddon = null
|
||||
webglAddon = null
|
||||
if (terminalRef.value) {
|
||||
terminalRef.value.innerHTML = ''
|
||||
}
|
||||
window.removeEventListener('resize', onTerminalResize)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const onTerminalResize = () => {
|
||||
if (fitAddon && term.value) {
|
||||
fitAddon.fit()
|
||||
}
|
||||
}
|
||||
|
||||
// 终端滚轮缩放
|
||||
const onTerminalWheel = (event: WheelEvent) => {
|
||||
if (event.ctrlKey && term.value && fitAddon) {
|
||||
event.preventDefault()
|
||||
if (event.deltaY > 0) {
|
||||
if (term.value.options.fontSize! > 12) {
|
||||
term.value.options.fontSize = term.value.options.fontSize! - 1
|
||||
}
|
||||
} else {
|
||||
term.value.options.fontSize = term.value.options.fontSize! + 1
|
||||
}
|
||||
fitAddon.fit()
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框关闭后清理
|
||||
const handleModalClose = () => {
|
||||
closeTerminal()
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
// 处理关闭前确认
|
||||
const handleBeforeClose = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
if (isRunning.value) {
|
||||
window.$dialog.warning({
|
||||
title: $gettext('Confirm'),
|
||||
content: $gettext(
|
||||
'Command is still running. Closing the window will terminate the command. Are you sure?'
|
||||
),
|
||||
positiveText: $gettext('Confirm'),
|
||||
negativeText: $gettext('Cancel'),
|
||||
onPositiveClick: () => {
|
||||
resolve(true)
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
resolve(false)
|
||||
},
|
||||
onClose: () => {
|
||||
resolve(false)
|
||||
},
|
||||
onMaskClick: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理遮罩点击
|
||||
const handleMaskClick = async () => {
|
||||
if (await handleBeforeClose()) {
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 show 变化,自动初始化终端
|
||||
watch(
|
||||
() => show.value,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await nextTick()
|
||||
await initTerminal()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
closeTerminal()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
initTerminal,
|
||||
closeTerminal
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="title || $gettext('Terminal')"
|
||||
style="width: 90vw; height: 80vh"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
:mask-closable="false"
|
||||
:closable="true"
|
||||
:on-close="handleBeforeClose"
|
||||
@mask-click="handleMaskClick"
|
||||
@after-leave="handleModalClose"
|
||||
>
|
||||
<div
|
||||
ref="terminalRef"
|
||||
@wheel="onTerminalWheel"
|
||||
style="height: 100%; min-height: 60vh; background: #111"
|
||||
></div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.xterm) {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport::-webkit-scrollbar) {
|
||||
border-radius: 0.4rem;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport::-webkit-scrollbar-thumb) {
|
||||
background-color: #666;
|
||||
border-radius: 0.4rem;
|
||||
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport:hover::-webkit-scrollbar-thumb) {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport::-webkit-scrollbar-track) {
|
||||
background-color: #111;
|
||||
border-radius: 0.4rem;
|
||||
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport:hover::-webkit-scrollbar-track) {
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { NButton, NCheckbox, NDataTable, NFlex, NInput, NPopconfirm, NTag } from
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import container from '@/api/panel/container'
|
||||
import PtyTerminalModal from '@/components/common/PtyTerminalModal.vue'
|
||||
import { useFileStore } from '@/store'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
@@ -28,6 +29,28 @@ const updateModel = ref({
|
||||
})
|
||||
const updateModal = ref(false)
|
||||
|
||||
// Compose 启动状态
|
||||
const upModal = ref(false)
|
||||
const upComposeName = ref('')
|
||||
const upCommand = ref('')
|
||||
|
||||
// 处理 Compose 启动
|
||||
const handleComposeUp = (row: any, force: boolean) => {
|
||||
upComposeName.value = row.name
|
||||
let cmd = `docker compose -f ${row.path}/docker-compose.yml up -d`
|
||||
if (force) {
|
||||
cmd += ' --pull always'
|
||||
}
|
||||
upCommand.value = cmd
|
||||
upModal.value = true
|
||||
}
|
||||
|
||||
// Compose 启动完成
|
||||
const handleUpComplete = () => {
|
||||
refresh()
|
||||
forcePull.value = false
|
||||
}
|
||||
|
||||
const columns: any = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
@@ -104,18 +127,7 @@ const columns: any = [
|
||||
{
|
||||
showIcon: false,
|
||||
onPositiveClick: () => {
|
||||
const messageReactive = window.$message.loading($gettext('Starting...'), {
|
||||
duration: 0
|
||||
})
|
||||
useRequest(container.composeUp(row.name, forcePull.value))
|
||||
.onSuccess(() => {
|
||||
refresh()
|
||||
forcePull.value = false
|
||||
window.$message.success($gettext('Start successful'))
|
||||
})
|
||||
.onComplete(() => {
|
||||
messageReactive?.destroy()
|
||||
})
|
||||
handleComposeUp(row, forcePull.value)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -391,4 +403,10 @@ onMounted(() => {
|
||||
{{ $gettext('Submit') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
<pty-terminal-modal
|
||||
v-model:show="upModal"
|
||||
:title="$gettext('Starting Compose') + ' - ' + upComposeName"
|
||||
:command="upCommand"
|
||||
@complete="handleUpComplete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@fontsource-variable/jetbrains-mono/wght-italic.css'
|
||||
import '@fontsource-variable/jetbrains-mono/wght.css'
|
||||
import { AttachAddon } from '@xterm/addon-attach'
|
||||
import { ClipboardAddon } from '@xterm/addon-clipboard'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
@@ -32,8 +31,8 @@ const terminalContainerName = ref('')
|
||||
const terminalRef = ref<HTMLElement | null>(null)
|
||||
const term = ref<Terminal | null>(null)
|
||||
let containerWs: WebSocket | null = null
|
||||
const fitAddon = new FitAddon()
|
||||
const webglAddon = new WebglAddon()
|
||||
let fitAddon: FitAddon | null = null
|
||||
let webglAddon: WebglAddon | null = null
|
||||
|
||||
const containerCreateModal = ref(false)
|
||||
const selectedRowKeys = ref<any>([])
|
||||
@@ -405,6 +404,7 @@ const handleOpenTerminal = async (row: any) => {
|
||||
|
||||
try {
|
||||
containerWs = await ws.container(row.id)
|
||||
containerWs.binaryType = 'arraybuffer'
|
||||
|
||||
term.value = new Terminal({
|
||||
allowProposedApi: true,
|
||||
@@ -417,7 +417,9 @@ const handleOpenTerminal = async (row: any) => {
|
||||
theme: { background: '#111', foreground: '#fff' }
|
||||
})
|
||||
|
||||
term.value.loadAddon(new AttachAddon(containerWs))
|
||||
fitAddon = new FitAddon()
|
||||
webglAddon = new WebglAddon()
|
||||
|
||||
term.value.loadAddon(fitAddon)
|
||||
term.value.loadAddon(new ClipboardAddon())
|
||||
term.value.loadAddon(new WebLinksAddon())
|
||||
@@ -425,11 +427,41 @@ const handleOpenTerminal = async (row: any) => {
|
||||
term.value.unicode.activeVersion = '11'
|
||||
term.value.loadAddon(webglAddon)
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon.dispose()
|
||||
webglAddon?.dispose()
|
||||
})
|
||||
term.value.open(terminalRef.value)
|
||||
|
||||
onTerminalResize()
|
||||
containerWs.onmessage = (ev) => {
|
||||
const data: ArrayBuffer | string = ev.data
|
||||
term.value?.write(typeof data === 'string' ? data : new Uint8Array(data))
|
||||
}
|
||||
term.value.onData((data) => {
|
||||
if (containerWs?.readyState === WebSocket.OPEN) {
|
||||
containerWs?.send(data)
|
||||
}
|
||||
})
|
||||
term.value.onBinary((data) => {
|
||||
if (containerWs?.readyState === WebSocket.OPEN) {
|
||||
const buffer = new Uint8Array(data.length)
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
buffer[i] = data.charCodeAt(i) & 255
|
||||
}
|
||||
containerWs?.send(buffer)
|
||||
}
|
||||
})
|
||||
term.value.onResize(({ rows, cols }) => {
|
||||
if (containerWs && containerWs.readyState === WebSocket.OPEN) {
|
||||
containerWs.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
fitAddon.fit()
|
||||
term.value.focus()
|
||||
window.addEventListener('resize', onTerminalResize, false)
|
||||
|
||||
@@ -457,14 +489,16 @@ const handleOpenTerminal = async (row: any) => {
|
||||
// 关闭容器终端
|
||||
const closeTerminal = () => {
|
||||
try {
|
||||
if (term.value) {
|
||||
term.value.dispose()
|
||||
term.value = null
|
||||
}
|
||||
if (containerWs) {
|
||||
containerWs.close()
|
||||
containerWs = null
|
||||
}
|
||||
if (term.value) {
|
||||
term.value.dispose()
|
||||
term.value = null
|
||||
}
|
||||
fitAddon = null
|
||||
webglAddon = null
|
||||
if (terminalRef.value) {
|
||||
terminalRef.value.innerHTML = ''
|
||||
}
|
||||
@@ -476,22 +510,14 @@ const closeTerminal = () => {
|
||||
|
||||
// 终端大小调整
|
||||
const onTerminalResize = () => {
|
||||
fitAddon.fit()
|
||||
if (containerWs != null && containerWs.readyState === 1 && term.value) {
|
||||
const { cols, rows } = term.value
|
||||
containerWs.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
if (fitAddon && term.value) {
|
||||
fitAddon.fit()
|
||||
}
|
||||
}
|
||||
|
||||
// 终端滚轮缩放
|
||||
const onTerminalWheel = (event: WheelEvent) => {
|
||||
if (event.ctrlKey && term.value) {
|
||||
if (event.ctrlKey && term.value && fitAddon) {
|
||||
event.preventDefault()
|
||||
if (event.deltaY > 0) {
|
||||
if (term.value.options.fontSize! > 12) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import CreateModal from '@/views/ssh/CreateModal.vue'
|
||||
import UpdateModal from '@/views/ssh/UpdateModal.vue'
|
||||
import '@fontsource-variable/jetbrains-mono/wght-italic.css'
|
||||
import '@fontsource-variable/jetbrains-mono/wght.css'
|
||||
import { AttachAddon } from '@xterm/addon-attach'
|
||||
import { ClipboardAddon } from '@xterm/addon-clipboard'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
@@ -22,10 +21,10 @@ import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const terminal = ref<HTMLElement | null>(null)
|
||||
const term = ref()
|
||||
const term = ref<Terminal | null>(null)
|
||||
let sshWs: WebSocket | null = null
|
||||
const fitAddon = new FitAddon()
|
||||
const webglAddon = new WebglAddon()
|
||||
let fitAddon: FitAddon | null = null
|
||||
let webglAddon: WebglAddon | null = null
|
||||
|
||||
const current = ref(0)
|
||||
const collapsed = ref(true)
|
||||
@@ -112,7 +111,7 @@ const handleDelete = (id: number) => {
|
||||
if (list.value.length > 0) {
|
||||
openSession(Number(list.value[0].key))
|
||||
} else {
|
||||
term.value.dispose()
|
||||
term.value?.dispose()
|
||||
}
|
||||
if (list.value.length === 0) {
|
||||
create.value = true
|
||||
@@ -127,8 +126,10 @@ const handleChange = (key: number) => {
|
||||
|
||||
const openSession = async (id: number) => {
|
||||
closeSession()
|
||||
await ws.ssh(id).then((ws) => {
|
||||
sshWs = ws
|
||||
await ws.ssh(id).then((socket) => {
|
||||
sshWs = socket
|
||||
sshWs.binaryType = 'arraybuffer'
|
||||
|
||||
term.value = new Terminal({
|
||||
allowProposedApi: true,
|
||||
lineHeight: 1.2,
|
||||
@@ -140,7 +141,9 @@ const openSession = async (id: number) => {
|
||||
theme: { background: '#111', foreground: '#fff' }
|
||||
})
|
||||
|
||||
term.value.loadAddon(new AttachAddon(ws))
|
||||
fitAddon = new FitAddon()
|
||||
webglAddon = new WebglAddon()
|
||||
|
||||
term.value.loadAddon(fitAddon)
|
||||
term.value.loadAddon(new ClipboardAddon())
|
||||
term.value.loadAddon(new WebLinksAddon())
|
||||
@@ -148,61 +151,94 @@ const openSession = async (id: number) => {
|
||||
term.value.unicode.activeVersion = '11'
|
||||
term.value.loadAddon(webglAddon)
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon.dispose()
|
||||
webglAddon?.dispose()
|
||||
})
|
||||
term.value.open(terminal.value!)
|
||||
|
||||
onResize()
|
||||
sshWs.onmessage = (ev) => {
|
||||
const data: ArrayBuffer | string = ev.data
|
||||
term.value?.write(typeof data === 'string' ? data : new Uint8Array(data))
|
||||
}
|
||||
term.value?.onData((data) => {
|
||||
if (sshWs?.readyState === WebSocket.OPEN) {
|
||||
sshWs?.send(data)
|
||||
}
|
||||
})
|
||||
term.value?.onBinary((data) => {
|
||||
if (sshWs?.readyState === WebSocket.OPEN) {
|
||||
const buffer = new Uint8Array(data.length)
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
buffer[i] = data.charCodeAt(i) & 255
|
||||
}
|
||||
sshWs?.send(buffer)
|
||||
}
|
||||
})
|
||||
term.value.onResize(({ rows, cols }) => {
|
||||
if (sshWs?.readyState === WebSocket.OPEN) {
|
||||
sshWs?.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
fitAddon.fit()
|
||||
term.value.focus()
|
||||
window.addEventListener('resize', onResize, false)
|
||||
current.value = id
|
||||
|
||||
ws.onclose = () => {
|
||||
term.value.write('\r\n' + $gettext('Connection closed. Please refresh.'))
|
||||
sshWs.onclose = () => {
|
||||
term.value?.write('\r\n' + $gettext('Connection closed. Please refresh.'))
|
||||
window.removeEventListener('resize', onResize)
|
||||
}
|
||||
|
||||
ws.onerror = (event) => {
|
||||
term.value.write('\r\n' + $gettext('Connection error. Please refresh.'))
|
||||
sshWs.onerror = (event) => {
|
||||
term.value?.write('\r\n' + $gettext('Connection error. Please refresh.'))
|
||||
console.error(event)
|
||||
ws.close()
|
||||
sshWs?.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeSession = () => {
|
||||
try {
|
||||
term.value.dispose()
|
||||
sshWs?.close()
|
||||
terminal.value!.innerHTML = ''
|
||||
if (sshWs) {
|
||||
sshWs.close()
|
||||
sshWs = null
|
||||
}
|
||||
if (term.value) {
|
||||
term.value.dispose()
|
||||
term.value = null
|
||||
}
|
||||
fitAddon = null
|
||||
webglAddon = null
|
||||
if (terminal.value) {
|
||||
terminal.value.innerHTML = ''
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
const onResize = () => {
|
||||
fitAddon.fit()
|
||||
if (sshWs != null && sshWs.readyState === 1) {
|
||||
const { cols, rows } = term.value
|
||||
sshWs.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
if (fitAddon && term.value) {
|
||||
fitAddon.fit()
|
||||
}
|
||||
}
|
||||
|
||||
const onTermWheel = (event: WheelEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.ctrlKey && term.value && fitAddon) {
|
||||
event.preventDefault()
|
||||
const fontSize = term.value.options.fontSize ?? 14
|
||||
if (event.deltaY > 0) {
|
||||
if (term.value.options.fontSize > 12) {
|
||||
term.value.options.fontSize = term.value.options.fontSize - 1
|
||||
if (fontSize > 12) {
|
||||
term.value.options.fontSize = fontSize - 1
|
||||
}
|
||||
} else {
|
||||
term.value.options.fontSize = term.value.options.fontSize + 1
|
||||
term.value.options.fontSize = fontSize + 1
|
||||
}
|
||||
fitAddon.fit()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user