2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 18:27:13 +08:00
Files
panel/web/src/views/ssh/IndexView.vue
2024-10-23 04:30:14 +08:00

307 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
defineOptions({
name: 'ssh-index'
})
import ssh from '@/api/panel/ssh'
import ws from '@/api/ws'
import TheIcon from '@/components/custom/TheIcon.vue'
import CreateModal from '@/views/ssh/CreateModal.vue'
import UpdateModal from '@/views/ssh/UpdateModal.vue'
import '@fontsource/jetbrains-mono/400-italic.css'
import '@fontsource/jetbrains-mono/400.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'
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 { NButton, NFlex, NPopconfirm } from 'naive-ui'
const terminal = ref<HTMLElement | null>(null)
const term = ref()
let sshWs: WebSocket | null = null
const fitAddon = new FitAddon()
const webglAddon = new WebglAddon()
const current = ref(0)
const collapsed = ref(true)
const create = ref(false)
const update = ref(false)
const updateId = ref(0)
const list = ref<any[]>([])
const fetchData = async () => {
list.value = []
const { data } = await ssh.list(1, 10000)
if (data.items.length === 0) {
window.$message.info('请先创建主机')
return
}
data.items.forEach((item: any) => {
list.value.push({
label: item.name === '' ? item.host : item.name,
key: item.id,
extra: () => {
return h(
NFlex,
{
size: 'small',
style: 'float: right'
},
{
default: () => [
h(
NButton,
{
type: 'primary',
size: 'small',
onClick: () => {
update.value = true
updateId.value = item.id
}
},
{
default: () => {
return '编辑'
}
}
),
h(
NPopconfirm,
{
onPositiveClick: () => handleDelete(item.id)
},
{
default: () => {
return '确定删除主机吗?'
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error'
},
{
default: () => {
return '删除'
}
}
)
}
}
)
]
}
)
}
})
})
await openSession(updateId.value === 0 ? Number(list.value[0].key) : updateId.value)
}
const handleDelete = async (id: number) => {
await ssh.delete(id)
list.value = list.value.filter((item: any) => item.key !== id)
if (current.value === id) {
if (list.value.length > 0) {
await openSession(Number(list.value[0].key))
} else {
term.value.dispose()
}
if (list.value.length === 0) {
create.value = true
}
}
}
const handleChange = (key: number) => {
openSession(key)
}
const openSession = async (id: number) => {
closeSession()
await ws.ssh(id).then((ws) => {
sshWs = ws
term.value = new Terminal({
allowProposedApi: true,
lineHeight: 1.2,
fontSize: 14,
fontFamily: 'JetBrains Mono',
cursorBlink: true,
cursorStyle: 'underline',
tabStopWidth: 4,
theme: { background: '#111', foreground: '#fff' }
})
term.value.loadAddon(new AttachAddon(ws))
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(terminal.value!)
fitAddon.fit()
term.value.focus()
window.addEventListener('resize', onResize, false)
current.value = id
ws.onclose = () => {
term.value.write('\r\n连接已关闭请刷新重试。')
term.value.write('\r\nConnection closed. Please refresh.')
window.removeEventListener('resize', onResize)
}
ws.onerror = (event) => {
term.value.write('\r\n连接发生错误请刷新重试。')
term.value.write('\r\nConnection error. Please refresh .')
console.error(event)
ws.close()
}
})
}
const closeSession = () => {
try {
term.value.dispose()
sshWs?.close()
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
})
)
}
}
const onTermWheel = (event: WheelEvent) => {
if (event.ctrlKey) {
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()
}
}
watch(create, () => {
if (!create.value) fetchData()
})
watch(update, () => {
if (!update.value) {
fetchData()
updateId.value = 0
}
})
onMounted(() => {
// https://github.com/xtermjs/xterm.js/pull/5178
document.fonts.ready.then((fontFaceSet: any) =>
Promise.all(Array.from(fontFaceSet).map((el: any) => el.load())).then(fetchData)
)
})
onUnmounted(() => {
closeSession()
})
</script>
<template>
<common-page show-footer>
<template #action>
<div flex items-center>
<n-button type="primary" @click="create = true">
<TheIcon :size="18" icon="material-symbols:add" />
创建主机
</n-button>
</div>
</template>
<n-layout has-sider sider-placement="right">
<n-layout content-style="overflow: visible" bg-hex-111>
<div ref="terminal" @wheel="onTermWheel" h-75vh></div>
</n-layout>
<n-layout-sider
bordered
:collapsed-width="0"
:collapsed="collapsed"
show-trigger
:native-scrollbar="false"
@collapse="collapsed = true"
@expand="collapsed = false"
@after-enter="onResize"
@after-leave="onResize"
>
<n-menu
v-model:value="current"
:collapsed="collapsed"
:collapsed-width="0"
:collapsed-icon-size="0"
:options="list"
@update-value="handleChange"
/>
</n-layout-sider>
</n-layout>
</common-page>
<create-modal v-model:show="create" />
<update-modal v-model:show="update" v-model:id="updateId" />
</template>
<style scoped lang="scss">
:deep(.xterm) {
padding: 4rem !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>