2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-06 12:27:13 +08:00
Files
panel/web/src/views/ssh/IndexView.vue
2025-04-13 01:45:40 +08:00

297 lines
7.7 KiB
Vue

<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-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'
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'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
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($gettext('Please create a host first'))
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 $gettext('Edit')
}
}
),
h(
NPopconfirm,
{
onPositiveClick: () => handleDelete(item.id)
},
{
default: () => {
return $gettext('Are you sure you want to delete this host?')
},
trigger: () => {
return h(
NButton,
{
size: 'small',
type: 'error'
},
{
default: () => {
return $gettext('Delete')
}
}
)
}
}
)
]
}
)
}
})
})
await openSession(updateId.value === 0 ? Number(list.value[0].key) : updateId.value)
}
const handleDelete = (id: number) => {
useRequest(ssh.delete(id)).onSuccess(() => {
list.value = list.value.filter((item: any) => item.key !== id)
if (current.value === id) {
if (list.value.length > 0) {
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 Variable', monospace`,
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!)
onResize()
term.value.focus()
window.addEventListener('resize', onResize, false)
current.value = id
ws.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.'))
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()
}
}
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)
)
window.$bus.on('ssh:refresh', fetchData)
})
onUnmounted(() => {
closeSession()
window.$bus.off('ssh:refresh')
})
</script>
<template>
<common-page show-footer>
<template #action>
<n-button type="primary" @click="create = true">
<TheIcon :size="18" icon="material-symbols:add" />
{{ $gettext('Create Host') }}
</n-button>
</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>