大修迷你终端
This commit is contained in:
parent
3b9d43acb7
commit
009f46879c
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -50,6 +50,27 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue