This commit is contained in:
niyyzf 2024-06-29 18:16:29 +08:00
parent dfbc1cfdb0
commit e77c7fa2df
73 changed files with 3401 additions and 9839 deletions

View File

@ -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

View File

@ -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">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Users\niyyz\.nuget\packages\czgl.systeminfo\2.2.0\lib\net7.0\CZGL.SystemInfo.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>

View File

@ -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; }

View File

@ -0,0 +1,3 @@
[CpuTotalUsage_d3YT]
Notify=20
Warning=30

View File

@ -1,2 +0,0 @@
[CpuTotalUsage]
Value=80

View File

@ -69,3 +69,11 @@ Description = A simple job that uses the Network
JobType = LoongPanel_Asp.Jobs.NetworkTotalJob, LoongPanel-Asp
Executor = d3YT,xseg
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 * * * * ? *

View File

@ -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

View File

@ -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"));
//判断用户名,邮箱是否唯一
var user = await userManager.FindByNameAsync(model.UserName);
if (user != null) return BadRequest("用户名已存在");
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
//创建用户
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 userManager.CreateAsync(user, model.Password);
if (!result.Succeeded) return BadRequest("无法创建用户,"+string.Join(",",result.Errors.ToList().Select(e=>e.Description)));
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"));
//添加用户到默认角色
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)

View File

@ -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 });
}
}

View File

@ -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,
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
};
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);
// 将新的配置对象序列化为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; }
}

View File

@ -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);
}
}
}
}

View File

@ -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 ?? [];
}

View File

@ -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);

View File

@ -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);
}
}

62
LoongPanel-Asp/Init.cs Normal file
View File

@ -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;
}
}
}

View File

@ -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);
});
}

View File

@ -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 =>

View File

@ -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"];

View File

@ -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)

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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))
@ -24,11 +24,16 @@ public class ApiPermissionMiddleware(
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)))
{

View File

@ -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"];

View File

@ -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

View File

@ -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();

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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",

View File

@ -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"
}
}
}
}
}

View File

@ -0,0 +1,31 @@
# 巡检记录
## 第一部分:基本信息
- **巡检日期**2023 年 11 月 8 日
- **巡检人员**:张三
- **巡检部门**:设备维护部
## 第二部分:巡检项目
| 序号 | 巡检项目名称 | 巡检要点 | 巡检内容 |
| ---- | ------------ | -------------------------------------------------- | -------------------------------------------------------------- |
| 1 | 设备运行状况 | 检查设备运行是否平稳,有无异常振动或噪音。 | 详细记录设备运行参数,比较历史数据,分析是否存在异常。 |
| 2 | 安全防护措施 | 检查安全防护设施是否完好,如安全栅栏、警示标志等。 | 确认所有安全设施无损坏,位置正确,且工作人员了解如何正确使用。 |
| 3 | 环境卫生状况 | 检查工作区域是否清洁,有无垃圾或障碍物。 | 清理工作区域,确保无杂物,保持环境整洁。 |
|...|.....|....|...|
## 第三部分:检查记录
| 序号 | 检查项目 | 检查结果 | 异常说明 | 处理措施 | 反馈意见 |
| ---- | -------- | -------- | ---------------- | ------------------------------ | ---------------------- |
| 1 | 外壳 | 正常 | 无 | 无 | 无 |
| 2 | 电源 | 异常 | 设备有轻微振动。 | 已联系维修人员,计划明日检修。 | 建议增加设备维护频率。 |
| 3 | 主机 | 正常 | 无 | 无 | 无 |
|...|.....|....|...|...|...|
## 第四部分:巡检总结
- **巡检总体评价**:本次巡检总体情况良好,除设备 2 存在轻微振动外,其他各项指标均正常。
- **重点问题处理**:设备 2 的振动问题已安排维修人员进行检修,预计明日完成。
- **后续工作建议**:建议加强设备日常维护,特别是对老旧设备进行更频繁的检查,以防止潜在的安全隐患。

BIN
LoongPanel-Asp/my.pfx Normal file

Binary file not shown.

View File

@ -1 +1 @@
API_SERVER="http://192.168.0.13:5253"
API_SERVER="https://192.168.0.13:7233"

View File

@ -15,4 +15,5 @@
:root {
--primary-color: $primary-color
}
</style>

View File

@ -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;

BIN
web/bun.lockb Normal file

Binary file not shown.

View File

@ -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(),

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}

View File

@ -13,10 +13,6 @@
target="_blank">
原神玩的队</a></p>
</div>
<!-- 吉祥物-->
<div class="Fox">
<iframe src="/Fox/index.html"></iframe>
</div>
</div>
</template>

View File

@ -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(() => {

View File

@ -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"/>

View File

@ -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

28
web/localhost+3-key.pem Normal file
View File

@ -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-----

26
web/localhost+3.pem Normal file
View File

@ -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-----

View File

@ -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'},

View File

@ -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",

View File

@ -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("发生了错误")
}
})
}

View File

@ -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>

View File

@ -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(() => {
//dataminutesTemp
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;
}

View File

@ -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%;
}
}
}

View File

@ -1,11 +0,0 @@
<script lang="ts" setup>
</script>
<template>
<p>{{ $route.params.id }}</p>
</template>
<style lang="scss" scoped>
</style>

View File

@ -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(() => {
//dataminutesTemp
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;
}
}
}

View File

@ -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>

View File

@ -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(() => {
//pidspidpids
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'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='process'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('pid')">
<div>
<p>进程ID</p>
<Icon v-if="currentSort==='pid'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='pid'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('netId')">
<div>
<p>类型</p>
<Icon v-if="currentSort==='netId'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='netId'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('recvQ')">
<div>
<p>收包数</p>
<Icon v-if="currentSort==='recvQ'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='recvQ'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('sendQ')">
<div>
<p>发包数</p>
<Icon v-if="currentSort==='sendQ'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='sendQ'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('addressForm')">
<div>
<p>发起地址</p>
<Icon v-if="currentSort==='addressForm'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='addressForm'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th><th>
<div @click="sort('addressTo')">
<p>接收地址</p>
<Icon v-if="currentSort==='addressTo'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='addressTo'&&currentSortDir==='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>

223
web/pages/host/process.vue Normal file
View File

@ -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'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='processName'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('pid')">
<div>
<p>进程ID</p>
<Icon v-if="currentSort==='pid'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='pid'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('user')">
<div>
<p>用户</p>
<Icon v-if="currentSort==='user'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='user'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('cpu')">
<div>
<p>CPU使用率</p>
<Icon v-if="currentSort==='cpu'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='cpu'&&currentSortDir==='asc'" name="ArrowUpNarrowWide"></Icon>
</div>
</th>
<th @click="sort('memory')">
<div>
<p>内存使用率</p>
<Icon v-if="currentSort==='memory'&&currentSortDir==='desc'" name="ArrowDownNarrowWide"></Icon>
<Icon v-if="currentSort==='memory'&&currentSortDir==='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>

View File

@ -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>

113
web/pages/serverUser.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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.

File diff suppressed because it is too large Load Diff