大修迷你终端

This commit is contained in:
niyyzf 2024-07-23 23:03:35 +08:00
parent 3b9d43acb7
commit 009f46879c
7 changed files with 129 additions and 32 deletions

View File

@ -13,19 +13,25 @@ public class TerminalHub(SshStreamService sshStreamService):Hub
await Groups.AddToGroupAsync(Context.ConnectionId, userId); await Groups.AddToGroupAsync(Context.ConnectionId, userId);
} }
//create a terminal //create a terminal
public Task CreateTerminal(string serverId) public void CreateTerminal(string serverId)
{ {
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
sshStreamService.Connect(userId, serverId); sshStreamService.Connect(userId, serverId);
return Task.CompletedTask;
} }
//send a message to the terminal //send a message to the terminal
public async Task SendMessage(string message) public void SendMessage(string message)
{ {
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
Console.WriteLine(message); Console.WriteLine(message);
sshStreamService.Write(userId,message); sshStreamService.Write(userId,message);
} }
public void ResizeTerminal(int row, int col)
{
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
sshStreamService.ReSize(userId,col,row);
}
//断开 //断开
public override Task OnDisconnectedAsync(Exception? exception) public override Task OnDisconnectedAsync(Exception? exception)
{ {

View File

@ -49,7 +49,28 @@ public class SshStreamService : IDisposable
_sshStreams[userId]= (sshClient, shellStream); _sshStreams[userId]= (sshClient, shellStream);
} }
public void ReSize(string userId,int col, int row)
{
//从sshStreams中获取对象
if (_sshStreams.TryGetValue(userId, out var tuple))
{
var (ssh, stream) = tuple;
if (ssh.IsConnected)
{
var ansiResizeCommand = $"stty cols {col}; stty rows {row}";
stream.WriteLine(ansiResizeCommand);
}
else
{
throw new InvalidOperationException("SSH client is not connected or shell stream is not available.");
}
}
else
{
throw new InvalidOperationException("SSH connection not found for the given user ID.");
}
}
public void Write(string userId, string data) public void Write(string userId, string data)
{ {

View File

@ -2,20 +2,36 @@
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore"; import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {HubConnectionBuilder} from "@microsoft/signalr"; import {HubConnectionBuilder} from "@microsoft/signalr";
import {FitAddon} from 'xterm-addon-fit' import {FitAddon} from 'xterm-addon-fit'
import { ArrowDownRight } from 'lucide-vue-next';
//xtrem //xtrem
import {Terminal} from "@xterm/xterm"; import {Terminal} from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
import _ from "lodash";
var term: Terminal; var term: Terminal;
onMounted(()=>{ const terminal = ref<HTMLDivElement|null>(null);
term = new Terminal({rows:40,cols:100,smoothScrollDuration:100,scrollback:1000}); const resizeObserver =_.debounce(()=>{
term.open(document.getElementById("terminal") as HTMLElement); if(terminal.value===null) return;
const fitAddon = new FitAddon(); const cols = Math.floor(terminal.value.clientWidth / 9);
term.loadAddon(fitAddon); const rows = Math.floor(terminal.value.clientHeight / 17);
fitAddon.fit(); term.resize(cols-2, rows-1)
connection.invoke("ResizeTerminal", rows-1, cols-2)
term.focus(); term.focus();
},500)
onMounted(()=>{
term = new Terminal({smoothScrollDuration:100,scrollback:1000,theme:{
background: '#00000000',
}});
term.open(document.getElementById("terminal") as HTMLElement);
if(terminal.value===null) return;
const cols = Math.floor(terminal.value.clientWidth / 9);
const rows = Math.floor(terminal.value.clientHeight / 17);
term.resize(cols-2, rows-1)
connection.invoke("ResizeTerminal", rows-1, cols-2)
term.focus();
// //
term.onData((data) => { term.onData((data) => {
console.log(data)
connection.invoke("SendMessage", data) connection.invoke("SendMessage", data)
.catch(err => { .catch(err => {
console.log("Error while sending message: " + err); console.log("Error while sending message: " + err);
@ -34,6 +50,7 @@ const connection = new HubConnectionBuilder()
.build(); .build();
onMounted(async () => { onMounted(async () => {
connection.on("ReceiveMessage", (message) => { connection.on("ReceiveMessage", (message) => {
if(message.startsWith("stty cols")||message.startsWith("stty rows")) return;
term.write(message) term.write(message)
}) })
await connection.start() await connection.start()
@ -51,12 +68,49 @@ onMounted(async () => {
connection.invoke("SendMessage","neofetch\n") connection.invoke("SendMessage","neofetch\n")
},200) },200)
}) })
const resize=()=>{
//
const onMouseMove=(e:MouseEvent)=>{
//
const x = e.clientX;
const y = e.clientY;
// terminal
if(terminal.value===null) return;
let width = x - terminal.value.getBoundingClientRect().left;
let height = y - terminal.value.getBoundingClientRect().top;
// terminal
terminal.value.style.width = `${width}px`;
terminal.value.style.height = `${height}px`;
}
document.addEventListener('mousemove', onMouseMove, { passive: true });
//
const onMouseUp = () => {
if(terminal.value===null) return;
let width = terminal.value.getBoundingClientRect().width;
width = Math.round(width / 9) * 9+20;
let height = terminal.value.getBoundingClientRect().height;
height = Math.round(height / 17) * 17+20;
terminal.value.style.width = `${width}px`;
terminal.value.style.height = `${height}px`;
resizeObserver()
//
document.removeEventListener('mousemove', onMouseMove);
//
document.removeEventListener('mouseup', onMouseUp);
};
//
document.addEventListener('mouseup', onMouseUp, { passive: true });
}
</script> </script>
<template> <template>
<div class="terminal-box"> <div class="terminal-box" ref="terminal">
<div id="terminal"> <div id="terminal" @resize="resizeObserver" >
</div> </div>
<ArrowDownRight class="arrow" @mousedown="resize" />
</div> </div>
@ -66,13 +120,18 @@ onMounted(async () => {
@import "base"; @import "base";
.terminal-box { .terminal-box {
width: 100%; padding: 10px;
padding: $padding; width: 800px;
background: #000; height: 500px;
border-radius: $radius*2; max-width: 80vw;
max-height: 80vh;
border-radius: $radius;
background: rgba(0,0,0,.5);
backdrop-filter: blur(20px)
} }
#terminal { #terminal {
cursor: pointer;
&::-webkit-scrollbar{ &::-webkit-scrollbar{
width:0; width:0;
} }
@ -80,5 +139,11 @@ onMounted(async () => {
:deep(.xterm-viewport::-webkit-scrollbar){ :deep(.xterm-viewport::-webkit-scrollbar){
width: 0; width: 0;
} }
.arrow{
position: absolute;
right: 0;
bottom: 0;
color: $light-bg-underline-color;
cursor: nw-resize;
}
</style> </style>

View File

@ -8,6 +8,7 @@ import type {HttpType} from "~/types/baseType";
import {useSessionSignalRStore} from "~/strores/HubStore"; import {useSessionSignalRStore} from "~/strores/HubStore";
import {POSITION, useToast} from "vue-toastification"; import {POSITION, useToast} from "vue-toastification";
import {type dataHistoryType, useDataStore} from "~/strores/DataStore"; import {type dataHistoryType, useDataStore} from "~/strores/DataStore";
import VueDragResize from 'vue-drag-resize'
const audio = ref<any>(null); const audio = ref<any>(null);
const audio1 = ref<any>(null); const audio1 = ref<any>(null);
@ -197,8 +198,9 @@ onKeyStroke('Shift', (e) => {
</audio> </audio>
<SideBar/> <SideBar/>
<TitleBar/> <TitleBar/>
<n-modal v-model:show.lazy="visible" auto-focus style="max-width: 80vw" > <n-modal v-model:show.lazy="visible" auto-focus >
<Term/>
<Term/>
</n-modal> </n-modal>
<div class="main-box"> <div class="main-box">
<n-back-top :right="40" style="z-index: 200"/> <n-back-top :right="40" style="z-index: 200"/>
@ -317,6 +319,9 @@ onKeyStroke('Shift', (e) => {
padding: 0 20px 20px; padding: 0 20px 20px;
} }
:deep(.n-modal-mask){
backdrop-filter: blur(100px);
background: rgba(0,0,0,.7);
}
</style> </style>

View File

@ -33,6 +33,7 @@
"unplugin-vue-components": "^0.27.3", "unplugin-vue-components": "^0.27.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.4.27", "vue": "^3.4.27",
"vue-drag-resize": "^1.5.4",
"vue-echarts": "^6.7.3", "vue-echarts": "^6.7.3",
"vue-router": "^4.3.2", "vue-router": "^4.3.2",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",

View File

@ -3,17 +3,6 @@ definePageMeta({
layout: 'main', layout: 'main',
middleware: ['auth'] middleware: ['auth']
}) })
const panels=ref<(string|number)[]>([1, 2, 3, 4, 5])
const name=ref<string|number>(1)
const handleAdd=()=>{
//
panels.value.push(panels.value.length+1)
}
const handleClose=(name: number|string)=>{
panels.value=panels.value.filter((item)=>item!=name)
}
</script> </script>
@ -30,6 +19,9 @@ const handleClose=(name: number|string)=>{
height: 100%; height: 100%;
border-radius: $radius; border-radius: $radius;
border: $border; border: $border;
background: #000;
padding: $padding*2;
} }
</style> </style>

View File

@ -8384,6 +8384,13 @@ vue-devtools-stub@^0.1.0:
resolved "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz" resolved "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz"
integrity sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ== integrity sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==
vue-drag-resize@^1.5.0-rc3, vue-drag-resize@^1.5.4:
version "1.5.4"
resolved "https://registry.npmmirror.com/vue-drag-resize/-/vue-drag-resize-1.5.4.tgz#f583f40f356e5792aa89109b3d13ba4407c25198"
integrity sha512-SR3U7n6TAZEBgP7zw7bR9mjtAlYBjqIoaWTDPz5HXN/nYhOxKSA31aD7p71fmq1jtyt9reAnCx62valNL9ZAcg==
dependencies:
vue-drag-resize "^1.5.0-rc3"
vue-echarts@^6.7.3: vue-echarts@^6.7.3:
version "6.7.3" version "6.7.3"
resolved "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.7.3.tgz" resolved "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.7.3.tgz"