This commit is contained in:
parent
dfbc1cfdb0
commit
e77c7fa2df
|
@ -2,8 +2,6 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoongPanel-Asp", "LoongPanel-Asp\LoongPanel-Asp.csproj", "{3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pty.Net", "PtyTerminal\Pty.Net\Pty.Net.csproj", "{FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -14,9 +12,5 @@ Global
|
|||
{3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/Dpa/IsEnabledInDebug/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||
<Assembly Path="C:\Users\niyyz\.nuget\packages\czgl.systeminfo\2.2.0\lib\net7.0\CZGL.SystemInfo.dll" />
|
||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
|
@ -1,4 +1,5 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using LoongPanel_Asp.Models;
|
||||
using LoongPanel_Asp.utils;
|
||||
|
@ -88,7 +89,7 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
|||
InitializeRoles(modelBuilder);
|
||||
}
|
||||
|
||||
private static void InitializeRoles(ModelBuilder modelBuilder)
|
||||
private void InitializeRoles(ModelBuilder modelBuilder)
|
||||
{
|
||||
string[] roleNames = ["Admin", "User"];
|
||||
|
||||
|
@ -114,12 +115,11 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
|||
Name = roleName,
|
||||
NormalizedName = roleName.ToUpperInvariant(),
|
||||
ApiPermissions =
|
||||
["1","2,","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"],
|
||||
RouterPermissions = ["1", "2", "3", "4"]
|
||||
["1","2","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20","21","22","23","24","25","26","27","28","29","30"],
|
||||
RouterPermissions = ["1", "2", "3", "4","5","6","7","8","9","10","11","12","13","14"]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
var apiRouterPermissions = ControllerScanner.GetApiPermissions();
|
||||
|
||||
foreach (var permission in apiRouterPermissions)
|
||||
|
@ -133,14 +133,23 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
|||
//创建列表 {name:"主页",rote:"/Home"}
|
||||
List<RotePermission> rotePermissions =
|
||||
[
|
||||
new RotePermission { Id = 1, Name = "主页", Router = "/Home" },
|
||||
new RotePermission { Id = 2, Name = "用户", Router = "/User" },
|
||||
new RotePermission { Id = 3, Name = "cpu", Router = "/Host/Cpu" },
|
||||
new RotePermission { Id = 4, Name = "内存", Router = "/Host/Memory" }
|
||||
new RotePermission { Id = 1, Name = "主页", Router = "/home" },
|
||||
new RotePermission { Id = 2, Name = "用户", Router = "^/user+$" },
|
||||
new RotePermission { Id = 3, Name = "cpu", Router = "/host/cpu" },
|
||||
new RotePermission { Id = 4, Name = "内存", Router = "/host/memory" },
|
||||
new RotePermission { Id = 5, Name = "磁盘", Router = "^/host/disk/.+$" },
|
||||
new RotePermission { Id = 6, Name = "网络设备", Router = "^/host/network/.+$" },
|
||||
new RotePermission { Id = 7, Name = "Gpu", Router = "^/host/gpu/.+$" },
|
||||
new RotePermission { Id = 8, Name = "用户详细", Router = "^/userinfo/.+$" },
|
||||
new RotePermission { Id = 9, Name = "用户监测", Router = "^/serveruser/.+$" },
|
||||
new RotePermission { Id = 10, Name = "进程列表", Router = "/host/process" },
|
||||
new RotePermission { Id = 11, Name = "网络连接列表", Router = "/host/networklist" },
|
||||
new RotePermission { Id = 12, Name = "巡检记录", Router = "/inspectionrecords" },
|
||||
];
|
||||
|
||||
foreach (var permission in rotePermissions) modelBuilder.Entity<RotePermission>().HasData(permission);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ApplicationUser : IdentityUser
|
||||
|
@ -151,7 +160,6 @@ public class ApplicationUser : IdentityUser
|
|||
|
||||
[MaxLength(255)] public string? Desc { get; set; }
|
||||
|
||||
//职位
|
||||
[MaxLength(255)] public required string Posts { get; set; }
|
||||
|
||||
[MaxLength(255)] public string? Address { get; set; }
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[CpuTotalUsage_d3YT]
|
||||
Notify=20
|
||||
Warning=30
|
|
@ -1,2 +0,0 @@
|
|||
[CpuTotalUsage]
|
||||
Value=80
|
|
@ -68,4 +68,12 @@ ValueName = 网络总使用率
|
|||
Description = A simple job that uses the Network
|
||||
JobType = LoongPanel_Asp.Jobs.NetworkTotalJob, LoongPanel-Asp
|
||||
Executor = d3YT,xseg
|
||||
CronExpression = 3/15 * * * * ? *
|
||||
CronExpression = 3/15 * * * * ? *
|
||||
|
||||
[UserTotalJob]
|
||||
Group = User
|
||||
ValueName = 分用户总使用
|
||||
Description = A simple job that uses the Network
|
||||
JobType = LoongPanel_Asp.Jobs.UserTotalJob, LoongPanel-Asp
|
||||
Executor = d3YT,xseg
|
||||
CronExpression = 5/15 * * * * ? *
|
|
@ -2,14 +2,14 @@
|
|||
address = 192.168.0.26
|
||||
port = 22
|
||||
serverName = 龙芯
|
||||
password = loongnix
|
||||
username = loongnix
|
||||
password = loongpanel
|
||||
username = loongpanel
|
||||
https = false
|
||||
|
||||
[xseg]
|
||||
address = 127.0.0.1
|
||||
address = 129.204.245.145
|
||||
port = 22
|
||||
serverName = 本机
|
||||
password = 123123
|
||||
username = zwb
|
||||
serverName = 远端debian
|
||||
password = loongpanel
|
||||
username = loongpanel
|
||||
https = false
|
|
@ -18,141 +18,43 @@ public class AccountController(
|
|||
ILiteDatabase db)
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpPost("SendVerificationCode")]
|
||||
public async Task<IActionResult> SendVerificationCode([FromBody] EmailModel model)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
|
||||
try
|
||||
{
|
||||
var code = emailHelper.GenerateVerificationCode();
|
||||
Console.WriteLine(code);
|
||||
await emailHelper.SendVerificationEmailAsync(model.Email, code);
|
||||
|
||||
// 生成过期时间 5分钟
|
||||
var expireTime = DateTime.Now.AddMinutes(5);
|
||||
var col = db.GetCollection<EmailCode>("EmailCode");
|
||||
var userCode = new EmailCode
|
||||
{
|
||||
Email = model.Email,
|
||||
Code = code,
|
||||
ExpireTime = expireTime
|
||||
};
|
||||
|
||||
col.EnsureIndex(x => x.Email, true);
|
||||
|
||||
// 使用 Upsert 来插入或更新验证码记录
|
||||
col.DeleteMany(x => x.Email == model.Email);
|
||||
col.Insert(userCode);
|
||||
|
||||
// 注册成功,返回一个合适的响应
|
||||
return Ok("Registration successful. Please check your email for the verification code.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception
|
||||
Console.WriteLine(ex.Message);
|
||||
// 返回一个错误响应
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
$"An error occurred while processing your request.{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterModel model)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request"));
|
||||
|
||||
if (!ModelState.IsValid) return BadRequest("错误的请求");
|
||||
try
|
||||
{
|
||||
// 获取code,email
|
||||
var col = db.GetCollection<EmailCode>("EmailCode");
|
||||
// 使用email查询
|
||||
var userCode = col.FindOne(x => x.Email == model.Email);
|
||||
// 判断结果
|
||||
if (userCode == null) return BadRequest(new ApiResponse(ApiResponseState.Error, "Code not found"));
|
||||
|
||||
if (userCode.Code != model.Code)
|
||||
return BadRequest(new ApiResponse(ApiResponseState.Error, "Code does not match"));
|
||||
|
||||
// 判断是否过期
|
||||
if (userCode.ExpireTime < DateTime.Now)
|
||||
return BadRequest(new ApiResponse(ApiResponseState.Error, "Code expired"));
|
||||
|
||||
// 注册用户
|
||||
if (HttpContext.RequestServices.GetService(typeof(UserManager<ApplicationUser>)) is not
|
||||
UserManager<ApplicationUser> manager)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
new ApiResponse(ApiResponseState.Error, "UserManager not found"));
|
||||
|
||||
var user = new ApplicationUser
|
||||
//判断用户名,邮箱是否唯一
|
||||
var user = await userManager.FindByNameAsync(model.UserName);
|
||||
if (user != null) return BadRequest("用户名已存在");
|
||||
|
||||
//创建用户
|
||||
user = new ApplicationUser
|
||||
{
|
||||
UserName = model.UserName,
|
||||
Email = model.Email,
|
||||
PhoneNumber = model.Phone,
|
||||
NickName = model.NickName,
|
||||
EmailConfirmed = true,
|
||||
|
||||
Avatar = $"https://api.multiavatar.com/{model.UserName}.svg",
|
||||
Posts = "员工",
|
||||
Posts = model.Position,
|
||||
CreateDate = DateTime.UtcNow,
|
||||
ModifiedDate = DateTime.UtcNow
|
||||
ModifiedDate = DateTime.UtcNow,
|
||||
Email = model.Email,
|
||||
UserName = model.UserName,
|
||||
PhoneNumber = model.Phone,
|
||||
NickName = model.FullName,
|
||||
};
|
||||
|
||||
var result = await manager.CreateAsync(user, model.Password);
|
||||
// 验证成功,删除验证码
|
||||
col.DeleteMany(x => x.Email == model.Email);
|
||||
|
||||
if (!result.Succeeded)
|
||||
return BadRequest(new ApiResponse(ApiResponseState.Error, "User creation failed", null, result.Errors));
|
||||
|
||||
// 添加用户角色
|
||||
if (HttpContext.RequestServices.GetService(typeof(RoleManager<ApplicationRole>)) is not
|
||||
RoleManager<ApplicationRole> roleManager)
|
||||
{
|
||||
// 如果角色管理器不存在,删除刚刚创建的用户
|
||||
await manager.DeleteAsync(user);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
new ApiResponse(ApiResponseState.Error, "RoleManager not found"));
|
||||
}
|
||||
|
||||
var roleResult = await manager.AddToRoleAsync(user, "user");
|
||||
|
||||
if (!roleResult.Succeeded)
|
||||
{
|
||||
// 如果角色分配失败,删除刚刚创建的用户
|
||||
await manager.DeleteAsync(user);
|
||||
return BadRequest(new ApiResponse(ApiResponseState.Error, "Role assignment failed", null,
|
||||
roleResult.Errors));
|
||||
}
|
||||
|
||||
// 用户注册成功
|
||||
return Ok(new ApiResponse(ApiResponseState.Success, "User registered successfully"));
|
||||
var result = await userManager.CreateAsync(user, model.Password);
|
||||
if (!result.Succeeded) return BadRequest("无法创建用户,"+string.Join(",",result.Errors.ToList().Select(e=>e.Description)));
|
||||
|
||||
//添加用户到默认角色
|
||||
result = await userManager.AddToRoleAsync(user, model.Role);
|
||||
if (!result.Succeeded) return BadRequest("无法创建用户,"+string.Join(",",result.Errors.ToList().Select(e=>e.Description)));
|
||||
return Ok("用户创建成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception e)
|
||||
{
|
||||
// Log the exception
|
||||
Console.WriteLine(ex.Message);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
"An error occurred while processing your request.");
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("VerifyEmailName")]
|
||||
public async Task<IActionResult> VerifyEmailName([FromBody] VerifyEmailNameModel model)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request", null));
|
||||
|
||||
var user = await userManager.FindByEmailAsync(model.Email);
|
||||
|
||||
if (user != null) return Ok(new ApiResponse(ApiResponseState.Error, "Email already exists", null));
|
||||
|
||||
var userName = await userManager.FindByNameAsync(model.UserName);
|
||||
|
||||
if (userName != null) return Ok(new ApiResponse(ApiResponseState.Error, "UserName already exists", null));
|
||||
|
||||
return Ok(new ApiResponse(ApiResponseState.Success, "Email and UserName are available", null));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("Login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginModel model)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace LoongPanel_Asp.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("Api/[controller]")]
|
||||
public class PublicFileController(IWebHostEnvironment webHostEnvironment) : ControllerBase
|
||||
{
|
||||
[HttpPost("UploadImage")]
|
||||
public async Task<IActionResult> UploadImage(IFormFile? file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest("文件不能为空");
|
||||
}
|
||||
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
Console.WriteLine(extension);
|
||||
if (!allowedExtensions.Contains(extension))
|
||||
{
|
||||
return BadRequest("不支持的文件类型");
|
||||
}
|
||||
var uploadsFolderPath = Path.Combine(webHostEnvironment.WebRootPath, "public/image");
|
||||
if (!Directory.Exists(uploadsFolderPath))
|
||||
{
|
||||
Directory.CreateDirectory(uploadsFolderPath);
|
||||
}
|
||||
|
||||
var uniqueFileName = $"{Guid.NewGuid()}{extension}";
|
||||
var filePath = Path.Combine(uploadsFolderPath, uniqueFileName);
|
||||
await using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
var fileUrl = $"/public/image/{uniqueFileName}";
|
||||
return Ok(new { fileUrl });
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Specialized;
|
||||
using System.Dynamic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
@ -7,6 +9,7 @@ using System.Text.RegularExpressions;
|
|||
using LoongPanel_Asp.Helpers;
|
||||
using LoongPanel_Asp.Models;
|
||||
using LoongPanel_Asp.Servers;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -187,8 +190,8 @@ public class ServerController(IServiceProvider serviceProvider, ApplicationDbCon
|
|||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x =>x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo {server.Password}","|","sudo -S fdisk -l","|","grep 'Disk /'","|","awk '{print $2,$3}'")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo {server.Password}","|","sudo -S /usr/sbin/fdisk -l","|","grep 'Disk /'","|","awk '{print $2,$3}'")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest(output);
|
||||
var diskList = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
|
||||
var outList = diskList.Select(disk => disk.Split(":", StringSplitOptions.RemoveEmptyEntries)).Select(info => new { name = info[0], size = info[1] }).ToList();
|
||||
return Ok(outList);
|
||||
|
@ -206,6 +209,19 @@ public class ServerController(IServiceProvider serviceProvider, ApplicationDbCon
|
|||
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
|
||||
return Ok(data);
|
||||
}
|
||||
[HttpGet("GetServerNetworkEquipmentInfo")]
|
||||
public async Task<IActionResult> GetServerNetworkEquipmentInfo([FromQuery] string serverId,[FromQuery] string networkId)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x =>x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,"/usr/sbin/ifconfig",networkId)!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest();
|
||||
output = string.Join(" ", output.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()));
|
||||
var data = output.Split(" ", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Skip(1).ToList();
|
||||
return Ok(data);
|
||||
}
|
||||
[HttpGet("GetServerGpuList")]
|
||||
public async Task<IActionResult> GetServerGpuList([FromQuery] string serverId)
|
||||
{
|
||||
|
@ -226,9 +242,19 @@ public class ServerController(IServiceProvider serviceProvider, ApplicationDbCon
|
|||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x =>x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var type = "ata";
|
||||
if (diskId.StartsWith("nvme")) type = "nvme";
|
||||
else if (diskId.StartsWith("vd")) type = "ata";
|
||||
else if (diskId.StartsWith("sd")) type = "ata";
|
||||
else if (diskId.StartsWith("hd")) type = "ata";
|
||||
diskId = $"/dev/{diskId}";
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo '{server.Password}'","|","sudo -S","smartctl -i",diskId,"-T permissive","|","awk 'NR>4'")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo '{server.Password}'","|","sudo -S","/usr/sbin/smartctl -i",diskId,$"-d {type}","-T permissive","2>/dev/null","|","awk 'NR>4'")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest(output);
|
||||
if (output.Contains("=== START OF INFORMATION SECTION ==="))
|
||||
{
|
||||
//截断
|
||||
output = output.Substring(output.IndexOf("=== START OF INFORMATION SECTION ===", StringComparison.Ordinal) + "=== START OF INFORMATION SECTION ===".Length);
|
||||
}
|
||||
var diskInfo = output
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.Trim().Split(':'))
|
||||
|
@ -240,34 +266,238 @@ public class ServerController(IServiceProvider serviceProvider, ApplicationDbCon
|
|||
return Ok(diskInfo);
|
||||
}
|
||||
|
||||
[HttpGet("GetServerTerminalPath")]
|
||||
public async Task<IActionResult> GetServerTerminalPath([FromQuery] string serverId)
|
||||
[HttpGet("GetServerUserList")]
|
||||
public async Task<IActionResult> GetServerUserList([FromQuery] string serverId)
|
||||
{
|
||||
var serverConfig = JobConfigHelper.GetServers().Find(x => x.Id == serverId);
|
||||
if (serverConfig == null) return BadRequest();
|
||||
//获得本机ip
|
||||
string? localIp;
|
||||
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0))
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x =>x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,"awk -F: '$1 != \"nobody\" && $1 != \"build\" && $3 >= 1000 {print $1}'","/etc/passwd")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest("无法获得用户树");
|
||||
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
|
||||
output = await sshClient?.ExecuteCommandAsync(serverId,"w -husf","|"," awk '$2 !~ /^tty/ {print$1, $2}'"," |"," sort ","|"," uniq")!;
|
||||
var onlineList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
|
||||
output = await sshClient?.ExecuteCommandAsync(serverId,"lastlog | awk 'NR > 1 { if ($2 ~ /^**Never/) {print $1, \"-\",\"NULL\"} else {print $1,$2, substr($0,index($0,$3))}}' ")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest("无法获得用户登录记录");
|
||||
var loginList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
|
||||
var serverUserList = new List<ServerUserInfo>();
|
||||
data.ForEach(x =>
|
||||
{
|
||||
await socket.ConnectAsync("8.8.8.8", 65530);
|
||||
var endPoint = socket.LocalEndPoint as IPEndPoint;
|
||||
localIp = endPoint?.Address.ToString();
|
||||
var d = new ServerUserInfo
|
||||
{
|
||||
Name = x,
|
||||
IsOnline = false,
|
||||
LastLoginTime = null,
|
||||
Port = null,
|
||||
Address = "::1"
|
||||
};
|
||||
serverUserList.Add(d);
|
||||
});
|
||||
|
||||
onlineList.ForEach(x =>
|
||||
{
|
||||
var line = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
var d = serverUserList.Find(y => y.Name == line[0]);
|
||||
if (d == null) return;
|
||||
d.IsOnline = true;
|
||||
d.Address = line[1];
|
||||
});
|
||||
|
||||
loginList.ForEach(x =>
|
||||
{
|
||||
var len = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
var name = len[0];
|
||||
var d = serverUserList.Find(y => y.Name == name);
|
||||
if(d==null) return;
|
||||
var port = len[1];
|
||||
var time = string.Join(" ",len.Skip(2).ToList());
|
||||
if (time != "NULL")
|
||||
{
|
||||
DateTimeOffset.TryParseExact(time,
|
||||
"ddd MMM d HH:mm:ss zzz yyyy",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var dateTimeOffset);
|
||||
time = dateTimeOffset.ToString("G");
|
||||
}
|
||||
d.LastLoginTime = time;
|
||||
d.Port = port;
|
||||
});
|
||||
serverUserList = serverUserList.OrderByDescending(x => x.IsOnline).ThenByDescending(x => x.LastLoginTime).ToList();
|
||||
return Ok(serverUserList);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("GetServerProcessesList")]
|
||||
public async Task<IActionResult> GetServerProcessesList([FromQuery] string serverId,[FromQuery] string? userName)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x => x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,
|
||||
"ps -eo pid,user,%cpu,%mem,comm --sort=-%cpu | awk 'NR>1'",userName!=null?$"| grep {userName}":"" )!;
|
||||
var data = output.Split("\n", StringSplitOptions.RemoveEmptyEntries);
|
||||
var processList = data.Select(x =>
|
||||
{
|
||||
x=x.Trim();
|
||||
var line = x.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
return new
|
||||
{
|
||||
Pid=line[0],
|
||||
User = line[1],
|
||||
Cpu=line[2],
|
||||
Memory=line[3],
|
||||
ProcessName=string.Join(" ",line.Skip(4))
|
||||
};
|
||||
}).ToList();
|
||||
return Ok(processList);
|
||||
}
|
||||
|
||||
[HttpGet("GetServerProcessesKill")]
|
||||
public async Task<IActionResult> GetServerProcessesKill([FromQuery] string serverId, [FromQuery] string pid,[FromQuery] bool force=false)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x => x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,
|
||||
$"echo {server.Password}","|","sudo -SS","kill",force?"-9":"-15",pid )!;
|
||||
return Ok($"关闭信号已发送,{output}");
|
||||
}
|
||||
[HttpGet("GetServerNetworkList")]
|
||||
public async Task<IActionResult> GetServerNetworkList([FromQuery] string serverId, [FromQuery] string? userName)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var serverConfigs = JobConfigHelper.GetServers().ToList();
|
||||
var server = serverConfigs.Find(x => x.Id == serverId);
|
||||
if (server == null) return BadRequest();
|
||||
var output = await sshClient?.ExecuteCommandAsync(serverId,
|
||||
$"echo {server.Password}","|","sudo -S ss -tunapo")!;
|
||||
if (string.IsNullOrEmpty(output)) return BadRequest("返回为空");
|
||||
try
|
||||
{
|
||||
var data = output.Split("\n", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Skip(1) // 跳过第一行
|
||||
.Select(x => x.Trim())
|
||||
.Select(x => x.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(x => x.Length != 7).ToList();
|
||||
|
||||
var networkList = data.Select(x => new
|
||||
{
|
||||
netId=x[0],
|
||||
recvQ=x[2],
|
||||
sendQ=x[3],
|
||||
addressForm=x[4],
|
||||
addressTo=x[5],
|
||||
process=x[6].Split("),(",StringSplitOptions.RemoveEmptyEntries).Select(s=>new
|
||||
{
|
||||
name=s.Split(",")[0].Replace("users:((\"","").Replace("\"",""),
|
||||
pid=s.Split(",")[1].Split("=",StringSplitOptions.RemoveEmptyEntries)[1]
|
||||
})
|
||||
});
|
||||
return Ok(networkList);
|
||||
}
|
||||
//拼接网络路径
|
||||
var uriBuilder = new UriBuilder
|
||||
catch (Exception e)
|
||||
{
|
||||
Scheme = "http",
|
||||
Host = localIp,
|
||||
Port = 8888,
|
||||
};
|
||||
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||
query["hostname"]=serverConfig.Address;
|
||||
query["username"] = serverConfig.Username;
|
||||
query["password"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(serverConfig.Password));
|
||||
query["port"] = serverConfig.Port.ToString();
|
||||
uriBuilder.Query = query.ToString();
|
||||
var url = uriBuilder.Uri.ToString();
|
||||
return Ok(url);
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("UpLoadWord")]
|
||||
public async Task<IActionResult> UpLoadWord(
|
||||
[FromQuery] string serverId,
|
||||
[FromQuery] string userName,
|
||||
[FromQuery] string wordId,
|
||||
[FromBody] WordModel content)
|
||||
{
|
||||
// 拼接路径 markdowns/ServerId/id.json
|
||||
var path = Path.Combine(AppContext.BaseDirectory,"markdowns",serverId, $"{wordId}.json");
|
||||
var createAt=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
var lastModifyAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
var directoryPath = Path.GetDirectoryName(path);
|
||||
|
||||
if (!Directory.Exists(directoryPath))
|
||||
{
|
||||
// 不存在则创建目录
|
||||
if (directoryPath != null) Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
// 读取现有JSON文件
|
||||
var json = await System.IO.File.ReadAllTextAsync(path);
|
||||
// 反序列化JSON为动态对象
|
||||
dynamic existingWord = Newtonsoft.Json.JsonConvert.DeserializeObject(json) ?? new ExpandoObject();;
|
||||
|
||||
// 使用内部的createAt更新createAt
|
||||
createAt = existingWord.createAt;
|
||||
}
|
||||
|
||||
|
||||
// 创建新的配置对象
|
||||
var newWord = new WordFileModel
|
||||
{
|
||||
UserName = userName,
|
||||
WordId = wordId,
|
||||
Content = content.Content,
|
||||
WordName = content.Name,
|
||||
CreateAt = createAt,
|
||||
LastModifyAt = lastModifyAt
|
||||
};
|
||||
// 将新的配置对象序列化为JSON
|
||||
var newJson = Newtonsoft.Json.JsonConvert.SerializeObject(newWord);
|
||||
|
||||
// 覆盖写入新的JSON配置
|
||||
await System.IO.File.WriteAllTextAsync(path, newJson);
|
||||
|
||||
// 返回成功响应
|
||||
return Ok("文件已经保存");
|
||||
}
|
||||
|
||||
[HttpGet("GetWordList")]
|
||||
public async Task<IActionResult> GetWordList([FromQuery] string serverId)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId);
|
||||
var files = Directory.GetFiles(path);
|
||||
var wordList = new List<WordFileModel>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileInfo = new FileInfo(file);
|
||||
var json = await System.IO.File.ReadAllTextAsync(file);
|
||||
var word = Newtonsoft.Json.JsonConvert.DeserializeObject<WordFileModel>(json);
|
||||
//去除content
|
||||
if (word == null) continue;
|
||||
word.Content = null;
|
||||
var fileSize = fileInfo.Length;
|
||||
word.FileSize = fileSize.ToString();
|
||||
wordList.Add(word);
|
||||
}
|
||||
return Ok(wordList);
|
||||
}
|
||||
|
||||
[HttpGet("GetWordContent")]
|
||||
public async Task<IActionResult> GetWordContent([FromQuery] string serverId, [FromQuery] string wordId)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", serverId, wordId + ".json");
|
||||
var json = await System.IO.File.ReadAllTextAsync(path);
|
||||
var word = Newtonsoft.Json.JsonConvert.DeserializeObject<WordFileModel>(json);
|
||||
return Ok(word);
|
||||
|
||||
}
|
||||
[HttpGet("GetWordTemplates")]
|
||||
public async Task<IActionResult> GetWordTemplates()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "markdowns", "templates");
|
||||
var files = Directory.GetFiles(path);
|
||||
var templates = files.Select( x => new
|
||||
{
|
||||
name = Path.GetFileName(x).Replace(".md",""),
|
||||
content= System.IO.File.ReadAllTextAsync(x).Result
|
||||
}).ToList();
|
||||
return Ok(templates);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,3 +506,29 @@ public class ServerInfo
|
|||
public required string Name { get; init; }
|
||||
public required string Id { get; init; }
|
||||
}
|
||||
|
||||
public class WordModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
||||
public class WordFileModel
|
||||
{
|
||||
public required string UserName { get; set; }
|
||||
public required string WordId { get; set; }
|
||||
public required string? Content { get; set; }
|
||||
public string? FileSize { get; set; }
|
||||
public required string CreateAt { get; set; }
|
||||
public required string WordName { get; set;}
|
||||
public required string LastModifyAt { get; set; }
|
||||
}
|
||||
|
||||
public class ServerUserInfo
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required bool IsOnline { get; set; }
|
||||
public required string? LastLoginTime { get; set; }
|
||||
public required string? Address { get; set; }
|
||||
public required string? Port { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
namespace LoongPanel_Asp.Helpers;
|
||||
using LoongPanel_Asp.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
public class DataHelper(ApplicationDbContext dbContext)
|
||||
namespace LoongPanel_Asp.Helpers;
|
||||
|
||||
public class DataHelper(ApplicationDbContext dbContext,IHubContext<SessionHub> context)
|
||||
{
|
||||
public async Task SaveData(ServerMonitoringData data)
|
||||
{
|
||||
|
@ -27,11 +30,33 @@ public class DataHelper(ApplicationDbContext dbContext)
|
|||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
public static async Task CheckData(string serverId,string dataType,double value)
|
||||
public async Task CheckData(string serverId,string valueType,string value,string valueName)
|
||||
{
|
||||
|
||||
var alertConfigs = JobConfigHelper.GetAlerts();
|
||||
var alert=alertConfigs[serverId][dataType];
|
||||
|
||||
|
||||
if (!alertConfigs.TryGetValue(serverId, out var serverAlert)) return;
|
||||
serverAlert.Notify.TryGetValue(valueType, out var notifyValuePairs);
|
||||
serverAlert.Warning.TryGetValue(valueType, out var warningValuePairs);
|
||||
var matchingValues = warningValuePairs?.Where(pair => double.Parse(value) >= double.Parse(pair.Key))
|
||||
.SelectMany(pair => pair.Value).Distinct().ToList();
|
||||
|
||||
if (matchingValues?.Count > 0)
|
||||
{
|
||||
foreach (var item in matchingValues)
|
||||
{
|
||||
await context.Clients.Group(item).SendAsync("ReceiveWaring", value, valueName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
matchingValues = notifyValuePairs?.Where(pair => double.Parse(value) >= double.Parse(pair.Key))
|
||||
.SelectMany(pair => pair.Value).Distinct().ToList();
|
||||
if (matchingValues?.Count > 0)
|
||||
{
|
||||
foreach (var item in matchingValues)
|
||||
{
|
||||
await context.Clients.Group(item).SendAsync("ReceiveNotify", value, valueName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ namespace LoongPanel_Asp.Helpers;
|
|||
public static class JobConfigHelper
|
||||
{
|
||||
private static List<ServerModel>? _serverConfigs;
|
||||
private static Dictionary<string, Dictionary<string, Dictionary<string, double>>>? _alertsConfigs;
|
||||
private static Dictionary<string, AlertConfiguration>? _alertsConfigs;
|
||||
|
||||
public static IEnumerable<JobConfiguration> ReadJobConfigurations()
|
||||
{
|
||||
|
@ -71,53 +71,98 @@ public static class JobConfigHelper
|
|||
return _serverConfigs;
|
||||
}
|
||||
|
||||
public static Dictionary<string, Dictionary<string, Dictionary<string, double>>> GetAlerts()
|
||||
public static Dictionary<string,AlertConfiguration> GetAlerts()
|
||||
{
|
||||
if (_alertsConfigs != null) return _alertsConfigs;
|
||||
|
||||
// 创建_alertsConfigs
|
||||
_alertsConfigs = new Dictionary<string, Dictionary<string, Dictionary<string, double>>>();
|
||||
var alertsConfigs = new Dictionary<string, AlertConfiguration>();
|
||||
var parser = new FileIniDataParser();
|
||||
var alertsFolderPath = Path.Combine(Environment.CurrentDirectory, "Configs", "Alerts");
|
||||
var defaultAlert = Path.Combine(Environment.CurrentDirectory, "Configs", "alert.ini");
|
||||
// 检查目录是否存在,如果不存在则创建
|
||||
if (!Directory.Exists(alertsFolderPath)) Directory.CreateDirectory(alertsFolderPath);
|
||||
|
||||
|
||||
// 获取目录下所有.ini文件的路径
|
||||
var alertFiles = Directory.GetFiles(alertsFolderPath, "*.ini");
|
||||
foreach (var filePath in alertFiles)
|
||||
var notifyUsersMap = new Dictionary<string,Dictionary<string, Dictionary<string, List<string>>>>();
|
||||
var warningUsersMap = new Dictionary<string,Dictionary<string, Dictionary<string, List<string>>>>();
|
||||
// 读取配置信息
|
||||
foreach (var alertFile in alertFiles)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(filePath);
|
||||
var parts = name.Split('_');
|
||||
var serverId = parts[0] ;
|
||||
var userId = parts[1];
|
||||
// 读取ini文件
|
||||
|
||||
var data = parser.ReadFile(filePath, Encoding.UTF8);
|
||||
Console.WriteLine(data.ToString());
|
||||
// 获取所有section
|
||||
foreach (var section in data.Sections)
|
||||
var alertData = parser.ReadFile(alertFile, Encoding.UTF8);
|
||||
var alertSections = alertData.Sections;
|
||||
if (alertSections.Count == 0) continue;
|
||||
//遍历每一个section
|
||||
foreach (var alertSection in alertSections)
|
||||
{
|
||||
// 解析每个section为AlertsModel
|
||||
var type = section.SectionName;
|
||||
var value = double.Parse(section.Keys["Value"]);
|
||||
|
||||
// 添加到字典
|
||||
if (serverId != null && !(_alertsConfigs.ContainsKey(serverId)))
|
||||
var serverId = alertSection.SectionName.Split("_")[1];
|
||||
var type = alertSection.SectionName.Split("_")[0];
|
||||
var notifyValue= alertSection.Keys["Notify"];
|
||||
var warningValue = alertSection.Keys["Warning"];
|
||||
var userId = Path.GetFileNameWithoutExtension(alertFile);
|
||||
if (!notifyUsersMap.TryGetValue(serverId, out var notifyTypes))
|
||||
{
|
||||
_alertsConfigs[serverId] = new Dictionary<string, Dictionary<string, double>>();
|
||||
}
|
||||
if (serverId != null && !_alertsConfigs[serverId].ContainsKey(type))
|
||||
{
|
||||
_alertsConfigs[serverId][type] = new Dictionary<string, double>();
|
||||
notifyTypes = [];
|
||||
notifyUsersMap.Add(serverId, notifyTypes);
|
||||
}
|
||||
|
||||
if (userId == null) continue;
|
||||
if (serverId != null) _alertsConfigs[serverId][type][userId] = value;
|
||||
if (!warningUsersMap.TryGetValue(serverId, out var warningTypes))
|
||||
{
|
||||
warningTypes = [];
|
||||
warningUsersMap.Add(serverId, warningTypes);
|
||||
}
|
||||
if (!notifyTypes.TryGetValue(type, out var notifyValues))
|
||||
{
|
||||
notifyValues = [];
|
||||
notifyTypes.Add(type, notifyValues);
|
||||
}
|
||||
|
||||
if (!warningTypes.TryGetValue(type, out var warningValues))
|
||||
{
|
||||
warningValues = [];
|
||||
warningTypes.Add(type, warningValues);
|
||||
}
|
||||
|
||||
if (!notifyValues.TryGetValue(notifyValue, out var notifyUsersList))
|
||||
{
|
||||
notifyUsersList = [];
|
||||
notifyValues.Add(notifyValue, notifyUsersList);
|
||||
}
|
||||
if (!warningValues.TryGetValue(warningValue, out var warningUsersList))
|
||||
{
|
||||
warningUsersList = [];
|
||||
warningValues.Add(warningValue, warningUsersList);
|
||||
}
|
||||
notifyUsersList.Add(userId);
|
||||
warningUsersList.Add(userId);
|
||||
}
|
||||
}
|
||||
//遍历 notifyUsersMap
|
||||
foreach (var (serverId, notifyUsersList) in notifyUsersMap)
|
||||
{
|
||||
//获得key 和 value
|
||||
if(!alertsConfigs.TryGetValue(serverId, out var alertsConfig)){
|
||||
//创建新的
|
||||
alertsConfig = new AlertConfiguration();
|
||||
alertsConfigs.Add(serverId, alertsConfig);
|
||||
}
|
||||
alertsConfig.Notify= notifyUsersList;
|
||||
}
|
||||
|
||||
return _alertsConfigs;
|
||||
foreach (var (serverId, emailUsersList) in warningUsersMap)
|
||||
{
|
||||
//获得key 和 value
|
||||
if (!alertsConfigs.TryGetValue(serverId, out var alertsConfig))
|
||||
{
|
||||
//创建新的
|
||||
alertsConfig = new AlertConfiguration();
|
||||
alertsConfigs.Add(serverId, alertsConfig);
|
||||
}
|
||||
alertsConfig.Warning = emailUsersList;
|
||||
}
|
||||
|
||||
_alertsConfigs =alertsConfigs;
|
||||
return alertsConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,3 +189,10 @@ public class JobConfiguration
|
|||
//ValueName
|
||||
public string? ValueName { get; init; }
|
||||
}
|
||||
|
||||
public class AlertConfiguration(Dictionary<string, Dictionary<string, List<string>>>? notify=null,
|
||||
Dictionary<string, Dictionary<string, List<string>>>? warning=null)
|
||||
{
|
||||
public Dictionary<string, Dictionary<string, List<string>>> Notify { get; set; } = notify ?? [];
|
||||
public Dictionary<string, Dictionary<string, List<string>>> Warning { get; set; } = warning ?? [];
|
||||
}
|
||||
|
|
|
@ -11,11 +11,8 @@ public class SessionHub(UserManager<ApplicationUser> userManager, ILiteDatabase
|
|||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
||||
Console.WriteLine(userId);
|
||||
//获得登陆ip
|
||||
var ip = Context.GetHttpContext()!.Connection.RemoteIpAddress;
|
||||
Console.WriteLine(ip);
|
||||
Console.WriteLine(userId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId);
|
||||
// 查询获取用户对象
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
using System.Security.Claims;
|
||||
using LoongPanel_Asp.Servers;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace LoongPanel_Asp.Hubs;
|
||||
|
||||
public class TerminalHub(SshStreamService sshStreamService):Hub
|
||||
{
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, userId);
|
||||
}
|
||||
//create a terminal
|
||||
public Task CreateTerminal(string serverId)
|
||||
{
|
||||
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
||||
sshStreamService.Connect(userId, serverId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
//send a message to the terminal
|
||||
public async Task SendMessage(string message)
|
||||
{
|
||||
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
||||
Console.WriteLine(message);
|
||||
sshStreamService.Write(userId,message);
|
||||
}
|
||||
//断开
|
||||
public override Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
|
||||
sshStreamService.Disconnect(userId);
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LoongPanel_Asp.Helpers;
|
||||
|
||||
namespace LoongPanel_Asp
|
||||
{
|
||||
public class Init : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public Init(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||
// 检查管理员用户是否存在
|
||||
var adminUser = await userManager.FindByNameAsync("admin");
|
||||
if (adminUser == null)
|
||||
{
|
||||
adminUser = new ApplicationUser
|
||||
{
|
||||
Avatar = "https://api.multiavatar.com/admin.svg",
|
||||
Posts = "管理员",
|
||||
CreateDate = DateTime.UtcNow,
|
||||
ModifiedDate = DateTime.UtcNow,
|
||||
Email = "admin@admin.com",
|
||||
UserName = "admin",
|
||||
PhoneNumber = "999999999",
|
||||
NickName = "默认管理员",
|
||||
};
|
||||
|
||||
var result = await userManager.CreateAsync(adminUser, "Qwertyuiop123!@#");
|
||||
//分配管理员角色
|
||||
if (result.Succeeded)
|
||||
await userManager.AddToRoleAsync(adminUser, "admin");
|
||||
if (result.Succeeded)
|
||||
Console.WriteLine("管理员创建成功,账号:{0},密码:{1}", "admin", "Qwertyuiop123!@#");
|
||||
else
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
Console.WriteLine(error.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 这里可以执行一些应用程序停止时的清理操作
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,12 +19,12 @@ public class CpuTotalJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var cpuDataListAll = new List<ServerMonitoringData>();
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
foreach (var server in serverList)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var output = await sshClient?.ExecuteCommandAsync(server.Id, "sar", "-u", "3 1", "|", "grep", "Average")!;
|
||||
if (string.IsNullOrEmpty(output)) continue;
|
||||
|
||||
|
@ -50,9 +50,10 @@ public class CpuTotalJob(
|
|||
DataType = "CpuTotalUsage"
|
||||
};
|
||||
cpuDataList.Add(totalUsage);
|
||||
cpuDataList.ForEach(data =>
|
||||
cpuDataList.ForEach(async data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType??"", data.Data??"", data.DataName);
|
||||
});
|
||||
cpuDataListAll.AddRange(cpuDataList);
|
||||
}
|
||||
|
@ -75,12 +76,12 @@ public class CpuSpeedJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var cpuDataListAll = new List<ServerMonitoringData>();
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
foreach (var server in serverList)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var output =
|
||||
await sshClient?.ExecuteCommandAsync(server.Id, "cat", "/proc/cpuinfo", "|", "grep", "'cpu MHz'")!;
|
||||
if (string.IsNullOrEmpty(output)) continue;
|
||||
|
@ -112,10 +113,10 @@ public class CpuSpeedJob(
|
|||
}
|
||||
|
||||
cpuDataListAll.AddRange(cpuDataList);
|
||||
cpuDataList.ForEach(data =>
|
||||
cpuDataList.ForEach(async data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
if (data is { Data: not null, DataType: not null }) _ = DataHelper.CheckData(server.Id, data.DataType, double.Parse(data.Data));
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -137,7 +138,7 @@ public class CpuSingleUsageJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var cpuDataListAll = new List<ServerMonitoringData>();
|
||||
foreach (var server in serverList)
|
||||
|
@ -161,9 +162,10 @@ public class CpuSingleUsageJob(
|
|||
cpuDataList.Add(singleUsage);
|
||||
}
|
||||
|
||||
cpuDataList.ForEach(data =>
|
||||
cpuDataList.ForEach(async data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ public class DiskTotalJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
//获得cpu信息
|
||||
var diskDataListAll = new List<ServerMonitoringData>();
|
||||
|
@ -58,7 +58,7 @@ public class DiskUseJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
//获得cpu信息
|
||||
var diskDataListAll = new List<ServerMonitoringData>();
|
||||
|
@ -81,7 +81,7 @@ public class DiskUseJob(
|
|||
ServerId = server.Id,
|
||||
Data = disk[1],
|
||||
DataName = $"磁盘每秒传输数-{dev}" ,
|
||||
DataType = $"diskTps-{dev}"
|
||||
DataType = $"DiskTps-{dev}"
|
||||
};
|
||||
diskDataList.Add(diskTps);
|
||||
var diskReadKb = new ServerMonitoringData
|
||||
|
@ -89,7 +89,7 @@ public class DiskUseJob(
|
|||
ServerId = server.Id,
|
||||
Data = disk[2],
|
||||
DataName = $"磁盘每秒读取数据量-{dev}" ,
|
||||
DataType = $"diskReadKB-{dev}"
|
||||
DataType = $"DiskReadKB-{dev}"
|
||||
};
|
||||
diskDataList.Add(diskReadKb);
|
||||
var diskWriteKb = new ServerMonitoringData
|
||||
|
@ -97,7 +97,7 @@ public class DiskUseJob(
|
|||
ServerId = server.Id,
|
||||
Data = disk[3],
|
||||
DataName = $"磁盘每秒写入数据量-{dev}" ,
|
||||
DataType = $"diskWriteKB-{dev}"
|
||||
DataType = $"DiskWriteKB-{dev}"
|
||||
};
|
||||
diskDataList.Add(diskWriteKb);
|
||||
var diskAwait = new ServerMonitoringData
|
||||
|
@ -105,7 +105,7 @@ public class DiskUseJob(
|
|||
ServerId = server.Id,
|
||||
Data = disk[7],
|
||||
DataName = $"磁盘平均等待时间-{dev}" ,
|
||||
DataType = $"diskAwait-{dev}"
|
||||
DataType = $"DiskAwait-{dev}"
|
||||
};
|
||||
diskDataList.Add(diskAwait);
|
||||
var diskUtil = new ServerMonitoringData
|
||||
|
@ -113,7 +113,7 @@ public class DiskUseJob(
|
|||
ServerId = server.Id,
|
||||
Data = disk[8],
|
||||
DataName = $"磁盘利用率-{dev}" ,
|
||||
DataType = $"diskUtil-{dev}"
|
||||
DataType = $"DiskUtil-{dev}"
|
||||
};
|
||||
diskDataList.Add(diskUtil);
|
||||
diskDataList.ForEach(data =>
|
||||
|
|
|
@ -18,7 +18,7 @@ public class MemoryTotalJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
// 从JobDataMap中获取参数
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ public class NetworkTotalJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var netWorkDataListAll = new List<ServerMonitoringData>();
|
||||
foreach (var server in serverList)
|
||||
|
|
|
@ -17,12 +17,12 @@ public class ProcessTotalJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var processDataListAll = new List<ServerMonitoringData>();
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
foreach (var server in serverList)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var output = await sshClient?.ExecuteCommandAsync(server.Id, "ps", "-e", "|", "wc", "-l")!;
|
||||
if (string.IsNullOrEmpty(output)) continue;
|
||||
var processDataList = new List<ServerMonitoringData>();
|
||||
|
@ -70,12 +70,12 @@ public class PhrasePatternJob(
|
|||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext);
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var processDataListAll = new List<ServerMonitoringData>();
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
foreach (var server in serverList)
|
||||
{
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
var output = await sshClient?.ExecuteCommandAsync(server.Id, "lsof", "|", "wc", "-l")!;
|
||||
if (string.IsNullOrEmpty(output)) continue;
|
||||
var count = int.Parse(output);
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
using LoongPanel_Asp.Helpers;
|
||||
using LoongPanel_Asp.Hubs;
|
||||
using LoongPanel_Asp.Models;
|
||||
using LoongPanel_Asp.Servers;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Quartz;
|
||||
|
||||
namespace LoongPanel_Asp.Jobs;
|
||||
|
||||
public class UserTotalJob(IHubContext<SessionHub> hubContext,
|
||||
IServiceProvider serviceProvider,
|
||||
ApplicationDbContext dbContext): IJob
|
||||
{
|
||||
private static int _count;
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
// 执行用户统计任务
|
||||
var dataMap = context.JobDetail.JobDataMap;
|
||||
var dataHelper = new DataHelper(dbContext,hubContext);
|
||||
var serverList = (List<ServerModel>)dataMap["executor"];
|
||||
var userDataListAll = new List<ServerMonitoringData>();
|
||||
var sshClient = serviceProvider.GetService<SshService>();
|
||||
foreach (var server in serverList)
|
||||
{
|
||||
var output = await sshClient?.ExecuteCommandAsync(server.Id, "ps -o ruser=userForLongName -eo user,pcpu,pmem,comm --sort=-pcpu | awk 'NR>1 && $1 !~ /^systemd/ {user[$1]+=$2; mem[$1]+=$3; count[$1]++; total[$1]=$2+$3} END {for (u in user) print u, user[u], mem[u]/count[u], count[u]}' | sort -k1,1r -k2,2nr")!;
|
||||
if (string.IsNullOrEmpty(output)) continue;
|
||||
var lines = output.Split("\n",StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var d = line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
|
||||
var name = d[0];
|
||||
var cpu = d[1];
|
||||
var mem = d[2];
|
||||
var command=d[3];
|
||||
var data = new ServerMonitoringData
|
||||
{
|
||||
ServerId = server.Id,
|
||||
Data = cpu,
|
||||
DataName = $"CPU使用率-{name}",
|
||||
DataType = $"CpuUsage-{name}"
|
||||
};
|
||||
userDataListAll.Add(data);
|
||||
data = new ServerMonitoringData
|
||||
{
|
||||
ServerId = server.Id,
|
||||
Data = mem,
|
||||
DataName = $"内存使用率-{name}",
|
||||
DataType = $"MemoryUsage-{name}"
|
||||
};
|
||||
userDataListAll.Add(data);
|
||||
data = new ServerMonitoringData
|
||||
{
|
||||
ServerId = server.Id,
|
||||
Data = command,
|
||||
DataName = $"用户进程数-{name}",
|
||||
DataType = $"UserProcesses-{name}"
|
||||
};
|
||||
userDataListAll.Add(data);
|
||||
}
|
||||
}
|
||||
userDataListAll.ForEach(data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
});
|
||||
_count++;
|
||||
if (_count <= 10) return;
|
||||
_count = 0;
|
||||
await dataHelper.SaveData(userDataListAll);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
|
||||
<PackageReference Include="MimeKit" Version="4.6.0"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
|
||||
<PackageReference Include="Quartz" Version="3.9.0"/>
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.9.0"/>
|
||||
|
@ -36,9 +37,12 @@
|
|||
<None Update="Configs\servers.ini">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="markdowns\templates\巡检模板1.md">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\"/>
|
||||
<Folder Include="wwwroot\public\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ActiveDebugProfile>https</ActiveDebugProfile>
|
||||
<ActiveDebugProfile>http</ActiveDebugProfile>
|
||||
<NameOfLastUsedPublishProfile>C:\Users\niyyz\RiderProjects\LoongPanel-Asp\LoongPanel-Asp\Properties\PublishProfiles\registry.hub.docker.com_zwb.pubxml</NameOfLastUsedPublishProfile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -15,7 +15,7 @@ public class ApiPermissionMiddleware(
|
|||
{
|
||||
// 获取配置中定义的公开API列表
|
||||
var publicApis = configuration["PublicApi"]?.Split(";", StringSplitOptions.RemoveEmptyEntries) ??
|
||||
new string[0];
|
||||
[];
|
||||
|
||||
// 如果请求路径在公开API列表中,则直接调用下一个中间件
|
||||
if (publicApis.Any(api => api == context.Request.Path.Value))
|
||||
|
@ -23,12 +23,17 @@ public class ApiPermissionMiddleware(
|
|||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (context.Request.Path.Value!.StartsWith("/public"))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证Token
|
||||
var payload = context.User;
|
||||
|
||||
string[] hubKeywords = { "ServerHub", "MessageHub", "SessionHub", "TermHub" };
|
||||
string[] hubKeywords = { "ServerHub", "MessageHub", "SessionHub", "TerminalHub" };
|
||||
//如果请求的地址是 (*Hub/*)
|
||||
if (hubKeywords.Any(keyword => context.Request.Path.Value!.Contains(keyword)))
|
||||
{
|
||||
|
|
|
@ -19,6 +19,12 @@ public class PermissionMiddleware(
|
|||
return;
|
||||
}
|
||||
Console.WriteLine(context.Request.Path.Value!);
|
||||
//如果访问 /public/*
|
||||
if (context.Request.Path.Value!.StartsWith("/public"))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取请求头中的Authorization信息
|
||||
var authorizationHeader = context.Request.Headers["Authorization"];
|
||||
|
|
|
@ -5,18 +5,15 @@ public class EmailModel
|
|||
public required string Email { get; set; }
|
||||
}
|
||||
|
||||
public class VerifyEmailNameModel : EmailModel
|
||||
{
|
||||
public required string UserName { get; set; }
|
||||
}
|
||||
|
||||
public class RegisterModel : EmailModel
|
||||
{
|
||||
public required string UserName { get; set; }
|
||||
public required string NickName { get; set; }
|
||||
public required string Code { get; set; }
|
||||
public required string FullName { get; set; }
|
||||
public required string Phone { get; set; }
|
||||
public required string Password { get; set; }
|
||||
public required string Position { get; set; }
|
||||
public required string Role { get; set; }
|
||||
}
|
||||
|
||||
public class LoginModel
|
||||
|
|
|
@ -80,8 +80,8 @@ builder.Services.AddCors(options =>
|
|||
policy =>
|
||||
{
|
||||
//允许全部
|
||||
policy.WithOrigins("http://localhost:3001", "http://192.168.0.13:3001", "http://192.168.0.13:3002",
|
||||
"http://192.168.0.13").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
|
||||
policy.WithOrigins("http://localhost:3001", "http://192.168.0.13:3001", "https://192.168.0.13:3001",
|
||||
"https://192.168.0.13:3000").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -107,6 +107,13 @@ builder.Services.AddQuartzServer(options =>
|
|||
});
|
||||
|
||||
builder.Services.AddScoped<SshService>();
|
||||
builder.Services.AddSingleton<SshStreamService>();
|
||||
|
||||
builder.Services.AddHostedService<Init>();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
@ -119,11 +126,10 @@ if (app.Environment.IsDevelopment())
|
|||
|
||||
|
||||
app.UseCors(myAllowSpecificOrigins);
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseMiddleware<PermissionMiddleware>();
|
||||
app.UseMiddleware<ApiPermissionMiddleware>();
|
||||
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
@ -131,4 +137,5 @@ app.UseAuthorization();
|
|||
app.MapControllers();
|
||||
|
||||
app.MapHub<SessionHub>("/SessionHub");
|
||||
app.MapHub<TerminalHub>("/TerminalHub");
|
||||
app.Run();
|
|
@ -54,8 +54,8 @@ public class SshService : IDisposable
|
|||
{
|
||||
// 确保在执行命令前连接到服务器
|
||||
if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token);
|
||||
|
||||
var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments));
|
||||
Console.WriteLine(commandString);
|
||||
using var commandResult = sshClient.RunCommand(commandString);
|
||||
output = commandResult.Result;
|
||||
if (commandResult.ExitStatus != 0) output = commandResult.Error;
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
using Renci.SshNet;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using LoongPanel_Asp.Helpers;
|
||||
using LoongPanel_Asp.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ConnectionInfo = Renci.SshNet.ConnectionInfo;
|
||||
|
||||
namespace LoongPanel_Asp.Servers;
|
||||
|
||||
public class SshStreamService : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, (SshClient Client, ShellStream Stream)> _sshStreams = new();
|
||||
private readonly IHubContext<TerminalHub> _hubContext;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private Task _readingTask;
|
||||
|
||||
public SshStreamService(IHubContext<TerminalHub> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_readingTask = Task.Run(() => StartReadingAsync(_cancellationTokenSource.Token));
|
||||
}
|
||||
public void Connect(string userId, string serverId)
|
||||
{
|
||||
// 假设 JobConfigHelper.GetServers() 返回一个包含服务器配置的列表
|
||||
var serverConfig = JobConfigHelper.GetServers().Find(x => x.Id == serverId);
|
||||
if (serverConfig == null) throw new InvalidOperationException("Server not found.");
|
||||
//从_sshStreams查找是否存在 替换
|
||||
|
||||
var host = serverConfig.Address;
|
||||
var port = serverConfig.Port;
|
||||
var username = serverConfig.Username;
|
||||
var password = serverConfig.Password;
|
||||
|
||||
var connectionInfo = new ConnectionInfo(host, port, username, new PasswordAuthenticationMethod(username, password));
|
||||
var sshClient = new SshClient(connectionInfo);
|
||||
sshClient.Connect();
|
||||
var shellStream = sshClient.CreateShellStream("xterm", 100, 40, 800, 600, 1024);
|
||||
if (_sshStreams.TryGetValue(userId, out var existing))
|
||||
{
|
||||
// 关闭旧的连接
|
||||
existing.Client.Disconnect();
|
||||
existing.Client.Dispose();
|
||||
existing.Stream.Close();
|
||||
existing.Stream.Dispose();
|
||||
}
|
||||
|
||||
_sshStreams[userId]= (sshClient, shellStream);
|
||||
}
|
||||
|
||||
|
||||
public void Write(string userId, string data)
|
||||
{
|
||||
if (!_sshStreams.TryGetValue(userId, out var sshStreamInfo)) return;
|
||||
var (_, shellStream) = sshStreamInfo;
|
||||
shellStream?.Write(data);
|
||||
}
|
||||
|
||||
public void Disconnect(string userId)
|
||||
{
|
||||
if (_sshStreams.TryRemove(userId, out var sshStreamInfo))
|
||||
{
|
||||
var (sshClient, shellStream) = sshStreamInfo;
|
||||
shellStream?.Close();
|
||||
shellStream?.Dispose();
|
||||
sshClient.Disconnect();
|
||||
sshClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
try
|
||||
{
|
||||
_readingTask.Wait();
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
ae.Handle(e => e is OperationCanceledException);
|
||||
}
|
||||
|
||||
foreach (var sshStreamInfo in _sshStreams.Values)
|
||||
{
|
||||
var (sshClient, shellStream) = sshStreamInfo;
|
||||
shellStream?.Close();
|
||||
shellStream?.Dispose();
|
||||
sshClient.Disconnect();
|
||||
sshClient.Dispose();
|
||||
}
|
||||
_sshStreams.Clear();
|
||||
}
|
||||
|
||||
private async Task StartReadingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
foreach (var (key, sshStreamInfo) in _sshStreams)
|
||||
{
|
||||
var (_, shellStream) = sshStreamInfo;
|
||||
if (shellStream is not { CanRead: true, Length: > 0 }) continue;
|
||||
var output = shellStream.Read();
|
||||
await _hubContext.Clients.Group(key).SendAsync("ReceiveMessage", output);
|
||||
}
|
||||
|
||||
// 等待一段时间再进行下一次检查
|
||||
await Task.Delay(10, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +1,11 @@
|
|||
{
|
||||
"md": [
|
||||
{
|
||||
"type": "chart",
|
||||
"chartRage": 6,
|
||||
"serverValues": [
|
||||
{
|
||||
"dataName": "CPU总使用率",
|
||||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "磁盘总使用率",
|
||||
"dataType": "DiskTotalUsage"
|
||||
}
|
||||
],
|
||||
"i": "069299c5-ceec-41cd-a1a8-b18ce0598317",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 12,
|
||||
"w": 10,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
"label": "卡片名称",
|
||||
"value": "系统信息卡片",
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"label": "卡片描述",
|
||||
"value": "这是一张默认的卡片",
|
||||
"type": "text"
|
||||
},
|
||||
"foreground": {
|
||||
"label": "卡片前景色",
|
||||
"value": "ffffff",
|
||||
"type": "color"
|
||||
},
|
||||
"color": {
|
||||
"label": "图表主色",
|
||||
"value": "002EA6",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"moved": false
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"h": 16,
|
||||
"w": 6,
|
||||
"x": 4,
|
||||
"y": 12,
|
||||
"w": 3,
|
||||
"x": 0,
|
||||
"y": 15,
|
||||
"i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c",
|
||||
"selectCard": "systemInfo",
|
||||
"moved": false
|
||||
|
@ -60,9 +13,9 @@
|
|||
{
|
||||
"type": "card",
|
||||
"h": 16,
|
||||
"w": 4,
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"w": 3,
|
||||
"x": 3,
|
||||
"y": 15,
|
||||
"i": "8104448e-6c47-455f-b635-43b3ac75d4df",
|
||||
"selectCard": "onlineUsers",
|
||||
"moved": false
|
||||
|
@ -81,10 +34,10 @@
|
|||
}
|
||||
],
|
||||
"i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8",
|
||||
"x": 6,
|
||||
"y": 44,
|
||||
"x": 3,
|
||||
"y": 47,
|
||||
"h": 16,
|
||||
"w": 4,
|
||||
"w": 3,
|
||||
"selectChart": "pie",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
|
@ -129,9 +82,9 @@
|
|||
],
|
||||
"i": "17334b32-cede-441f-9bef-772d170f1c2a",
|
||||
"x": 0,
|
||||
"y": 44,
|
||||
"y": 47,
|
||||
"h": 16,
|
||||
"w": 6,
|
||||
"w": 3,
|
||||
"selectChart": "histogram",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
|
@ -172,9 +125,9 @@
|
|||
],
|
||||
"i": "06e64544-dfb8-4a28-b777-96cb4fd70aec",
|
||||
"x": 0,
|
||||
"y": 28,
|
||||
"y": 31,
|
||||
"h": 16,
|
||||
"w": 10,
|
||||
"w": 9,
|
||||
"selectChart": "area",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
|
@ -199,61 +152,61 @@
|
|||
}
|
||||
},
|
||||
"moved": false
|
||||
},
|
||||
{
|
||||
"type": "chart",
|
||||
"chartRage": 6,
|
||||
"serverValues": [
|
||||
{
|
||||
"dataName": "CPU总使用率",
|
||||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存总使用率",
|
||||
"dataType": "MemoryTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "网络接口总体使用率",
|
||||
"dataType": "InterfaceTotalUtilizationPercentage"
|
||||
}
|
||||
],
|
||||
"i": "ae1f81ab-19dc-44ad-9120-fcdea694d5c4",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 15,
|
||||
"w": 6,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
"label": "卡片名称",
|
||||
"value": "默认卡片名称",
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"label": "卡片描述",
|
||||
"value": "这是一张默认的卡片",
|
||||
"type": "text"
|
||||
},
|
||||
"foreground": {
|
||||
"label": "卡片前景色",
|
||||
"value": "ffffff",
|
||||
"type": "color"
|
||||
},
|
||||
"color": {
|
||||
"label": "图表主色",
|
||||
"value": "002EA6",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"moved": false
|
||||
}
|
||||
],
|
||||
"lg": [
|
||||
{
|
||||
"type": "chart",
|
||||
"chartRage": 6,
|
||||
"serverValues": [
|
||||
{
|
||||
"dataName": "CPU总使用率",
|
||||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "磁盘总使用率",
|
||||
"dataType": "DiskTotalUsage"
|
||||
}
|
||||
],
|
||||
"i": "069299c5-ceec-41cd-a1a8-b18ce0598317",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 13,
|
||||
"w": 10,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
"label": "卡片名称",
|
||||
"value": "系统信息卡片",
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"label": "卡片描述",
|
||||
"value": "这是一张默认的卡片",
|
||||
"type": "text"
|
||||
},
|
||||
"foreground": {
|
||||
"label": "卡片前景色",
|
||||
"value": "ffffff",
|
||||
"type": "color"
|
||||
},
|
||||
"color": {
|
||||
"label": "图表主色",
|
||||
"value": "002EA6",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"moved": false
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"h": 13,
|
||||
"w": 2,
|
||||
"x": 10,
|
||||
"w": 3,
|
||||
"x": 9,
|
||||
"y": 0,
|
||||
"i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c",
|
||||
"selectCard": "systemInfo",
|
||||
|
@ -321,8 +274,8 @@
|
|||
"dataType": "DiskTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
"dataName": "内存总使用率",
|
||||
"dataType": "MemoryTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "CPU总使用率",
|
||||
|
@ -401,9 +354,7 @@
|
|||
}
|
||||
},
|
||||
"moved": false
|
||||
}
|
||||
],
|
||||
"xl": [
|
||||
},
|
||||
{
|
||||
"type": "chart",
|
||||
"chartRage": 6,
|
||||
|
@ -413,24 +364,24 @@
|
|||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
"dataName": "内存总使用率",
|
||||
"dataType": "MemoryTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "磁盘总使用率",
|
||||
"dataType": "DiskTotalUsage"
|
||||
"dataName": "网络接口总体使用率",
|
||||
"dataType": "InterfaceTotalUtilizationPercentage"
|
||||
}
|
||||
],
|
||||
"i": "069299c5-ceec-41cd-a1a8-b18ce0598317",
|
||||
"i": "ae1f81ab-19dc-44ad-9120-fcdea694d5c4",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 12,
|
||||
"h": 13,
|
||||
"w": 9,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
"label": "卡片名称",
|
||||
"value": "系统信息卡片",
|
||||
"value": "默认卡片名称",
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
|
@ -450,7 +401,9 @@
|
|||
}
|
||||
},
|
||||
"moved": false
|
||||
},
|
||||
}
|
||||
],
|
||||
"xl": [
|
||||
{
|
||||
"type": "card",
|
||||
"h": 15,
|
||||
|
@ -603,9 +556,7 @@
|
|||
}
|
||||
},
|
||||
"moved": false
|
||||
}
|
||||
],
|
||||
"sm": [
|
||||
},
|
||||
{
|
||||
"type": "chart",
|
||||
"chartRage": 6,
|
||||
|
@ -615,24 +566,24 @@
|
|||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
"dataName": "内存总使用率",
|
||||
"dataType": "MemoryTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "磁盘总使用率",
|
||||
"dataType": "DiskTotalUsage"
|
||||
"dataName": "网络接口总体使用率",
|
||||
"dataType": "InterfaceTotalUtilizationPercentage"
|
||||
}
|
||||
],
|
||||
"i": "069299c5-ceec-41cd-a1a8-b18ce0598317",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 12,
|
||||
"w": 9,
|
||||
"i": "ae1f81ab-19dc-44ad-9120-fcdea694d5c4",
|
||||
"x": 10,
|
||||
"y": 29,
|
||||
"h": 5,
|
||||
"w": 5,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
"label": "卡片名称",
|
||||
"value": "系统信息卡片",
|
||||
"value": "默认卡片名称",
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
|
@ -652,13 +603,15 @@
|
|||
}
|
||||
},
|
||||
"moved": false
|
||||
},
|
||||
}
|
||||
],
|
||||
"sm": [
|
||||
{
|
||||
"type": "card",
|
||||
"h": 16,
|
||||
"w": 3,
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"y": 15,
|
||||
"i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c",
|
||||
"selectCard": "systemInfo",
|
||||
"moved": false
|
||||
|
@ -668,7 +621,7 @@
|
|||
"h": 16,
|
||||
"w": 3,
|
||||
"x": 3,
|
||||
"y": 12,
|
||||
"y": 15,
|
||||
"i": "8104448e-6c47-455f-b635-43b3ac75d4df",
|
||||
"selectCard": "onlineUsers",
|
||||
"moved": false
|
||||
|
@ -688,7 +641,7 @@
|
|||
],
|
||||
"i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8",
|
||||
"x": 3,
|
||||
"y": 44,
|
||||
"y": 47,
|
||||
"h": 16,
|
||||
"w": 3,
|
||||
"selectChart": "pie",
|
||||
|
@ -735,7 +688,7 @@
|
|||
],
|
||||
"i": "17334b32-cede-441f-9bef-772d170f1c2a",
|
||||
"x": 0,
|
||||
"y": 44,
|
||||
"y": 47,
|
||||
"h": 16,
|
||||
"w": 3,
|
||||
"selectChart": "histogram",
|
||||
|
@ -778,7 +731,7 @@
|
|||
],
|
||||
"i": "06e64544-dfb8-4a28-b777-96cb4fd70aec",
|
||||
"x": 0,
|
||||
"y": 28,
|
||||
"y": 31,
|
||||
"h": 16,
|
||||
"w": 9,
|
||||
"selectChart": "area",
|
||||
|
@ -805,6 +758,53 @@
|
|||
}
|
||||
},
|
||||
"moved": false
|
||||
},
|
||||
{
|
||||
"type": "chart",
|
||||
"chartRage": 6,
|
||||
"serverValues": [
|
||||
{
|
||||
"dataName": "CPU总使用率",
|
||||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存总使用率",
|
||||
"dataType": "MemoryTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "网络接口总体使用率",
|
||||
"dataType": "InterfaceTotalUtilizationPercentage"
|
||||
}
|
||||
],
|
||||
"i": "ae1f81ab-19dc-44ad-9120-fcdea694d5c4",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 15,
|
||||
"w": 6,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
"label": "卡片名称",
|
||||
"value": "默认卡片名称",
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"label": "卡片描述",
|
||||
"value": "这是一张默认的卡片",
|
||||
"type": "text"
|
||||
},
|
||||
"foreground": {
|
||||
"label": "卡片前景色",
|
||||
"value": "ffffff",
|
||||
"type": "color"
|
||||
},
|
||||
"color": {
|
||||
"label": "图表主色",
|
||||
"value": "002EA6",
|
||||
"type": "color"
|
||||
}
|
||||
},
|
||||
"moved": false
|
||||
}
|
||||
],
|
||||
"xs": [
|
||||
|
@ -816,10 +816,6 @@
|
|||
"dataName": "CPU总使用率",
|
||||
"dataType": "CpuTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "磁盘总使用率",
|
||||
"dataType": "DiskTotalUsage"
|
||||
|
@ -827,8 +823,8 @@
|
|||
],
|
||||
"i": "069299c5-ceec-41cd-a1a8-b18ce0598317",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 12,
|
||||
"y": 45,
|
||||
"h": 13,
|
||||
"w": 9,
|
||||
"selectChart": "line",
|
||||
"cardConfig": {
|
||||
|
@ -857,10 +853,10 @@
|
|||
},
|
||||
{
|
||||
"type": "card",
|
||||
"h": 16,
|
||||
"w": 2,
|
||||
"x": 2,
|
||||
"y": 12,
|
||||
"h": 13,
|
||||
"w": 4,
|
||||
"x": 0,
|
||||
"y": 32,
|
||||
"i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c",
|
||||
"selectCard": "systemInfo",
|
||||
"moved": false
|
||||
|
@ -868,9 +864,9 @@
|
|||
{
|
||||
"type": "card",
|
||||
"h": 16,
|
||||
"w": 2,
|
||||
"w": 4,
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"y": 16,
|
||||
"i": "8104448e-6c47-455f-b635-43b3ac75d4df",
|
||||
"selectCard": "onlineUsers",
|
||||
"moved": false
|
||||
|
@ -889,8 +885,8 @@
|
|||
}
|
||||
],
|
||||
"i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8",
|
||||
"x": 3,
|
||||
"y": 59,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"h": 16,
|
||||
"w": 4,
|
||||
"selectChart": "pie",
|
||||
|
@ -927,8 +923,8 @@
|
|||
"dataType": "DiskTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "内存使用率",
|
||||
"dataType": "MemoryUsage"
|
||||
"dataName": "内存总使用率",
|
||||
"dataType": "MemoryTotalUsage"
|
||||
},
|
||||
{
|
||||
"dataName": "CPU总使用率",
|
||||
|
@ -937,9 +933,9 @@
|
|||
],
|
||||
"i": "17334b32-cede-441f-9bef-772d170f1c2a",
|
||||
"x": 0,
|
||||
"y": 44,
|
||||
"h": 15,
|
||||
"w": 4,
|
||||
"y": 74,
|
||||
"h": 16,
|
||||
"w": 9,
|
||||
"selectChart": "histogram",
|
||||
"cardConfig": {
|
||||
"name": {
|
||||
|
@ -980,7 +976,7 @@
|
|||
],
|
||||
"i": "06e64544-dfb8-4a28-b777-96cb4fd70aec",
|
||||
"x": 0,
|
||||
"y": 28,
|
||||
"y": 58,
|
||||
"h": 16,
|
||||
"w": 9,
|
||||
"selectChart": "area",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "None"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
@ -19,5 +20,17 @@
|
|||
"Secret": "p4Qzf/+GPP/XNLalZGCzwlelOl6skiFZscj6iZ6rZZE=",
|
||||
"Issuer": "LoongPanel",
|
||||
"Audience": "LoongPanel",
|
||||
"PubLicApi": "/Api/Account/SendVerificationCode;/Api/Account/Register;/Api/Account/Login;/Api/Account/VerifyEmailName;"
|
||||
"PubLicApi": "/Api/Account/Login",
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"MyHttpsEndpoint": {
|
||||
"Url": "https://192.168.0.13:7233",
|
||||
"ClientCertificateMode": "AllowCertificate",
|
||||
"Certificate": {
|
||||
"Path": "./my.pfx",
|
||||
"Password": "z1377952468zz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# 巡检记录
|
||||
|
||||
## 第一部分:基本信息
|
||||
|
||||
- **巡检日期**:2023 年 11 月 8 日
|
||||
- **巡检人员**:张三
|
||||
- **巡检部门**:设备维护部
|
||||
|
||||
## 第二部分:巡检项目
|
||||
|
||||
| 序号 | 巡检项目名称 | 巡检要点 | 巡检内容 |
|
||||
| ---- | ------------ | -------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| 1 | 设备运行状况 | 检查设备运行是否平稳,有无异常振动或噪音。 | 详细记录设备运行参数,比较历史数据,分析是否存在异常。 |
|
||||
| 2 | 安全防护措施 | 检查安全防护设施是否完好,如安全栅栏、警示标志等。 | 确认所有安全设施无损坏,位置正确,且工作人员了解如何正确使用。 |
|
||||
| 3 | 环境卫生状况 | 检查工作区域是否清洁,有无垃圾或障碍物。 | 清理工作区域,确保无杂物,保持环境整洁。 |
|
||||
|...|.....|....|...|
|
||||
|
||||
## 第三部分:检查记录
|
||||
|
||||
| 序号 | 检查项目 | 检查结果 | 异常说明 | 处理措施 | 反馈意见 |
|
||||
| ---- | -------- | -------- | ---------------- | ------------------------------ | ---------------------- |
|
||||
| 1 | 外壳 | 正常 | 无 | 无 | 无 |
|
||||
| 2 | 电源 | 异常 | 设备有轻微振动。 | 已联系维修人员,计划明日检修。 | 建议增加设备维护频率。 |
|
||||
| 3 | 主机 | 正常 | 无 | 无 | 无 |
|
||||
|...|.....|....|...|...|...|
|
||||
|
||||
## 第四部分:巡检总结
|
||||
|
||||
- **巡检总体评价**:本次巡检总体情况良好,除设备 2 存在轻微振动外,其他各项指标均正常。
|
||||
- **重点问题处理**:设备 2 的振动问题已安排维修人员进行检修,预计明日完成。
|
||||
- **后续工作建议**:建议加强设备日常维护,特别是对老旧设备进行更频繁的检查,以防止潜在的安全隐患。
|
Binary file not shown.
2
web/.env
2
web/.env
|
@ -1 +1 @@
|
|||
API_SERVER="http://192.168.0.13:5253"
|
||||
API_SERVER="https://192.168.0.13:7233"
|
|
@ -15,4 +15,5 @@
|
|||
:root {
|
||||
--primary-color: $primary-color
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -19,6 +19,8 @@ $light-unfocused-color: $unfocused-color;
|
|||
$dark-text-color: #D3D3D3;
|
||||
$dark-unfocused-color: $unfocused-color;
|
||||
|
||||
$border:1px solid rgba(51, 51, 51, 0.17);
|
||||
|
||||
$gap: 8px;
|
||||
$padding: 16px;
|
||||
$radius: 8px;
|
||||
|
|
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import {cards} from "~/config/cards";
|
||||
import {type GridCardItem, useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {type IGridItem, useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import ICard from "~/components/Cards/ICard.vue";
|
||||
|
||||
|
@ -14,7 +14,7 @@ const props = defineProps({
|
|||
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const addCard = (selectCardId: string) => {
|
||||
const newCard: GridCardItem = {
|
||||
const newCard: IGridItem = {
|
||||
type: "card",
|
||||
h: 5, w: 5, x: 1, y: 1,
|
||||
i: uuidv4(),
|
||||
|
|
|
@ -54,7 +54,7 @@ onBeforeMount(() => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius*2;
|
||||
border-radius: $radius;
|
||||
padding-bottom: $padding;
|
||||
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
|
||||
position: relative;
|
||||
|
@ -62,7 +62,7 @@ onBeforeMount(() => {
|
|||
flex-direction: column;
|
||||
gap: $gap*1.5;
|
||||
will-change: scroll-position, contents;
|
||||
|
||||
border: $border;
|
||||
.dark-mode & {
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@ const items = [
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius*2;
|
||||
border-radius: $radius;
|
||||
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
|
||||
padding: $padding*1.5;
|
||||
position: relative;
|
||||
|
@ -119,7 +119,7 @@ const items = [
|
|||
flex-direction: column;
|
||||
gap: $gap*1.5;
|
||||
will-change: scroll-position, contents;
|
||||
|
||||
border: $border;
|
||||
.dark-mode & {
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ const updateProgress = (value: number) => {
|
|||
$gsap.to("#" + props.title + "Arrow", {
|
||||
duration: 0.4,
|
||||
ease: 'bounce.inOut',
|
||||
rotation: 360 * (value / 100) - 30,
|
||||
rotation: 360 * (value / 100) - 40,
|
||||
transformOrigin: "center center"
|
||||
})
|
||||
let color;
|
||||
|
@ -93,6 +93,13 @@ const updateProgress = (value: number) => {
|
|||
});
|
||||
}
|
||||
}
|
||||
onMounted(()=>{
|
||||
updateProgress(0)
|
||||
//从datStore中读取数据
|
||||
nextTick(()=>{
|
||||
updateProgress(Number(dataStore.data[props.watcher]))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -129,9 +136,10 @@ const updateProgress = (value: number) => {
|
|||
align-items: center;
|
||||
background: $light-bg-color;
|
||||
padding: $padding*1.5;
|
||||
border-radius: $radius*2;
|
||||
border-radius: $radius;
|
||||
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
|
||||
gap: $gap*3;
|
||||
border: $border;
|
||||
|
||||
> svg {
|
||||
width: 68px;
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
<script lang="ts" setup>
|
||||
import VChart from 'vue-echarts';
|
||||
import {useDataStore} from "~/strores/DataStore";
|
||||
import _ from "lodash";
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import type {PropType} from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const dataStore = useDataStore()
|
||||
const isLoading = ref(false)
|
||||
const chartRef = ref<any>(null);
|
||||
|
||||
const props = defineProps({
|
||||
valueIds: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
valueNames: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
colors: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
configs: {
|
||||
type: Array as PropType<Array<object>>,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
min: {
|
||||
type: [Number, null],
|
||||
default: () => {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: [Number, null],
|
||||
default: () => {
|
||||
return 100
|
||||
}
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
})
|
||||
const option = computed(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '30',
|
||||
show: true,
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 90,
|
||||
end: 100,
|
||||
throttle: 50, // 设置触发视图刷新的频率
|
||||
minValueSpan: 10, // 设置最小值跨度,用于控制坐标轴标签的显示
|
||||
},
|
||||
],
|
||||
legend: {
|
||||
//嵌入式左上角
|
||||
orient: 'vertical',
|
||||
right: '30',
|
||||
//向下30px
|
||||
top: '50',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
position: 'top',
|
||||
boundaryGap: false,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: [props.bgColor],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 10,
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: [props.bgColor],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
},
|
||||
series: props.valueNames?.map((id, index) => {
|
||||
return {
|
||||
name: id,
|
||||
type: 'line',
|
||||
large: true,
|
||||
smooth: true,
|
||||
color: [props.colors[index] ?? props.colors[0]],
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
...props.configs[index],
|
||||
data: [],
|
||||
}
|
||||
}) ?? []
|
||||
}
|
||||
})
|
||||
|
||||
let interval: NodeJS.Timeout;
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval);
|
||||
})
|
||||
onMounted(() => {
|
||||
let history = dataStore.dataHistory;
|
||||
nextTick(() => {
|
||||
props.valueIds?.forEach((id, index) => {
|
||||
chartRef.value.appendData({
|
||||
seriesIndex: index,
|
||||
data: history.data[id]
|
||||
})
|
||||
})
|
||||
const currentOption = chartRef.value.getOption();
|
||||
currentOption.xAxis[0].data = history.times
|
||||
chartRef.value.setOption(currentOption)
|
||||
isLoading.value = false
|
||||
})
|
||||
interval = setInterval(() => {
|
||||
const data = dataStore.data
|
||||
props.valueIds?.forEach((id, index) => {
|
||||
const newData = data[id] ?? 0
|
||||
chartRef.value.appendData({
|
||||
seriesIndex: index,
|
||||
data: [newData]
|
||||
})
|
||||
})
|
||||
const currentOption = chartRef.value.getOption();
|
||||
currentOption.xAxis[0].data.push(dayjs().format('MM-DD HH:mm:ss'))
|
||||
chartRef.value.setOption(currentOption)
|
||||
}, 1000)
|
||||
})
|
||||
let endIndex = dataStore.dataHistory.times ? dataStore.dataHistory.times.length : 0
|
||||
let startTemp = 0;
|
||||
let done = false;
|
||||
type Data = {
|
||||
times: string[]; // 时间轴
|
||||
data: { [key: string]: string[] }; // 数据,键是字符串,值是字符串数组的数组
|
||||
endIndex: number; // 结束索引
|
||||
done: boolean; // 是否完成加载
|
||||
};
|
||||
const zoom = _.throttle((e: any) => {
|
||||
let start = e.start ?? e.batch[0].start
|
||||
if (start <= 50 && start !== startTemp) {
|
||||
if (done) return
|
||||
startTemp = e.start
|
||||
isLoading.value = true
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
}, 5000)
|
||||
$fetch('/Api/Server/GetServerHistoryDate', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + useCookie('token').value
|
||||
},
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataTypes: props.valueIds,
|
||||
StartIndex: endIndex,
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
}).then((res) => {
|
||||
console.log(res)
|
||||
const data = res as Data;
|
||||
if (data.done) {
|
||||
done = true
|
||||
return
|
||||
}
|
||||
endIndex = data.endIndex
|
||||
//获得图表data
|
||||
const currentOption = chartRef.value.getOption();
|
||||
currentOption.series.map((series: any, index: number) => {
|
||||
series.data = [...data.data[props.valueIds[index]], ...series.data]
|
||||
})
|
||||
currentOption.xAxis[0].data = [...data.times, ...currentOption.xAxis[0].data]
|
||||
//start 为5
|
||||
currentOption.dataZoom[0].start = 55
|
||||
chartRef.value.setOption(currentOption)
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-chart ref="chartRef" :loading="isLoading" :manual-update="true"
|
||||
:option="option"
|
||||
autoresize
|
||||
class="chart"
|
||||
@datazoom="zoom"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.chart {
|
||||
will-change: contents, scroll-position;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,56 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" fill="none">
|
||||
<g filter="url(#filter0_ii_1_166)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M55 20C46.1634 20 39 27.1634 39 36V204C39 212.837 46.1634 220 55 220H185C193.837 220 201 212.837 201 204V70L151 20H55Z" fill="#4876F9"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dd_1_166)">
|
||||
<path d="M104.847 170L119.908 114.791H120.185L135.152 170H143.56L162.5 103.75H154L139.495 159.33H139.125L124.158 103.75H115.935L100.875 159.33H100.505L86 103.75H77.5L96.44 170H104.847Z" fill="white"/>
|
||||
</g>
|
||||
<path d="M196.309 65.3125H155.688L201 110.625V70L196.309 65.3125Z" fill="url(#paint0_linear_1_166)"/>
|
||||
<path d="M167 70L201 70L151 20L151 54C151 62.8366 158.163 70 167 70Z" fill="#B5C8FC"/>
|
||||
<defs>
|
||||
<filter id="filter0_ii_1_166" x="39" y="19" width="162" height="202" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1_166"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_1_166" result="effect2_innerShadow_1_166"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd_1_166" x="76.5" y="102.75" width="94" height="77.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="4" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_166"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1_166" result="effect2_dropShadow_1_166"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1_166" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1_166" x1="165.844" y1="55.1563" x2="211.156" y2="100.469" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-opacity="0.2"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
|
@ -0,0 +1,175 @@
|
|||
<script lang="ts" setup>
|
||||
import {MdEditor,MdPreview} from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {useToast} from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props=defineProps({
|
||||
wordId:{
|
||||
type:String,
|
||||
default:null
|
||||
},
|
||||
preview:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
},
|
||||
template:{
|
||||
type:String,
|
||||
default: `# ${dayjs().format()}`
|
||||
}
|
||||
})
|
||||
const text = ref<string>(props.template);
|
||||
const mainLayoutStore=useMainLayoutStore()
|
||||
const toast=useToast()
|
||||
const filenameVisible=ref(false)
|
||||
|
||||
let id = uuidv4();
|
||||
const fileName = ref<string|null>(null);
|
||||
const saveFileName=()=>{
|
||||
if(fileName.value===null||fileName.value===''){
|
||||
toast.error('文件名不能为空')
|
||||
if(fileName.value!==null&&fileName.value.length<=6){
|
||||
toast.error('文件名不能小于6位')
|
||||
}
|
||||
return
|
||||
}
|
||||
filenameVisible.value=false
|
||||
uploadFile()
|
||||
}
|
||||
const onUploadImg = async (files: any[], callback: Function) => {
|
||||
const res = await Promise.all(
|
||||
files.map((file) => {
|
||||
return new Promise((rev, rej) => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
$fetch('/Api/PublicFile/UploadImage', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
}).then(
|
||||
|
||||
(res:any) => {
|
||||
toast.success('上传成功')
|
||||
rev(`${useRuntimeConfig().public.baseUrl}${res.fileUrl}`)
|
||||
},
|
||||
).catch((err) => {
|
||||
toast.error('上传失败'+err)
|
||||
rej(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
callback(res.map((item) => item));
|
||||
};
|
||||
const onSave = (v:string, _:string) => {
|
||||
if(fileName.value===null||fileName.value===''&&fileName.value.length<=6){
|
||||
filenameVisible.value=true
|
||||
return
|
||||
}
|
||||
uploadFile()
|
||||
};
|
||||
const uploadFile=()=>{
|
||||
$fetch('/Api/Server/UpLoadWord', {
|
||||
method: 'POST',
|
||||
params:{
|
||||
serverId: mainLayoutStore.SelectServer.id,
|
||||
userName:mainLayoutStore.UserInfo.userName,
|
||||
wordId: id,
|
||||
},
|
||||
body: {
|
||||
name: fileName.value,
|
||||
content: text.value
|
||||
},
|
||||
headers: {
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
}).then(
|
||||
(res:any) => {
|
||||
toast.success(res,{
|
||||
timeout:1000
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
onBeforeMount(()=>{
|
||||
if(props.wordId){
|
||||
id=props.wordId
|
||||
$fetch('/Api/Server/GetWordContent', {
|
||||
method: 'GET',
|
||||
params:{
|
||||
serverId: mainLayoutStore.SelectServer.id,
|
||||
wordId: props.wordId,
|
||||
},
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
}).then(
|
||||
(res:any) =>{
|
||||
text.value=res.content
|
||||
fileName.value=res.wordName
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor-layout">
|
||||
<Dialog
|
||||
v-model:visible="filenameVisible"
|
||||
modal
|
||||
header="输入文件名"
|
||||
>
|
||||
<div class="fileName">
|
||||
<input v-model="fileName">
|
||||
<Button label="保存" @click="saveFileName" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<MdEditor v-model="text" v-if="!preview" :theme="$colorMode.value as 'light'| 'dark'" class="editor" codeTheme="kimbie"
|
||||
@onSave="onSave" @onUploadImg="onUploadImg" />
|
||||
<MdPreview :modelValue="text" v-else class="editor-preview"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "base";
|
||||
|
||||
.editor-layout {
|
||||
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.editor {
|
||||
width: 80vw;
|
||||
height: 100%;
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
}
|
||||
.editor-preview{
|
||||
border-radius: $radius;
|
||||
width: 40vw;
|
||||
min-width: 1000px;
|
||||
}
|
||||
.fileName{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
input{
|
||||
width: 70%;
|
||||
border: $border;
|
||||
background: rgba(51, 51, 51, 0.1);
|
||||
border-radius: $radius;
|
||||
padding: $padding;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,137 @@
|
|||
<script setup lang="ts">
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {useToast} from "vue-toastification";
|
||||
|
||||
const toast = useToast()
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const userList = ref<UserListItem[]>([])
|
||||
type UserListItem = {
|
||||
"name": string,
|
||||
"isOnline": boolean,
|
||||
"lastLoginTime": string,
|
||||
"address": string,
|
||||
"port": string,
|
||||
}
|
||||
const GetServerUserList = () => {
|
||||
$fetch('/Api/Server/GetServerUserList', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res => {
|
||||
userList.value=res as UserListItem[];
|
||||
}).catch(err => {
|
||||
toast.error(err)
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
GetServerUserList()
|
||||
})
|
||||
const refresh = () => {
|
||||
userList.value=[]
|
||||
GetServerUserList()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-list-layout">
|
||||
<div class="user-list-header">
|
||||
<Icon name="ArrowDownUp" :size="24" :stroke-width="1.5"></Icon>
|
||||
<h3>用户列表</h3>
|
||||
<Icon name="Repeat2" :size="24" :stroke-width="1.5" @click="refresh"/>
|
||||
</div>
|
||||
<div class="user-list-body">
|
||||
<div class="user-list-item" @click="navigateTo(`/serverUser/all`)">
|
||||
<div class="avatar">
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" :image="`https://api.multiavatar.com/all.svg`"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="top">
|
||||
<h3>全部</h3>
|
||||
</div>
|
||||
<p>NULL</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-list-item" v-for="item in userList" @click="navigateTo(`/serverUser/${item.name}`)">
|
||||
<div class="avatar">
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" :image="`https://api.multiavatar.com/${item.name}.svg`"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="top">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<div class="tags">
|
||||
<Tag v-if="item.isOnline">在线</Tag>
|
||||
<Tag v-if="item.isOnline">{{item.port}}</Tag>
|
||||
<Tag v-if="item.isOnline">{{item.address}}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{item.lastLoginTime}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
|
||||
.user-list-layout {
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
padding: $padding 0;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
|
||||
.user-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 $padding;
|
||||
justify-content: space-between;
|
||||
|
||||
svg {
|
||||
color: rgba(51, 51, 51, 0.34);
|
||||
stroke: rgba(51, 51, 51, 0.44);
|
||||
|
||||
&:hover {
|
||||
stroke: rgba(51, 51, 51, 0.64);
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-list-body{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
padding: $padding*2 $padding;
|
||||
}
|
||||
.user-list-item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $gap;
|
||||
padding: 8px;
|
||||
border-radius: $radius;
|
||||
&:hover{
|
||||
background: rgba(51, 51, 51, 0.06);
|
||||
}
|
||||
.info{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex:1;
|
||||
gap: $gap;
|
||||
.top{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.tags{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,46 +1,83 @@
|
|||
<script lang="ts" setup>
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {HubConnectionBuilder} from "@microsoft/signalr";
|
||||
import {FitAddon} from 'xterm-addon-fit'
|
||||
//导入xtrem
|
||||
import {Terminal} from "@xterm/xterm";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
var term: Terminal;
|
||||
onMounted(()=>{
|
||||
term = new Terminal({rows:40,cols:100,smoothScrollDuration:100,scrollback:1000});
|
||||
term.open(document.getElementById("terminal") as HTMLElement);
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
// fitAddon.fit();
|
||||
term.focus();
|
||||
//绑定输入事件
|
||||
term.onData((data) => {
|
||||
console.log(data)
|
||||
connection.invoke("SendMessage", data)
|
||||
.catch(err => {
|
||||
console.log("Error while sending message: " + err);
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const path = ref<string>('')
|
||||
onBeforeMount(() => {
|
||||
$fetch('/Api/Server/GetServerTerminalPath', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
path.value = res
|
||||
})
|
||||
const connection = new HubConnectionBuilder()
|
||||
.withUrl(`${useRuntimeConfig().public.baseUrl}/TerminalHub`)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
onMounted(async () => {
|
||||
connection.on("ReceiveMessage", (message) => {
|
||||
term.write(message)
|
||||
})
|
||||
await connection.start()
|
||||
.then(() => {
|
||||
console.log("Connection started");
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Error while starting connection: " + err);
|
||||
});
|
||||
await connection.invoke("CreateTerminal", mainLayoutStore.SelectServer.id)
|
||||
.then(() => {
|
||||
console.log("Terminal created")
|
||||
})
|
||||
setTimeout(()=>{
|
||||
connection.invoke("SendMessage","neofetch\n")
|
||||
},200)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="terminal">
|
||||
<iframe :src="path"></iframe>
|
||||
<div class="terminal-box">
|
||||
<div id="terminal">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "base";
|
||||
|
||||
#terminal {
|
||||
min-width: 60vw;
|
||||
min-height: 60vh;
|
||||
.terminal-box {
|
||||
width: 100%;
|
||||
padding: $padding;
|
||||
background: #000;
|
||||
border-radius: $radius*2;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
border: none;
|
||||
#terminal {
|
||||
&::-webkit-scrollbar{
|
||||
width:0;
|
||||
}
|
||||
}
|
||||
:deep(.xterm-viewport::-webkit-scrollbar){
|
||||
width: 0;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -79,12 +79,12 @@ const isOnline = computed(() => {
|
|||
<p>{{ physicalAddress }}</p>
|
||||
</div>
|
||||
<div class="user-item-box">
|
||||
<h4 v-tooltip=" lastLoginTime">{{ lastLoginTime.split(' ')[0] }}</h4>
|
||||
<h4 v-tooltip="lastLoginTime">{{ lastLoginTime?.split(' ')[0] }}</h4>
|
||||
<p>{{ loginIp }}</p>
|
||||
</div>
|
||||
<div class="user-item-box">
|
||||
<h4 v-tooltip="modifyTime">{{ modifyTime.split(' ')[0] }}</h4>
|
||||
<p v-tooltip="createTime">{{ createTime.split(' ')[0] }}</p>
|
||||
<h4 v-tooltip="modifyTime">{{ modifyTime?.split(' ')[0] }}</h4>
|
||||
<p v-tooltip="createTime">{{ createTime?.split(' ')[0] }}</p>
|
||||
</div>
|
||||
<div class="user-item-box end">
|
||||
<h4 :style="{
|
||||
|
@ -93,7 +93,7 @@ const isOnline = computed(() => {
|
|||
<p>{{ isLock ? '锁定' : '' }}</p>
|
||||
</div>
|
||||
<div class="user-end">
|
||||
<button>
|
||||
<button @click="navigateTo(`userInfo/${userId}`)">
|
||||
<p>查看更多</p>
|
||||
<Icon name="ChevronRight"></Icon>
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
<script setup lang="ts">
|
||||
import {watchDebounced} from '@vueuse/core'
|
||||
import * as Yup from "yup";
|
||||
import {useToast} from "vue-toastification";
|
||||
import type {UserInfoListType} from "~/types/UserType";
|
||||
const toast = useToast()
|
||||
const avatarImgUrl = ref("")
|
||||
const errors = ref<string[]>([]);
|
||||
const props=defineProps({
|
||||
closeBack: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
//随机生成密码
|
||||
const randomPassword = () => {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+';
|
||||
let password = '';
|
||||
for (let i = 0; i < 24; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return password
|
||||
}
|
||||
const form = reactive({
|
||||
fullName: '',
|
||||
userName: '',
|
||||
position: '',
|
||||
email: 'examp@examp.com',
|
||||
phone: '1888888888',
|
||||
password: randomPassword().toString(),
|
||||
role: 'user',
|
||||
});
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
fullName: Yup.string().required('请输入姓名'),
|
||||
userName: Yup.string().required('请输入用户名').matches(/^[A-Za-z0-9_]+$/,"用户名必须为英文,且只能包含英文字母、数字和下划线"),
|
||||
position: Yup.string().required('请输入职位'),
|
||||
email: Yup.string().email('请输入正确的邮箱').required('请输入邮箱'),
|
||||
phone: Yup.string().required('请输入手机号'),
|
||||
password: Yup.string().required('请输入密码').min(6, '密码至少需要6个字符')
|
||||
.matches(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^&*()_+])[0-9a-zA-Z!@#$%^&*()_+]{6,}/, '密码必须包含字母、数字和至少一个特殊字符'),
|
||||
role: Yup.string().required('请选择权限'),
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
() => form.userName,
|
||||
() => {
|
||||
avatarImgUrl.value = `https://api.multiavatar.com/${form.userName}.svg`
|
||||
},
|
||||
{debounce: 2000, maxWait: 4000},
|
||||
)
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
await schema.validate(form, {abortEarly: false});
|
||||
errors.value = []; // 清空错误信息
|
||||
// 执行创建用户的逻辑
|
||||
$fetch('Api/Account/Register', {
|
||||
method: "POST",
|
||||
body: form,
|
||||
headers:{
|
||||
"Authorization": "Bearer " + useCookie("token").value
|
||||
},
|
||||
baseURL:useRuntimeConfig().public.baseUrl
|
||||
}).then(res => {
|
||||
toast.success(res)
|
||||
//调用copy
|
||||
const { text, copy, copied, isSupported } = useClipboard({
|
||||
source: JSON.stringify(form),
|
||||
copiedDuring: 2000,
|
||||
})
|
||||
if(!isSupported.value){
|
||||
toast.error('浏览器不支持复制')
|
||||
setTimeout(()=>{
|
||||
props.closeBack()
|
||||
return
|
||||
},2000)
|
||||
}
|
||||
copy()
|
||||
toast.success('账号已复制到剪贴板')
|
||||
//检查是否拷贝成功
|
||||
if (!copied.value) {
|
||||
toast.error('复制失败')
|
||||
}
|
||||
setTimeout(()=>{
|
||||
props.closeBack()
|
||||
},2000)
|
||||
}).catch(
|
||||
err => {
|
||||
toast.error(err)
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
// 处理验证错误
|
||||
if (error instanceof Yup.ValidationError) {
|
||||
errors.value = error.inner.map(e =>{
|
||||
toast.error(e.message)
|
||||
return e.message
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="create-user-layout">
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" :image="avatarImgUrl"/>
|
||||
<div class="header">
|
||||
<Icon name="User" :stroke-width="2" :size="32"></Icon>
|
||||
<h2>创建新的账户</h2>
|
||||
</div>
|
||||
<form class="form" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<input placeholder="用户名" type="text" v-model="form.userName"/>
|
||||
<input placeholder="姓名" type="text" v-model="form.fullName"/>
|
||||
</div>
|
||||
<div>
|
||||
<input placeholder="职位" type="text" v-model="form.position"/>
|
||||
<select v-model="form.role">>
|
||||
<option value="user">普通运维人员</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<input placeholder="邮箱" type="email" v-model="form.email"/>
|
||||
<input placeholder="手机号" type="text" v-model="form.phone"/>
|
||||
<input placeholder="密码" type="text" v-model="form.password"/>
|
||||
<div class="active">
|
||||
<button @click.prevent="closeBack()">取消</button>
|
||||
<button type="submit">创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
|
||||
.create-user-layout {
|
||||
background: $light-bg-color;
|
||||
width: 400px;
|
||||
min-height: 600px;
|
||||
border-radius: $radius;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $gap*4;
|
||||
flex-direction: column;
|
||||
padding: $padding*6 $padding*2 $padding*2;
|
||||
box-shadow: 0 0 10px rgba($light-bg-color, .4);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
box-shadow: 0 0 10px rgba($light-bg-color, .4);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: #2F3F53;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: $gap;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
align-items: center;
|
||||
|
||||
input, select {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
input, select {
|
||||
border: 2px solid rgb(212, 217, 221);
|
||||
padding: $padding;
|
||||
border-radius: $radius;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.active{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-top:$gap*2;
|
||||
button {
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
border: 2px solid rgb(212, 217, 221);
|
||||
border-radius: $radius;
|
||||
background: $primary-color;
|
||||
color:#fff;
|
||||
font-size: 16px;
|
||||
|
||||
&:first-child {
|
||||
background: unset;
|
||||
color: rgb(212, 217, 221);
|
||||
&:hover {
|
||||
border-color: $light-text-color;
|
||||
color: $light-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -20,10 +20,28 @@ const fitters = [{
|
|||
value: 'offline'
|
||||
}]
|
||||
const selectedFitters = ref([])
|
||||
const visible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bar-layout">
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
:breakpoints="{ '1199px': '75vw', '575px': '90vw' }"
|
||||
:pt="{
|
||||
root: {
|
||||
style:'border:unset;background-color:unset;'
|
||||
},
|
||||
mask: {
|
||||
style: 'backdrop-filter: blur(20px)'
|
||||
}
|
||||
}"
|
||||
modal
|
||||
>
|
||||
<template #container="{ closeCallback }">
|
||||
<UserPageAddUser :close-back="closeCallback"/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<div class="user-num">
|
||||
<Icon :size="30" :stroke-width="1.8" name="User"></Icon>
|
||||
<h3>30</h3>
|
||||
|
@ -39,7 +57,7 @@ const selectedFitters = ref([])
|
|||
</div>
|
||||
<MultiSelect v-model="selectedFitters" :maxSelectedLabels="3" :options="fitters" display="chip"
|
||||
optionLabel="name" placeholder="过滤"></MultiSelect>
|
||||
<button class="add-user">
|
||||
<button class="add-user" @click="visible=true" >
|
||||
<p>添加用户</p>
|
||||
<Icon :size="18" :stroke-width="1.3" name="CirclePlus"></Icon>
|
||||
</button>
|
||||
|
@ -121,6 +139,13 @@ const selectedFitters = ref([])
|
|||
border-radius: 6px;
|
||||
outline: unset;
|
||||
border: unset;
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05);;
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05);
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
//不允许选中
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -17,7 +17,7 @@ const {width: boxWidth, height: boxHeight} = useElementSize(boxEl)
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "base";
|
||||
.scroll-container {
|
||||
--w: calc(v-bind('width') * 1px);
|
||||
--h: calc(v-bind('height') * 1px);
|
||||
|
@ -44,6 +44,7 @@ const {width: boxWidth, height: boxHeight} = useElementSize(boxEl)
|
|||
.content {
|
||||
position: absolute;
|
||||
left: var(--h);
|
||||
width: var(--bw);
|
||||
transform-origin: left top;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
|
|
@ -13,10 +13,6 @@
|
|||
target="_blank">
|
||||
原神玩的队</a></p>
|
||||
</div>
|
||||
<!-- 吉祥物-->
|
||||
<div class="Fox">
|
||||
<iframe src="/Fox/index.html"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -7,17 +7,25 @@ const {$gsap} = useNuxtApp()
|
|||
const mainLayoutStore = useMainLayoutStore()
|
||||
const Menus = [
|
||||
{
|
||||
"label": "概览",
|
||||
"label": "系统概览",
|
||||
"icon": "LayoutGrid",
|
||||
"route": "/Home"
|
||||
}, {
|
||||
"label": "用户",
|
||||
"icon": "User",
|
||||
"route": "/User"
|
||||
}, {
|
||||
"label": "主机",
|
||||
"label": "主机监测",
|
||||
"icon": "Cpu",
|
||||
"route": "/host/cpu"
|
||||
},{
|
||||
"label": "用户监测",
|
||||
"icon": "Cctv",
|
||||
"route": "/serverUser/all"
|
||||
},{
|
||||
"label": "账号列表",
|
||||
"icon": "UsersRound",
|
||||
"route": "/User"
|
||||
},{
|
||||
"label": "巡检记录",
|
||||
"icon": "PackageSearch",
|
||||
"route": "/InspectionRecords"
|
||||
},
|
||||
]
|
||||
onMounted(() => {
|
||||
|
|
|
@ -16,6 +16,7 @@ const visibleRight = ref(false)
|
|||
</Sidebar>
|
||||
<h1 class="name">
|
||||
{{ $router.currentRoute.value.name }}
|
||||
<button @click="req">申请</button>
|
||||
</h1>
|
||||
<div class="action">
|
||||
<BellRing @click="visibleRight=true"/>
|
||||
|
|
|
@ -7,10 +7,10 @@ import type {UserInfoType} from "~/types/UserType";
|
|||
import type {HttpType} from "~/types/baseType";
|
||||
import {useSessionSignalRStore} from "~/strores/HubStore";
|
||||
import {POSITION, useToast} from "vue-toastification";
|
||||
import {type dataHistoryType, useDataStore} from "~/strores/DataStore";
|
||||
import {useBeep} from "~/utils";
|
||||
import {type dataHistoryType, useDataStore} from "~/strores/DataStore";;
|
||||
|
||||
const audio = ref<any>(null);
|
||||
const audio1 = ref<any>(null);
|
||||
const toast = useToast()
|
||||
const visible = ref<boolean>(false)
|
||||
const DataStore = useDataStore()
|
||||
|
@ -64,6 +64,8 @@ onMounted(() => {
|
|||
})
|
||||
})
|
||||
const signalR = useSessionSignalRStore();
|
||||
|
||||
|
||||
//有且运行一次
|
||||
onMounted(() => {
|
||||
signalR.initConnection();
|
||||
|
@ -79,8 +81,51 @@ onMounted(() => {
|
|||
if (id !== mainLayoutStore.SelectServer.id) return
|
||||
DataStore.setData(type, message)
|
||||
})
|
||||
signalR.connection?.on('ReceiveWaring', (value: string,valueName) => {
|
||||
// //两位小数
|
||||
// audio1.value.currentTime = 0;
|
||||
// audio1.value && audio1.value.click()
|
||||
// audio1.value && audio1.value.play()
|
||||
// toast.info(`你设定的${valueName}已经达到通知阈值,当前值为 ${value}`,{
|
||||
// position: POSITION.BOTTOM_RIGHT,
|
||||
// timeout:5000,
|
||||
// })
|
||||
})
|
||||
signalR.connection?.on('ReceiveNotify', (value: string,valueName) => {
|
||||
//两位小数
|
||||
audio1.value.currentTime = 0;
|
||||
const {
|
||||
isSupported,
|
||||
notification,
|
||||
show,
|
||||
close,
|
||||
onClick,
|
||||
onShow,
|
||||
onError,
|
||||
onClose,
|
||||
} = useWebNotification({
|
||||
title: 'Hello, VueUse world!',
|
||||
dir: 'auto',
|
||||
lang: 'en',
|
||||
renotify: true,
|
||||
tag: 'test',
|
||||
})
|
||||
if(isSupported.value){
|
||||
Notification.requestPermission().then(res => {
|
||||
//如果允许
|
||||
console.log(res)
|
||||
})
|
||||
show()
|
||||
return
|
||||
}
|
||||
audio1.value && audio1.value.click()
|
||||
audio1.value && audio1.value.play()
|
||||
toast.info(`你设定的${valueName}已经达到通知阈值,当前值为 ${value}`,{
|
||||
position: POSITION.BOTTOM_RIGHT,
|
||||
timeout:5000,
|
||||
})
|
||||
})
|
||||
signalR.connection?.on("sendMessage", (id, message) => {
|
||||
console.log(id)
|
||||
audio.value.currentTime = 0;
|
||||
audio.value && audio.value.click()
|
||||
audio.value && audio.value.play()
|
||||
|
@ -98,7 +143,7 @@ onMounted(() => {
|
|||
rate: 1,
|
||||
})
|
||||
speech.speak()
|
||||
}, 2000)
|
||||
}, 1000)
|
||||
|
||||
})
|
||||
})
|
||||
|
@ -139,10 +184,19 @@ onMounted(async () => {
|
|||
watch(() => mainLayoutStore.SelectServer.id, async () => {
|
||||
await getHistoryData()
|
||||
})
|
||||
let isShift = false
|
||||
onKeyStroke('Shift', (e) => {
|
||||
e.preventDefault()
|
||||
visible.value = !visible.value
|
||||
})
|
||||
setTimeout(() => {
|
||||
isShift = true
|
||||
},100)
|
||||
setTimeout(() => {
|
||||
isShift = false
|
||||
},200)
|
||||
if(isShift){
|
||||
visible.value = !visible.value
|
||||
}
|
||||
},{eventName: 'keyup'})
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -152,6 +206,10 @@ onKeyStroke('Shift', (e) => {
|
|||
<source src="/audios/idle1.mp3" type="audio/mpeg">
|
||||
您的浏览器不支持 audio 元素。
|
||||
</audio>
|
||||
<audio ref="audio1">
|
||||
<source src="/audios/audio_0a383a4c11.mp3" type="audio/mpeg">
|
||||
您的浏览器不支持 audio 元素。
|
||||
</audio>
|
||||
<SideBar/>
|
||||
<TitleBar/>
|
||||
<Dialog
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDzpzzqmhHGQyqD
|
||||
EHJoLMh5+zjYj6NB7qRiCJiuxfaUC+t4msrUfsdMmcLdvD7Xfs3JcBQn1vXKZ2YF
|
||||
c844p0IgowwmyhY2EvE0cji9fj0JfjlNDAu7ttNd+Pz6z9HPgFj0K7RqAJzKdPSN
|
||||
mX6L/q5h0lgrcPr+tJl1+CrFakXY9OMxbpql/xsHJh4lX7irzMbuYoNSNqBG3in6
|
||||
McEE/ATbVV3hTp9ZNoxHMmrAzLUmUeQdZpa3GyfHumumVALL/trxOQRQ3I2egy/w
|
||||
Y0asY28dmg6c53xvlpts+ho4xkEZPUo7rUGgJgPv7b8pzRGiJ7VfIIIMe3L4iO3c
|
||||
5rVwZtSFAgMBAAECggEBAJqmMo8BfXim1wvbW5Jtok1yPDGQceH+U435wNdlxU1Q
|
||||
h4PEVCst0Nf43Gua+RQUYw+ROOnUkauHl4SsbV8eImXOt37DU/e8bCaKvSLhRmKJ
|
||||
IFub4rKhp2UFDaUwVQ5se4l3WArUGrCSLUrW+zBmVU63kMj57AXQoXr0KkmlW3IJ
|
||||
ODvWsR1/wmYtMcCgGTH5/pi0JcxmSveVqHKJBDttkwob3oP1umvl4ANmv2w+fCkT
|
||||
1sOv3MhpqRyJdfMr+e6bpmTqfZK+vm6Jfr5AOvjVi25rZqBniEI7ywDAx5KkAT97
|
||||
/sd9/WK/iytKH5s08QNpRd3LC4b6FnBrksp9zMVcet0CgYEA9jydxIlVSRTvXfKG
|
||||
Ks7jXdlxtEhJcOEesxlrjKetB1R2UYTBvKodRP0RHgNEXAqPITOovJrDNnVdixg1
|
||||
wM17HwmgNiux99QfszF8U76pXvsnh5hj1DjVONcFHlu+3YW6nTB5xxxC2QgLIVPo
|
||||
av4SKgFPpd5/PSabDdRrZbDCjf8CgYEA/VBl8VsX3yp8K+46+9usb7fNnx/Pg1e8
|
||||
pT/ZWIfPr5v8ugGAlKmFaO7xhNlc5XQcnW+0T1kmvfaE0MWLT6jGjRXj4nPwdoGw
|
||||
PesKdscZbA+4jPbAl8CqpL9HOLuerKY0qAmNm+ETeR8ntIr2GNTl8gaZCefnCwaw
|
||||
mgo8+R0ZZXsCgYAv6IVNqua0DGWyIrCl/ZDRPrBXwkS/uJ0vfX+mYy1QIsfOfoTv
|
||||
Py3osVA2Ra50Nf25GQL4hyf6HYWwvWof9BrDZC0OvRuoO1ZbmAI3jP4JI9aCFE1A
|
||||
Cjq6D2PIj1MoaI9xa/AVpFMBRQZdWqT6xComkBC+Ffctn6hFXZHzvBtuYwKBgQDR
|
||||
jllHOVyeOb9PeF1DTY9xPFTWdrJsrYBaFF/xZSji1eBU4DlGwpajIEic5lR7XXru
|
||||
oyI/IjlynSVysHl3BOB8hsdm5xLedpseHfsiF8NoKfk6ZEcfQzvn3nVE8bFqknSt
|
||||
Lnn/oktBwAxQx0SfdkBj4CFqmHYCIR6n0CBw1SnVUQKBgQDyOxaUifgHb8HeiIPO
|
||||
oxSNEe8rg6vdL6z01DcSYhx0cqanXjdk/AVK7KpmyQYDxtAySZPEkOU95iEQga1k
|
||||
UxjTs1+UMJFXZ4EfBfAgl+t9yZ6M8bXfRKPPra2mwGKQCr1fK7+Dnsz18tds4hGC
|
||||
XSbRaLgFYShExC92JvMNij5jnw==
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,26 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIETzCCAregAwIBAgIRALQzqhzF8A4k30s+JwmV6JAwDQYJKoZIhvcNAQELBQAw
|
||||
dzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSYwJAYDVQQLDB1aV0Jf
|
||||
UENcbml5eXpAendiX3BjICh5eXpmIG5pKTEtMCsGA1UEAwwkbWtjZXJ0IFpXQl9Q
|
||||
Q1xuaXl5ekB6d2JfcGMgKHl5emYgbmkpMB4XDTI0MDYyOTA4MTczMloXDTI2MDky
|
||||
OTA4MTczMlowUTEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmlj
|
||||
YXRlMSYwJAYDVQQLDB1aV0JfUENcbml5eXpAendiX3BjICh5eXpmIG5pKTCCASIw
|
||||
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPOnPOqaEcZDKoMQcmgsyHn7ONiP
|
||||
o0HupGIImK7F9pQL63iaytR+x0yZwt28Ptd+zclwFCfW9cpnZgVzzjinQiCjDCbK
|
||||
FjYS8TRyOL1+PQl+OU0MC7u20134/PrP0c+AWPQrtGoAnMp09I2Zfov+rmHSWCtw
|
||||
+v60mXX4KsVqRdj04zFumqX/GwcmHiVfuKvMxu5ig1I2oEbeKfoxwQT8BNtVXeFO
|
||||
n1k2jEcyasDMtSZR5B1mlrcbJ8e6a6ZUAsv+2vE5BFDcjZ6DL/BjRqxjbx2aDpzn
|
||||
fG+Wm2z6GjjGQRk9SjutQaAmA+/tvynNEaIntV8gggx7cviI7dzmtXBm1IUCAwEA
|
||||
AaN8MHowDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1Ud
|
||||
IwQYMBaAFDxxYJAogU4WZp1fm389HhoxHrSsMDIGA1UdEQQrMCmCCWxvY2FsaG9z
|
||||
dIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAAYcEwKgADTANBgkqhkiG9w0BAQsFAAOC
|
||||
AYEADzM88RDxs05JPf36nz0XwF9oLnSmsL6i+c+M0op4KKe3IDxewHH2+GhHMONV
|
||||
HdJYLoFOiLQ/BPmsiwKWWNja9OEbUSHXb04KaXwvbPyMfFaDuqnYfVQN8065+9S+
|
||||
zV/ikHO+MYJMs+hFpn7IfMAMceu7rkPAaoQz+8b/YOIOvlClpajNJxR4EO6VxMss
|
||||
mApCt4rzVlcNMS4UIFmdy40NWHnqugYOavzKsZicKKrv4Pfhy3yw2yPcki74zESj
|
||||
yffQdtPf9IrX8tDwllHQ52r2MhoyiIZTdO9I+0psYV9qh7JpHNvQ05XzzegirliM
|
||||
rLncbLhwXJLNgWZlnC0iKD4UroD3RQALVJXPsv22mXGQjwOjVk3aWybV2vBXOOUn
|
||||
FVMOd9Er4CcUwCgzSjWUWPh3aTNT5OYbQ4nRf6ypvbv7n3wM/LjHxRb7+wxREqw4
|
||||
yri8WIDfewhtFz0OHI2Jht33r8IWf02ydOBrdGTHHRN3MGZwgdjSWMNsVrgUpxKY
|
||||
grlJ
|
||||
-----END CERTIFICATE-----
|
|
@ -33,10 +33,10 @@ export default defineNuxtConfig({
|
|||
css: ['assets/min.scss', 'primevue/resources/themes/aura-light-green/theme.css', 'primeicons/primeicons.css', 'vue-toastification/dist/index.css'],
|
||||
devServer: {
|
||||
port: 3001, host: '0.0.0.0',
|
||||
// https: {
|
||||
// key: "./cert.key",
|
||||
// cert: "./cert.crt",
|
||||
// }
|
||||
https: {
|
||||
key: "./localhost+3-key.pem",
|
||||
cert: "./localhost+3.pem",
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
{src: '~/plugins/vue-toast.ts'},
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"echarts": "^5.5.0",
|
||||
"grid-layout-plus": "^1.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"md-editor-v3": "^4.17.0",
|
||||
"nuxt": "^3.11.2",
|
||||
"nuxt-primevue": "^3.0.0",
|
||||
"patch-package": "^8.0.0",
|
||||
|
|
|
@ -6,7 +6,7 @@ definePageMeta({
|
|||
})
|
||||
import * as Yup from 'yup';
|
||||
import type {HttpType} from "~/types/baseType";
|
||||
import {useToast} from "#imports";
|
||||
import {useToast} from "vue-toastification";
|
||||
|
||||
const toast = useToast()
|
||||
const errors = ref<string[]>([]);
|
||||
|
@ -48,12 +48,12 @@ const onSuccess = () => {
|
|||
const data = res as HttpType<any>;
|
||||
if (data.code == 200) {
|
||||
useCookie('token').value = data.data['token'];
|
||||
toast.add({severity: 'success', summary: '登录成功', detail: `欢迎回来!${data.data['userName']}`, life: 3000})
|
||||
toast.success(`登录成功,欢迎回来!${data.data['userName']}`)
|
||||
setTimeout(() => {
|
||||
navigateTo('/Home')
|
||||
}, 1000)
|
||||
} else {
|
||||
toast.add({severity: 'error', summary: data.message, detail: "发生了错误", life: 3000})
|
||||
toast.error("发生了错误")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ type RouteType = {
|
|||
const mainLayoutStore = useMainLayoutStore()
|
||||
const diskRoute = ref<RouteType[]>([])
|
||||
const netRoute = ref<RouteType[]>([])
|
||||
const vgaRoute = ref<RouteType[]>([])
|
||||
const reLoad=ref<boolean>(false)
|
||||
|
||||
const headers = computed(() => {
|
||||
{
|
||||
|
@ -40,22 +40,21 @@ const headers = computed(() => {
|
|||
route: x.route
|
||||
}
|
||||
}),
|
||||
...vgaRoute.value.map(x => {
|
||||
return {
|
||||
label: x.label,
|
||||
icon: x.icon,
|
||||
route: x.route
|
||||
}
|
||||
})
|
||||
{
|
||||
label:'进程',
|
||||
icon:'LayoutList',
|
||||
route:'/host/process'
|
||||
},{
|
||||
label:'网络连接',
|
||||
icon:'Waypoints',
|
||||
route:'/host/networkList'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
getRote()
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer, () => {
|
||||
getRote()
|
||||
})
|
||||
const getRote = () => {
|
||||
$fetch('/Api/Server/GetServerDiskList', {
|
||||
method: 'GET',
|
||||
|
@ -96,26 +95,15 @@ const getRote = () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
$fetch('/Api/Server/GetServerGpuList', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + useCookie('token').value
|
||||
}
|
||||
}).then(res => {
|
||||
const data = res as string[]
|
||||
vgaRoute.value = data.map((x, index) => {
|
||||
return {
|
||||
label: `GPU ${index} (${x})`,
|
||||
icon: 'Gpu',
|
||||
route: '/host/Gpu/' + index
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
watch(() => mainLayoutStore.SelectServer.id, () => {
|
||||
getRote()
|
||||
navigateTo('/host/cpu')
|
||||
reLoad.value = true
|
||||
nextTick(() => {
|
||||
reLoad.value = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -130,7 +118,7 @@ const getRote = () => {
|
|||
</div>
|
||||
</div>
|
||||
</XScroll>
|
||||
<NuxtPage :foobar="123"/>
|
||||
<NuxtPage v-if="!reLoad" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import VChart from "vue-echarts";
|
||||
import dayjs from "dayjs";
|
||||
import {useDataStore} from "~/strores/DataStore";
|
||||
import AreaChart2 from "~/components/Charts/AreaChart2.vue";
|
||||
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
type CpuInfo = {
|
||||
|
@ -11,7 +10,6 @@ type CpuInfo = {
|
|||
}
|
||||
const lsCpu = ref<CpuInfo[]>([])
|
||||
const dataStore = useDataStore()
|
||||
const colorMode = useColorMode()
|
||||
const getSystemInfo = () => {
|
||||
$fetch(`/Api/Server/GetServerCpuInfo`, {
|
||||
method: 'GET',
|
||||
|
@ -34,229 +32,6 @@ const getSystemInfo = () => {
|
|||
onMounted(() => {
|
||||
getSystemInfo();
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer, () => {
|
||||
getSystemInfo();
|
||||
})
|
||||
const values = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const hoursValue = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const dayValue = ref<(number | null)[]>([...Array.from({length: 13}, (_, index) => null)])
|
||||
const option = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: values.value.length}, (_, index) => dayjs().add(-index, 's').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: values.value,
|
||||
color: ['#0167d7'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
const optionHours = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: hoursValue.value.length}, (_, index) => dayjs().add(-index, 'h').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: hoursValue.value,
|
||||
color: ['#0167d7'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
const optionDay = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: dayValue.value.length}, (_, index) => dayjs().add(-index, 'h').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: dayValue.value,
|
||||
color: ['#0167d7'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
let Interval: NodeJS.Timeout
|
||||
let IntervalHours: NodeJS.Timeout
|
||||
let IntervalRefresh: NodeJS.Timeout
|
||||
let minutesTemp: number[] = []
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
Interval = setInterval(() => {
|
||||
let data: number = Number(dataStore.data["CpuTotalUsage"])
|
||||
//随机抖动-5,+5
|
||||
const jitter = Math.random() * 2 - 1
|
||||
data = Number((data + jitter).toFixed(2))
|
||||
values.value.push(data)
|
||||
minutesTemp.push(data)
|
||||
values.value.shift()
|
||||
}, 1000)
|
||||
IntervalHours = setInterval(() => {
|
||||
//data为minutesTemp的均值
|
||||
let data: number = minutesTemp.reduce((a, b) => a + b, 0) / minutesTemp.length
|
||||
data = Number((data).toFixed(2))
|
||||
hoursValue.value.push(data)
|
||||
values.value.shift()
|
||||
}, 60000)
|
||||
IntervalRefresh = setInterval(() => {
|
||||
getHistoryData()
|
||||
}, 600000)
|
||||
})
|
||||
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer.id, () => {
|
||||
values.value = [...Array.from({length: 61}, (_, index) => null)]
|
||||
getHistoryData()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Interval)
|
||||
clearInterval(IntervalHours)
|
||||
})
|
||||
const getHistoryData = () => {
|
||||
values.value = dataStore.dataHistory.data["CpuTotalUsage"].slice(-60).map(x => Number(x))
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'CpuTotalUsage',
|
||||
timeRange: '1h'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
hoursValue.value = res.data
|
||||
})
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'CpuTotalUsage',
|
||||
timeRange: '1d'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
dayValue.value = res.data
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
getHistoryData()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -272,41 +47,10 @@ onBeforeMount(() => {
|
|||
<p>100%</p>
|
||||
</div>
|
||||
<div class="cpu-chart">
|
||||
<v-chart
|
||||
:option="option"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
<AreaChart2 :value-ids="['CpuTotalUsage']" :value-names="['CPU使用率']" :colors="['#0073f4']" :bg-color="'rgba(0,115,244,0.3)'" :configs="[{areaStyle:{}}]"/>
|
||||
</div>
|
||||
<div class="cpu-chat-bottom">
|
||||
<p>1分钟</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
<div class="cpu-chart-top">
|
||||
<p>使用率</p>
|
||||
<p>100%</p>
|
||||
</div>
|
||||
<div class="cpu-chart">
|
||||
<v-chart
|
||||
:option="optionHours"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
</div>
|
||||
<div class="cpu-chat-bottom">
|
||||
<p>1小时</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
<div class="cpu-chart-top">
|
||||
<p>使用率</p>
|
||||
<p>100%</p>
|
||||
</div>
|
||||
<div class="cpu-chart">
|
||||
<v-chart
|
||||
:option="optionDay"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
</div>
|
||||
<div class="cpu-chat-bottom">
|
||||
<p>1天</p>
|
||||
<p>更早</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -351,7 +95,7 @@ onBeforeMount(() => {
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: $gap*2;
|
||||
gap: $gap*4;
|
||||
}
|
||||
|
||||
.left-chart {
|
||||
|
@ -391,8 +135,8 @@ onBeforeMount(() => {
|
|||
border: 4px solid #0073f4;
|
||||
border-radius: $radius*2;
|
||||
height: min-content;
|
||||
max-height: 32rem;
|
||||
min-height: 24rem;
|
||||
max-height: 48rem;
|
||||
min-height: 32rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
import VChart from "vue-echarts";
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {useDataStore} from "~/strores/DataStore";
|
||||
import dayjs from "dayjs";
|
||||
import AreaChart2 from "~/components/Charts/AreaChart2.vue";
|
||||
|
||||
type diskInfo = {
|
||||
name: string,
|
||||
value: strings
|
||||
value: string
|
||||
}
|
||||
const id = toRef(useRoute().params.id);
|
||||
const lsDisk = ref<diskInfo[]>([])
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const dataStore = useDataStore()
|
||||
const route = useRoute()
|
||||
onMounted(() => {
|
||||
$fetch('/Api/Server/GetServerDiskInfo', {
|
||||
method: 'GET',
|
||||
|
@ -30,134 +28,6 @@ onMounted(() => {
|
|||
lsDisk.value = res as diskInfo[]
|
||||
})
|
||||
})
|
||||
const values = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const rValues = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const wValues = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const option = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: values.value.length}, (_, index) => dayjs().add(-index, 's').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(38,153,100,0.6)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(38,153,100,0.6)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: values.value,
|
||||
color: ['#269964'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
const RWOption = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: rValues.value.length}, (_, index) => dayjs().add(-index, 's').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(38,153,100,0.6)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 10,
|
||||
min: 0,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(38,153,100,0.6)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: rValues.value,
|
||||
color: ['#269964'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
},
|
||||
{
|
||||
data: wValues.value,
|
||||
color: ['#269964'],
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
width: 2, // 线宽
|
||||
dashArray: [5, 10] // 虚线段长度和间隔
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
let Interval: NodeJS.Timeout
|
||||
onMounted(() => {
|
||||
Interval = setInterval(() => {
|
||||
let data: number = Number(dataStore.data[`diskUtil-${id.value}`])
|
||||
//随机抖动-5,+5
|
||||
const jitter = Math.random() * 2 - 1
|
||||
data = Number((data + jitter).toFixed(2))
|
||||
values.value.push(data)
|
||||
values.value.shift()
|
||||
data = Number(dataStore.data[`diskReadKB-${id.value}`])
|
||||
data = Number((data + jitter).toFixed(2))
|
||||
rValues.value.push(data)
|
||||
rValues.value.shift()
|
||||
data = Number(dataStore.data[`diskWriteKB-${id.value}`])
|
||||
data = Number((data + jitter).toFixed(2))
|
||||
wValues.value.push(data)
|
||||
wValues.value.shift()
|
||||
}, 1000)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -172,13 +42,16 @@ onBeforeUnmount(() => {
|
|||
<p>100%</p>
|
||||
</div>
|
||||
<div class="disk-chart">
|
||||
<v-chart
|
||||
:option="option"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
<AreaChart2 :bg-color="'rgba(38,153,100,0.3)'" :colors="['#269964']" :value-ids="[`DiskUtil-${$route.params.id}`]"
|
||||
:configs="[
|
||||
{
|
||||
areaStyle: {}
|
||||
}
|
||||
]"
|
||||
:value-names="['磁盘使用率']"/>
|
||||
</div>
|
||||
<div class="disk-chat-bottom">
|
||||
<p>1分钟</p>
|
||||
<p>更早</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
<div class="disk-chart-top">
|
||||
|
@ -186,13 +59,11 @@ onBeforeUnmount(() => {
|
|||
<p></p>
|
||||
</div>
|
||||
<div class="disk-chart">
|
||||
<v-chart
|
||||
:option="RWOption"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
<AreaChart2 :bg-color="'rgba(38,153,100,0.3)'" :colors="['#269964']" :min="null" :max="null" :configs="[{},{lineStyle: {type: 'dashed'},type: 'line'}]"
|
||||
:value-ids="[`DiskReadKB-${$route.params.id}`,`DiskWriteKB-${$route.params.id}`]" :value-names="['磁盘读取KB','磁盘写入KB']"/>
|
||||
</div>
|
||||
<div class="disk-chat-bottom">
|
||||
<p>1分钟</p>
|
||||
<p>更早</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -237,7 +108,7 @@ onBeforeUnmount(() => {
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: $gap*2;
|
||||
gap: $gap*4;
|
||||
}
|
||||
|
||||
.left-chart {
|
||||
|
@ -278,7 +149,7 @@ onBeforeUnmount(() => {
|
|||
border-radius: $radius*2;
|
||||
height: min-content;
|
||||
max-height: 32rem;
|
||||
min-height: 24rem;
|
||||
min-height: 32rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -312,7 +183,7 @@ onBeforeUnmount(() => {
|
|||
.other-info {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
|
||||
overflow: hidden;
|
||||
p:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
@ -321,6 +192,7 @@ onBeforeUnmount(() => {
|
|||
flex: 1;
|
||||
//文本换行
|
||||
word-wrap: break-word;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>{{ $route.params.id }}</p>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import VChart from "vue-echarts";
|
||||
import dayjs from "dayjs";
|
||||
import {useDataStore} from "~/strores/DataStore";
|
||||
import AreaChart2 from "~/components/Charts/AreaChart2.vue";
|
||||
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
type MemoryInfo = {
|
||||
|
@ -33,230 +32,6 @@ const getSystemInfo = () => {
|
|||
onMounted(() => {
|
||||
getSystemInfo();
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer, () => {
|
||||
getSystemInfo();
|
||||
})
|
||||
const values = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const hoursValue = ref<(number | null)[]>([...Array.from({length: 61}, (_, index) => null)])
|
||||
const dayValue = ref<(number | null)[]>([...Array.from({length: 13}, (_, index) => null)])
|
||||
const option = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: values.value.length}, (_, index) => dayjs().add(-index, 's').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: values.value,
|
||||
color: ['#5c9be8'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
const optionHours = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: hoursValue.value.length}, (_, index) => dayjs().add(-index, 'h').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: hoursValue.value,
|
||||
color: ['#5c9be8'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
const optionDay = computed(() => {
|
||||
return {
|
||||
animation: false,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({length: dayValue.value.length}, (_, index) => dayjs().add(-index, 'h').toString()),
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
show: true,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: ['rgba(0,115,244,0.3)'],
|
||||
width: 2,
|
||||
type: 'solid'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: dayValue.value,
|
||||
color: ['#5c9be8'],
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
}
|
||||
})
|
||||
let Interval: NodeJS.Timeout
|
||||
let IntervalHours: NodeJS.Timeout
|
||||
let IntervalRefresh: NodeJS.Timeout
|
||||
let minutesTemp: number[] = []
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
Interval = setInterval(() => {
|
||||
let data: number = Number(dataStore.data["MemoryTotalUsage"])
|
||||
//随机抖动-5,+5
|
||||
const jitter = Math.random() * 2 - 1
|
||||
data = Number((data + jitter).toFixed(2))
|
||||
values.value.push(data)
|
||||
minutesTemp.push(data)
|
||||
values.value.shift()
|
||||
}, 1000)
|
||||
IntervalHours = setInterval(() => {
|
||||
//data为minutesTemp的均值
|
||||
let data: number = minutesTemp.reduce((a, b) => a + b, 0) / minutesTemp.length
|
||||
data = Number((data).toFixed(2))
|
||||
hoursValue.value.push(data)
|
||||
values.value.shift()
|
||||
}, 60000)
|
||||
IntervalRefresh = setInterval(() => {
|
||||
getHistoryData()
|
||||
}, 600000)
|
||||
})
|
||||
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer.id, () => {
|
||||
values.value = [...Array.from({length: 61}, (_, index) => null)]
|
||||
getHistoryData()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Interval)
|
||||
clearInterval(IntervalHours)
|
||||
clearInterval(IntervalRefresh)
|
||||
})
|
||||
const getHistoryData = () => {
|
||||
values.value = dataStore.dataHistory.data["MemoryTotalUsage"].slice(-60).map(x => Number(x))
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'MemoryTotalUsage',
|
||||
timeRange: '1h'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
hoursValue.value = res.data
|
||||
})
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'MemoryTotalUsage',
|
||||
timeRange: '1d'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
dayValue.value = res.data
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
getHistoryData()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -272,10 +47,11 @@ onBeforeMount(() => {
|
|||
<p>100%</p>
|
||||
</div>
|
||||
<div class="memory-chart">
|
||||
<v-chart
|
||||
:option="option"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
<AreaChart2 :bg-color="'rgba(92,155,232,0.3)'" :colors="['#5c9be8']" :value-names="['内存使用率']" :value-ids="['MemoryTotalUsage']" :configs="[
|
||||
{
|
||||
areaStyle:{}
|
||||
}
|
||||
]"/>
|
||||
</div>
|
||||
<div class="memory-chat-bottom">
|
||||
<p>1分钟</p>
|
||||
|
@ -293,34 +69,6 @@ onBeforeMount(() => {
|
|||
}" class="composition"></div>
|
||||
<div class="composition"></div>
|
||||
</div>
|
||||
<div class="memory-chart-top">
|
||||
<p>内存使用率</p>
|
||||
<p>100%</p>
|
||||
</div>
|
||||
<div class="memory-chart">
|
||||
<v-chart
|
||||
:option="optionHours"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
</div>
|
||||
<div class="memory-chat-bottom">
|
||||
<p>1小时</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
<div class="memory-chart-top">
|
||||
<p>内存使用率</p>
|
||||
<p>100%</p>
|
||||
</div>
|
||||
<div class="memory-chart">
|
||||
<v-chart
|
||||
:option="optionDay"
|
||||
autoresize
|
||||
class="chart"/>
|
||||
</div>
|
||||
<div class="memory-chat-bottom">
|
||||
<p>1天</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-info">
|
||||
<div class="info-title-box">
|
||||
|
@ -334,9 +82,7 @@ onBeforeMount(() => {
|
|||
</div>
|
||||
<div class="info-title">
|
||||
<p>已提交</p>
|
||||
<h2>{{
|
||||
((Number(dataStore.data["MemoryTotal"]) - Number(dataStore.data["MemoryCache"])) / 1024 / 1024).toFixed(2)
|
||||
}} GB</h2>
|
||||
<h2>{{((Number(dataStore.data["MemoryTotal"]) - Number(dataStore.data["MemoryCache"])) / 1024 / 1024).toFixed(2) }} GB</h2>
|
||||
</div>
|
||||
<div class="info-title">
|
||||
<p>已缓存</p>
|
||||
|
@ -369,7 +115,7 @@ onBeforeMount(() => {
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: $gap*2;
|
||||
gap: $gap*4;
|
||||
}
|
||||
|
||||
.left-chart {
|
||||
|
@ -409,8 +155,8 @@ onBeforeMount(() => {
|
|||
border: 4px solid #5c9be8;
|
||||
border-radius: $radius*2;
|
||||
height: min-content;
|
||||
max-height: 32rem;
|
||||
min-height: 24rem;
|
||||
max-height: 48rem;
|
||||
min-height: 32rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -468,7 +214,6 @@ onBeforeMount(() => {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*.5;
|
||||
|
||||
.other-info {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
|
@ -479,6 +224,8 @@ onBeforeMount(() => {
|
|||
|
||||
p {
|
||||
flex: 1;
|
||||
//文本换行
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,207 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {useDataStore} from "~/strores/DataStore";
|
||||
import AreaChart2 from "~/components/Charts/AreaChart2.vue";
|
||||
|
||||
const id = toRef(useRoute().params.id);
|
||||
const lsNet = ref<string[]>([])
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const dataStore = useDataStore()
|
||||
onMounted(() => {
|
||||
$fetch('/Api/Server/GetServerNetworkEquipmentInfo', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
NetworkId: id.value
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res => {
|
||||
lsNet.value = res as string[]
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>{{ $route.params.id }}</p>
|
||||
<div class="net-layout">
|
||||
<div class="left-chart">
|
||||
<div class="net-title">
|
||||
<h1>网络设备 {{ $route.params.id }}</h1>
|
||||
</div>
|
||||
<div class="net-chart-top">
|
||||
<p>活动时间</p>
|
||||
<p>100%</p>
|
||||
</div>
|
||||
<div class="net-chart">
|
||||
<AreaChart2 :bg-color="'rgba(208,157,209,0.3)'" :colors="['#d09dd1']" :value-ids="[`ReceivedPacketsPerSecond-${id}`,`TransmittedPacketsPerSecond-${id}`]"
|
||||
:configs="[
|
||||
{
|
||||
|
||||
},{
|
||||
lineStyle: {
|
||||
type: 'dashed' }
|
||||
}]"
|
||||
:value-names="['每秒收包数','每秒发包数']"/>
|
||||
</div>
|
||||
<div class="net-chat-bottom">
|
||||
<p>更早</p>
|
||||
<p>0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-info">
|
||||
<div class="info-title-box">
|
||||
<div class="infos">
|
||||
<div class="info2">
|
||||
<div></div>
|
||||
<p>接受</p>
|
||||
<h2>{{ dataStore.data[`ReceivedPacketsPerSecond-${id}`] }} KiB/s</h2>
|
||||
</div>
|
||||
<div class="info2">
|
||||
<div></div>
|
||||
<p>发送</p>
|
||||
<h2>{{ dataStore.data[`TransmittedPacketsPerSecond-${id}`] }} KiB/s</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other-info-box">
|
||||
<div v-for="value in lsNet" class="other-info">
|
||||
<p>{{ value}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "base";
|
||||
|
||||
.net-layout {
|
||||
padding: $padding*1.5;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
grid-template-rows: 1fr;
|
||||
gap: $gap*4;
|
||||
}
|
||||
|
||||
.left-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
|
||||
.net-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.dark-mode & {
|
||||
h1, h2 {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: $light-text-color;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.net-chart-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.net-chart {
|
||||
flex: 1;
|
||||
border: 4px solid #d09dd1;
|
||||
border-radius: $radius*2;
|
||||
height: min-content;
|
||||
max-height: 32rem;
|
||||
min-height: 24rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.net-chat-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-title-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $gap*2;
|
||||
|
||||
.info-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*.5;
|
||||
}
|
||||
|
||||
.info-title:nth-of-type(2) {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.other-info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*.5;
|
||||
|
||||
.other-info {
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
|
||||
p:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
//文本换行
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*2;
|
||||
}
|
||||
|
||||
.infos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*2;
|
||||
|
||||
.info2 {
|
||||
display: grid;
|
||||
grid-template-columns: 4px 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-column-gap: $gap;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
border: 3px solid #d09dd1;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
}
|
||||
|
||||
.info2:last-of-type {
|
||||
& > div {
|
||||
height: 100%;
|
||||
border: 3px dashed #d09dd1;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,216 @@
|
|||
<script setup lang="ts">
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {ref} from "vue";
|
||||
type NetworkList= {
|
||||
netId:string,
|
||||
recvQ:string,
|
||||
sendQ:string,
|
||||
addressForm:string,
|
||||
addressTo:string,
|
||||
process:{
|
||||
name:string,
|
||||
pid:string,
|
||||
}[]
|
||||
}
|
||||
const props=defineProps({
|
||||
pids:{
|
||||
type:Array<string>,
|
||||
default:[]
|
||||
},
|
||||
filter:{
|
||||
type:Boolean,
|
||||
default:false,
|
||||
}
|
||||
})
|
||||
const mainLayoutStore=useMainLayoutStore()
|
||||
const lsNetworkList=ref<NetworkList[]>([])
|
||||
const currentSort = ref<'netId' | 'recvQ' | 'sendQ'|'pid' | 'addressForm' | 'addressTo'|'process'>('recvQ');
|
||||
const currentSortDir = ref<'asc' | 'desc'>('desc');
|
||||
let interval:NodeJS.Timeout;
|
||||
onMounted(()=>{
|
||||
getNetworkList()
|
||||
interval = setInterval(()=>{
|
||||
getNetworkList();
|
||||
},10000)
|
||||
})
|
||||
onBeforeUnmount(()=>{
|
||||
clearInterval(interval);
|
||||
})
|
||||
const getNetworkList=()=>{
|
||||
$fetch('/Api/Server/GetServerNetworkList',{
|
||||
method:'GET',
|
||||
params:{
|
||||
ServerId:mainLayoutStore.SelectServer.id,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL:useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
console.log(res)
|
||||
lsNetworkList.value=res as NetworkList[]
|
||||
}).catch(err=>{
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
const sort = (key: 'netId' | 'recvQ' | 'sendQ'|'pid' | 'addressForm' | 'addressTo'|'process') => {
|
||||
if (currentSort.value === key) {
|
||||
currentSortDir.value = currentSortDir.value === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.value = key;
|
||||
currentSortDir.value = 'asc';
|
||||
}
|
||||
};
|
||||
const sortedNetworkList = computed(() => {
|
||||
//如果pids不为空则要求pid包含在pids中
|
||||
let filteredList = lsNetworkList.value;
|
||||
if (props.filter) {
|
||||
filteredList = filteredList.filter(item => {
|
||||
// 检查当前网络连接对象中的每一个 process.pid 是否包含在 pids 数组中
|
||||
return item.process.some(processItem => props.pids.includes(processItem.pid));
|
||||
});
|
||||
}
|
||||
return filteredList.sort((a, b) => {
|
||||
let modifier = 1;
|
||||
if (currentSortDir.value === 'desc') modifier = -1;
|
||||
let aValue: number | string;
|
||||
let bValue: number | string;
|
||||
|
||||
if (currentSort.value === 'process') {
|
||||
aValue = a.process.length;
|
||||
bValue = b.process.length;
|
||||
} else if (currentSort.value === 'pid') {
|
||||
aValue = a.process[0].pid;
|
||||
bValue = b.process[0].pid;
|
||||
} else {
|
||||
// 如果有其他排序条件,可以在这里添加
|
||||
aValue = a[currentSort.value] as string;
|
||||
bValue = b[currentSort.value] as string;
|
||||
}
|
||||
|
||||
if (aValue < bValue) return -1 * modifier;
|
||||
if (aValue > bValue) return modifier;
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="network-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th @click="sort('process')">
|
||||
<div>
|
||||
<p>执行进程</p>
|
||||
<Icon v-if="currentSort==='process'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='process'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<th @click="sort('pid')">
|
||||
<div>
|
||||
<p>进程ID</p>
|
||||
<Icon v-if="currentSort==='pid'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='pid'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('netId')">
|
||||
<div>
|
||||
<p>类型</p>
|
||||
<Icon v-if="currentSort==='netId'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='netId'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('recvQ')">
|
||||
<div>
|
||||
<p>收包数</p>
|
||||
<Icon v-if="currentSort==='recvQ'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='recvQ'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('sendQ')">
|
||||
<div>
|
||||
<p>发包数</p>
|
||||
<Icon v-if="currentSort==='sendQ'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='sendQ'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('addressForm')">
|
||||
<div>
|
||||
<p>发起地址</p>
|
||||
<Icon v-if="currentSort==='addressForm'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='addressForm'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th><th>
|
||||
<div @click="sort('addressTo')">
|
||||
<p>接收地址</p>
|
||||
<Icon v-if="currentSort==='addressTo'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='addressTo'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item,index) in sortedNetworkList" :key="index">
|
||||
<td>
|
||||
<Icon name="Rss"/>
|
||||
{{ item.process[0].name }}
|
||||
<Tag v-if="item.process.length>1">{{item.process.length}}</Tag>
|
||||
</td>
|
||||
<td>{{ item.process[0].pid}}</td>
|
||||
<td>{{ item.netId }}</td>
|
||||
<td>{{ item.recvQ }}</td>
|
||||
<td>{{ item.sendQ }}</td>
|
||||
<td>{{ item.addressForm }}</td>
|
||||
<td>{{ item.addressTo }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.network-table {
|
||||
width: 100%;
|
||||
padding: $padding*2;
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
border-top: unset;
|
||||
border-bottom: unset;
|
||||
padding: 8px;
|
||||
}
|
||||
th {
|
||||
>div{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
p{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr:hover{
|
||||
background: $light-bg-underline-color;
|
||||
}
|
||||
tr>td:first-of-type{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
}
|
||||
td:not(:first-child) {
|
||||
text-align: right;
|
||||
}
|
||||
td:first-of-type,th:first-of-type,td:last-of-type,th:last-of-type {
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,223 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRuntimeConfig, useCookie } from '#imports';
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import { useToast } from 'vue-toastification';
|
||||
type ProcessListType = {
|
||||
pid: string;
|
||||
user: string;
|
||||
cpu: string;
|
||||
memory: string;
|
||||
processName: string;
|
||||
};
|
||||
const toast=useToast()
|
||||
const processList = ref<ProcessListType[]>([]);
|
||||
const mainLayoutStore = useMainLayoutStore();
|
||||
const currentSort = ref<'pid' | 'user' | 'cpu' | 'memory' | 'processName'>('cpu');
|
||||
const currentSortDir = ref<'asc' | 'desc'>('desc');
|
||||
const props=defineProps({
|
||||
userName:{
|
||||
type:String,
|
||||
default:''
|
||||
},
|
||||
})
|
||||
const emit=defineEmits([
|
||||
'update'
|
||||
])
|
||||
const getProcessList = async () => {
|
||||
const response = await $fetch('/Api/Server/GetServerProcessesList', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
UserName:props.userName
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + useCookie('token').value,
|
||||
},
|
||||
});
|
||||
processList.value = response as ProcessListType[];
|
||||
}
|
||||
let interval:NodeJS.Timeout;
|
||||
onMounted(async () => {
|
||||
await getProcessList();
|
||||
interval = setInterval(async ()=>{
|
||||
await getProcessList();
|
||||
},10000)
|
||||
});
|
||||
onBeforeUnmount(()=>{
|
||||
clearInterval(interval);
|
||||
})
|
||||
|
||||
const sortedProcessList = computed(() => {
|
||||
emit('update',processList.value.map(x=>x.pid))
|
||||
return processList.value.sort((a, b) => {
|
||||
let modifier = 1;
|
||||
if (currentSortDir.value === 'desc') modifier = -1;
|
||||
if (a[currentSort.value] < b[currentSort.value]) return -1 * modifier;
|
||||
if (a[currentSort.value] > b[currentSort.value]) return modifier;
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
const sort = (s: 'pid' | 'user' | 'cpu' | 'memory' | 'processName') => {
|
||||
if (currentSort.value === s) {
|
||||
currentSortDir.value = currentSortDir.value === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.value = s;
|
||||
currentSortDir.value = 'asc';
|
||||
}
|
||||
};
|
||||
const killProcess = (pid: string,force:boolean=false) => {
|
||||
$fetch('/Api/Server/GetServerProcessesKill', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
Pid: pid,
|
||||
Force:force,
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + useCookie('token').value,
|
||||
},
|
||||
}).then((res) => {
|
||||
console.log(res)
|
||||
toast.success(res)
|
||||
}).catch((err) => {
|
||||
toast.error(err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="process-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th @click="sort('processName')">
|
||||
<div>
|
||||
<p>进程名称</p>
|
||||
<Icon v-if="currentSort==='processName'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='processName'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<th @click="sort('pid')">
|
||||
<div>
|
||||
<p>进程ID</p>
|
||||
<Icon v-if="currentSort==='pid'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='pid'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('user')">
|
||||
<div>
|
||||
<p>用户</p>
|
||||
<Icon v-if="currentSort==='user'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='user'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('cpu')">
|
||||
<div>
|
||||
<p>CPU使用率</p>
|
||||
<Icon v-if="currentSort==='cpu'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='cpu'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sort('memory')">
|
||||
<div>
|
||||
<p>内存使用率</p>
|
||||
<Icon v-if="currentSort==='memory'&¤tSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
|
||||
<Icon v-if="currentSort==='memory'&¤tSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div>
|
||||
<p>操作</p>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in sortedProcessList" :key="item.pid">
|
||||
<td>
|
||||
<Icon name="Shell"/>
|
||||
{{ item.processName }}
|
||||
</td>
|
||||
<td>{{ item.pid }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.cpu }}</td>
|
||||
<td>{{ item.memory }}</td>
|
||||
<td><div>
|
||||
<button @click="killProcess(item.pid)">关闭</button><button @click="killProcess(item.pid,true)">杀死</button>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.process-table {
|
||||
width: 100%;
|
||||
padding: $padding*2;
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
border-top: unset;
|
||||
border-bottom: unset;
|
||||
padding: 8px;
|
||||
}
|
||||
th {
|
||||
>div{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
p{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr:hover{
|
||||
background: $light-bg-underline-color;
|
||||
}
|
||||
tr>td:first-of-type{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
}
|
||||
td:not(:first-child):not(:last-child) {
|
||||
text-align: right;
|
||||
}
|
||||
td:last-of-type{
|
||||
//大小适应内容
|
||||
>div{
|
||||
display: flex;
|
||||
gap: $gap*2;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
button{
|
||||
background: unset;
|
||||
border: unset;
|
||||
padding: 0;
|
||||
&:hover{
|
||||
color: $primary-color;
|
||||
font-weight: 800;
|
||||
}
|
||||
&:last-of-type:hover{
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
td:first-of-type,th:first-of-type,td:last-of-type,th:last-of-type {
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,304 @@
|
|||
<script setup lang="ts">
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'main',
|
||||
middleware: ['auth']
|
||||
})
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const selectWord = ref<string>()
|
||||
type Document = {
|
||||
wordId: string,
|
||||
wordName: string,
|
||||
lastModifyAt: string,
|
||||
createAt: string,
|
||||
fileSize: string
|
||||
userName: string
|
||||
}
|
||||
type template = {
|
||||
name: string,
|
||||
content: string,
|
||||
}
|
||||
const documentList = ref<Document[]>([])
|
||||
const templateList = ref<template[]>([])
|
||||
onMounted(() => {
|
||||
getWordList()
|
||||
$fetch('/Api/Server/GetWordTemplates',{
|
||||
method:'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL:useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
templateList.value = res as template[]
|
||||
})
|
||||
})
|
||||
const getWordList = ()=>{
|
||||
$fetch('/Api/Server/GetWordList',{
|
||||
method:'GET',
|
||||
params:{
|
||||
ServerId:mainLayoutStore.SelectServer.id,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL:useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
documentList.value = res as Document[]
|
||||
})
|
||||
}
|
||||
const editVisible= ref(false)
|
||||
const isPreview = ref(false)
|
||||
const selectWordChange=(wordId:string,preview:boolean=false)=>{
|
||||
selectWord.value = wordId
|
||||
editVisible.value = true
|
||||
isPreview.value = preview
|
||||
}
|
||||
watch(editVisible,()=>{
|
||||
if(!editVisible.value){
|
||||
selectWord.value = undefined
|
||||
isPreview.value = false
|
||||
templateContent.value = undefined
|
||||
}
|
||||
})
|
||||
const templateVisible = ref(false)
|
||||
const templateContent= ref<string>()
|
||||
const selectTemplate = (template:string)=>{
|
||||
templateContent.value=template
|
||||
editVisible.value = true
|
||||
}
|
||||
watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
||||
getWordList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inspection-layout">
|
||||
<Dialog
|
||||
v-model:visible="editVisible"
|
||||
:pt="{
|
||||
root: {
|
||||
style:'border:unset;background-color:unset;'
|
||||
},
|
||||
mask: {
|
||||
style: 'backdrop-filter: blur(20px)'
|
||||
}
|
||||
}"
|
||||
modal
|
||||
>
|
||||
<template #container="{ closeCallback }">
|
||||
<MarkdownEdit :word-id="selectWord" :preview="isPreview" :template="templateContent"/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model:visible="templateVisible"
|
||||
modal
|
||||
header="选择一个模板"
|
||||
>
|
||||
<div class="template-layout">
|
||||
<div class="template-item" @click="selectTemplate('')">
|
||||
<doc-icons-word/>
|
||||
<h5>不使用模板</h5>
|
||||
</div>
|
||||
<div class="template-item" v-for="t in templateList" :key="t.name" @click="selectTemplate(t.content)">
|
||||
<doc-icons-word/>
|
||||
<h5>{{t.name}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<Toolbar class="inspection-toolbar">
|
||||
<template #start>
|
||||
<input class="search" placeholder="搜索"/>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="toolbar-start">
|
||||
<button @click="templateVisible=true">
|
||||
<Icon name="Plus" :stroke-width="1.2" />
|
||||
<span>新建</span>
|
||||
</button>
|
||||
<button @click="getWordList" >
|
||||
<Icon name="ListRestart" :stroke-width="1.2" />
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<div class="inspection-content">
|
||||
<div class="doc-item" v-for="doc in documentList" :key="doc.wordId">
|
||||
<div class="icon">
|
||||
<doc-icons-word/>
|
||||
<h5>{{doc.wordName}}</h5>
|
||||
</div>
|
||||
<div class="info">
|
||||
<p>创建日期:</p>
|
||||
<p>{{doc.createAt}}</p>
|
||||
<p>最后修改日期:</p>
|
||||
<p>{{doc.lastModifyAt}}</p>
|
||||
<p>大小:</p>
|
||||
<p>{{doc.fileSize}} KiB</p>
|
||||
<p>创建者:</p>
|
||||
<p>{{doc.userName}}</p>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button>删除</button>
|
||||
<button @click="selectWordChange(doc.wordId,true)">查看</button>
|
||||
<button @click="selectWordChange(doc.wordId)">编辑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.inspection-layout{
|
||||
width: 100%;
|
||||
min-height: 800px;
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*2;
|
||||
container-type: layout/inline-size;
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
}
|
||||
.inspection-toolbar{
|
||||
border: $border;
|
||||
}
|
||||
.toolbar-start{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
button{
|
||||
border: unset;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
gap: $gap*.5;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $radius;
|
||||
background: unset;
|
||||
svg,span{
|
||||
stroke: rgba(51, 51, 51, 0.6);
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
}
|
||||
&:hover,*:hover{
|
||||
cursor: pointer;
|
||||
svg,span{
|
||||
stroke: rgba(51, 51, 51, 1);
|
||||
color: rgba(51, 51, 51, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.inspection-content{
|
||||
border-radius: $radius;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(20, 1fr);
|
||||
gap: $gap*2;
|
||||
}
|
||||
@for $i from 1 through 20 {
|
||||
@media (min-width: #{$i * 450}px) {
|
||||
.inspection-content {
|
||||
grid-template-columns: repeat($i, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-item{
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
display: grid;
|
||||
padding: $padding;
|
||||
grid-template-columns: 80px 1fr;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: $gap*2;
|
||||
.icon{
|
||||
width: 100%;
|
||||
height: min-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*.5;
|
||||
padding-bottom: $padding*.5;
|
||||
align-items: center;
|
||||
background: $light-bg-underline-color;
|
||||
border-radius: $radius;
|
||||
h5{
|
||||
text-align: center;
|
||||
padding: 0 $padding*.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
.info{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-row-gap: $gap*.5;
|
||||
}
|
||||
.action{
|
||||
display: flex;
|
||||
grid-column: 1/3;
|
||||
border-top: $border;
|
||||
padding: $padding 0 0;
|
||||
gap: $gap;
|
||||
button{
|
||||
flex: 1;
|
||||
background: unset;
|
||||
border: unset;
|
||||
border-radius: $radius;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
button:focus {
|
||||
outline: unset;
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.template-item{
|
||||
width: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*.5;
|
||||
padding-bottom: $padding*.5;
|
||||
align-items: center;
|
||||
background: $light-bg-underline-color;
|
||||
border: $border;
|
||||
border-radius: $radius;
|
||||
h5{
|
||||
text-align: center;
|
||||
padding: 0 $padding*.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
.template-layout{
|
||||
display: grid;
|
||||
grid-template-columns:repeat(3,minmax(0,auto));
|
||||
gap: $gap;
|
||||
.template-item:first-of-type{
|
||||
svg{
|
||||
filter: grayscale(100%)
|
||||
}
|
||||
}
|
||||
}
|
||||
.search{
|
||||
border: $border;
|
||||
padding: $padding*.5;
|
||||
border-radius: $radius;
|
||||
background: rgba(51, 51, 51, 0.1);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,113 @@
|
|||
<script setup lang="ts">
|
||||
import UserList from "~/components/ServerUserPage/UserList.vue";
|
||||
import MiniCard from "~/components/Cards/MiniCard.vue";
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import Process from "~/pages/host/process.vue";
|
||||
import NetworkList from "~/pages/host/networkList.vue";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'main',
|
||||
middleware: ['auth']
|
||||
})
|
||||
const reload = ref(false)
|
||||
const pageReload=ref(false)
|
||||
const route = useRoute()
|
||||
const mainLayoutStore=useMainLayoutStore()
|
||||
const userId=computed(()=>route.params.id as string)
|
||||
watch(()=>userId.value,()=>{
|
||||
reload.value=true;
|
||||
nextTick(()=>{
|
||||
reload.value=false;
|
||||
})
|
||||
})
|
||||
watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
||||
pageReload.value=true;
|
||||
setTimeout(()=> {
|
||||
pageReload.value = false;
|
||||
},400)
|
||||
})
|
||||
const processList=ref<string[]>([])
|
||||
const updateProcessList=(data:string[])=>{
|
||||
processList.value=data
|
||||
}
|
||||
watch(() => mainLayoutStore.SelectServer.id, () => {
|
||||
navigateTo('/serverUser/all')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="server-user-layout" v-if="!pageReload">
|
||||
<UserList />
|
||||
<XScroll>
|
||||
<div class="top" v-if="!reload&&userId!=='all'">
|
||||
<MiniCard :title="`CPU使用率-用户${userId}`" :watcher="`CpuUsage-${userId}`"/>
|
||||
<MiniCard :title="`内存总使用率-用户${userId}`" :watcher="`MemoryUsage-${userId}`"/>
|
||||
<MiniCard :title="`磁盘总使用率`" watcher="DiskTotalUsage"/>
|
||||
<MiniCard :title="`网络接口使用率`" watcher="InterfaceTotalUtilizationPercentage"/>
|
||||
</div>
|
||||
<div class="top" v-if="!reload&&userId==='all'">
|
||||
<MiniCard title="CPU总使用率" :watcher="`CpuTotalUsage`"/>
|
||||
<MiniCard title="内存总使用率" :watcher="`MemoryTotalUsage`"/>
|
||||
<MiniCard title="磁盘总使用率" watcher="DiskTotalUsage"/>
|
||||
<MiniCard title="网络接口使用率" watcher="InterfaceTotalUtilizationPercentage"/>
|
||||
</div>
|
||||
</XScroll>
|
||||
<div class="box" >
|
||||
<NuxtPage v-if="!reload"/>
|
||||
</div>
|
||||
<div class="bottom-box">
|
||||
<div class="process-box" v-if="!reload">
|
||||
<process :user-name="userId==='all'?'':userId" @update="updateProcessList" />
|
||||
</div>
|
||||
<div class="net-box" v-if="!reload">
|
||||
<network-list :filter="userId!=='all'" :pids="processList"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.server-user-layout{
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(auto,400px) 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 800px;
|
||||
gap: $gap*2;
|
||||
}
|
||||
.top{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
width: 100%;
|
||||
>*{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.box{
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
padding: $padding;
|
||||
}
|
||||
.bottom-box{
|
||||
border-radius: $radius;
|
||||
display: flex;
|
||||
gap: $gap*2;
|
||||
grid-column: 1/3;
|
||||
min-height: 1000px;
|
||||
}
|
||||
//窗体小于800
|
||||
@media screen and (max-width: 1600px) {
|
||||
.bottom-box{
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.process-box,.net-box{
|
||||
flex: 1;
|
||||
border: $border;
|
||||
border-radius: $radius;
|
||||
background: $light-bg-color;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,63 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import AreaChart2 from "~/components/Charts/AreaChart2.vue";
|
||||
|
||||
const route = useRoute()
|
||||
const userId=toRef(route.params.id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-info-layout">
|
||||
<div class="cpu-chart">
|
||||
<AreaChart2 :value-ids="[userId==='all'?'CpuTotalUsage':`CpuUsage-${userId}`]" :value-names="['CPU使用率']" :colors="['#0073f4']" :bg-color="'rgba(0,115,244,0.3)'" :configs="[
|
||||
{
|
||||
areaStyle:{}
|
||||
}
|
||||
]"/>
|
||||
|
||||
</div>
|
||||
<div class="memory-chart">
|
||||
<AreaChart2 :bg-color="'rgba(38,153,100,0.3)'" :colors="['#269964']" :value-names="['内存使用率']" :value-ids="[userId==='all'?'MemoryTotalUsage':`MemoryUsage-${userId}`]" :configs="[
|
||||
{
|
||||
areaStyle:{}
|
||||
}
|
||||
]"/>
|
||||
</div>
|
||||
<div class="processes-chart">
|
||||
<AreaChart2 :bg-color="'rgba(208,157,209,0.3)'" :colors="['#d09dd1']" :value-names="['进程数']" :value-ids="[`UserProcesses-${userId}`]" :configs="[
|
||||
{
|
||||
areaStyle:{}
|
||||
}
|
||||
]"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.user-info-layout{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 100%;
|
||||
gap: $gap*2;
|
||||
}
|
||||
.cpu-chart {
|
||||
border: 4px solid #0073f4;
|
||||
border-radius: $radius*2;
|
||||
height: 32rem;
|
||||
overflow: hidden;
|
||||
grid-column: 1/3;
|
||||
}
|
||||
.memory-chart {
|
||||
border: 4px solid #269964;
|
||||
border-radius: $radius*2;
|
||||
height: 32rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.processes-chart{
|
||||
border: 4px solid #d09dd1;
|
||||
border-radius: $radius*2;
|
||||
height: 32rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'main',
|
||||
middleware: ['auth']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-info-layout">
|
||||
<div class="user-card">
|
||||
<div class="user-card-top">
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" image="https://api.multiavatar.com/asdasd.svg"/>
|
||||
<h2>sdasdas</h2>
|
||||
<p>1231231231sdasd2</p>
|
||||
<Tag>思科交换机哈克斯的</Tag>
|
||||
</div>
|
||||
<div class="user-card-bottom">
|
||||
<div>
|
||||
<p>asdas</p>
|
||||
<p>sadasd</p>
|
||||
</div><div>
|
||||
<p>asdas</p>
|
||||
<p>sadasd</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.user-info-layout{
|
||||
width: 100%;
|
||||
display: grid;
|
||||
min-height: 800px;
|
||||
border-radius: $radius;
|
||||
grid-template-columns: minmax(300px,400px) 3fr;
|
||||
gap: $gap*4;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
.user-card{
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius;
|
||||
border: 1px solid rgba(51, 51, 51, 0.17);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
.user-card-top{
|
||||
padding: $padding*3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $gap;
|
||||
.p-avatar{
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p{
|
||||
color: #333;
|
||||
//字符间距
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.p-tag{
|
||||
letter-spacing: 1px;
|
||||
font-size: 13px;
|
||||
padding: $padding*.5;
|
||||
}
|
||||
}
|
||||
.user-card-bottom{
|
||||
border-top: 1px solid rgba(51, 51, 51, 0.17);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
padding: $padding $padding 0;
|
||||
div{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
p{
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-info{
|
||||
background: $light-bg-color;
|
||||
border-radius: $radius;
|
||||
padding: $padding*3;
|
||||
border: 1px solid rgba(51, 51, 51, 0.17);
|
||||
}
|
||||
</style>
|
Binary file not shown.
8642
web/yarn.lock
8642
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue