This commit is contained in:
niyyzf 2024-06-22 10:54:02 +08:00
commit dfbc1cfdb0
146 changed files with 21508 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/LoongPanel-Asp/bin/
/LoongPanel-Asp/app.db
/LoongPanel-Asp/app.db-shm
/LoongPanel-Asp/app.db-wal
/LoongPanel-Asp/temp.db
/LoongPanel-Asp/obj/
/LoongPanel-Asp/Migrations/
/LoongPanel-Asp/Entities/

22
LoongPanel-Asp.sln Executable file
View File

@ -0,0 +1,22 @@

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
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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

@ -0,0 +1,4 @@
<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: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

@ -0,0 +1,210 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using LoongPanel_Asp.Models;
using LoongPanel_Asp.utils;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace LoongPanel_Asp;
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: IdentityDbContext<ApplicationUser>(options)
{
public DbSet<ApiPermission> ApiPermissions { get; set; }
public DbSet<RotePermission> RotePermissions { get; set; }
public DbSet<ApplicationRole> ApplicationRoles { get; set; }
public DbSet<ServerMonitoringData> ServerMonitoringData { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置 Identity 框架的默认实体
modelBuilder.Entity<ApplicationUser>().ToTable("Users");
modelBuilder.Entity<ApplicationRole>().ToTable("Roles");
modelBuilder.Entity<IdentityRole>().ToTable("Roles");
modelBuilder.Entity<IdentityUserClaim<string>>().ToTable("UserClaims");
modelBuilder.Entity<IdentityUserRole<string>>().ToTable("UserRoles");
modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("UserLogins");
modelBuilder.Entity<IdentityRoleClaim<string>>().ToTable("RoleClaims");
modelBuilder.Entity<IdentityUserToken<string>>().ToTable("UserTokens");
// 配置 ApiPermission 实体
modelBuilder.Entity<ApiPermission>(entity =>
{
entity.ToTable("ApiPermissions");
entity.HasKey(ap => ap.Id);
entity.Property(ap => ap.Name).HasMaxLength(255).IsRequired()
.HasColumnType("varchar(255)");
entity.Property(ap => ap.Controller).HasMaxLength(100).IsRequired()
.HasColumnType("varchar(100)");
});
modelBuilder.Entity<RotePermission>(entity =>
{
entity.ToTable("RotePermissions");
entity.HasKey(rp => rp.Id);
entity.Property(rp => rp.Name).HasMaxLength(255).IsRequired()
.HasColumnType("varchar(255)");
entity.Property(rp => rp.Router).HasMaxLength(100).IsRequired()
.HasColumnType("varchar(100)");
});
// 配置 ApplicationRole 实体
modelBuilder.Entity<ApplicationRole>(entity =>
{
entity.Property(r => r.ApiPermissions)
.HasColumnType("longtext")
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
).Metadata.SetValueComparer(new ListOfStringValueComparer());
entity.Property(r => r.RouterPermissions)
.HasColumnType("longtext")
.HasConversion(
v => string.Join(',', v),
v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
).Metadata.SetValueComparer(new ListOfStringValueComparer());
});
// 配置 ServerMonitoringData 实体
modelBuilder.Entity<ServerMonitoringData>(entity =>
{
entity.ToTable("ServerMonitoringData");
//id 自增主键
entity.Property(smd => smd.Id).HasMaxLength(100).ValueGeneratedOnAdd();
entity.Property(smd => smd.Time).HasMaxLength(100).IsRequired().HasConversion(
v => v!.Value.ToUniversalTime(), v => v.ToLocalTime());
entity.Property(smd => smd.ServerId).HasMaxLength(100).IsRequired();
entity.Property(smd => smd.Data).HasMaxLength(100).IsRequired();
entity.Property(smd => smd.DataName).HasMaxLength(100).IsRequired();
entity.Property(smd => smd.DataType).HasMaxLength(100).IsRequired();
});
InitializeRoles(modelBuilder);
}
private static void InitializeRoles(ModelBuilder modelBuilder)
{
string[] roleNames = ["Admin", "User"];
foreach (var roleName in roleNames)
{
//如果是管理员角色,则赋予所有权限
if (roleName == "Admin")
{
modelBuilder.Entity<ApplicationRole>().HasData(new ApplicationRole
{
Id = roleName.ToLowerInvariant(),
Name = roleName,
NormalizedName = roleName.ToUpperInvariant(),
ApiPermissions = ["*"],
RouterPermissions = ["*"]
});
continue;
}
modelBuilder.Entity<ApplicationRole>().HasData(new ApplicationRole
{
Id = roleName.ToLowerInvariant(),
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"]
});
}
var apiRouterPermissions = ControllerScanner.GetApiPermissions();
foreach (var permission in apiRouterPermissions)
modelBuilder.Entity<ApiPermission>().HasData(new ApiPermission
{
Id = permission.Id,
Name = permission.Name,
Controller = permission.Controller
});
//创建列表 {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" }
];
foreach (var permission in rotePermissions) modelBuilder.Entity<RotePermission>().HasData(permission);
}
}
public class ApplicationUser : IdentityUser
{
[MaxLength(50)] public string? NickName { get; set; }
[MaxLength(255)] public required string Avatar { get; set; }
[MaxLength(255)] public string? Desc { get; set; }
//职位
[MaxLength(255)] public required string Posts { get; set; }
[MaxLength(255)] public string? Address { get; set; }
[MaxLength(255)] public DateTime? LastLoginTime { get; set; }
[MaxLength(255)] public required DateTime CreateDate { get; init; }
[MaxLength(255)] public required DateTime ModifiedDate { get; set; }
[MaxLength(255)] public string? PhysicalAddress { get; set; }
}
public class ApplicationRole : IdentityRole
{
public required List<string> ApiPermissions { get; set; }
public required List<string> RouterPermissions { get; set; }
}
public class ApiPermission
{
public required int Id { get; set; }
[MaxLength(255)] public required string Name { get; set; } // 权限名称
[MaxLength(100)] public required string Controller { get; set; } // 接口所在控制器
}
public class RotePermission
{
public int Id { get; init; }
[MaxLength(255)] public required string Name { get; set; } // 权限名称
[MaxLength(100)] public required string Router { get; set; } // 路由名称
}
public class ServerMonitoringData
{
public int? Id { get; set; }
public DateTime? Time { get; set; }
public required string ServerId { get; set; }
public required string? Data { get; set; }
public required string DataName { get; set; }
public required string? DataType { get; set; }
}
public class ListOfStringValueComparer() : ValueComparer<List<string>>((c1, c2) => c1!.SequenceEqual(c2!),
c => c.Aggregate(0, (h, v) => HashCode.Combine(h, v.GetHashCode())),
c => c.ToList());

View File

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

71
LoongPanel-Asp/Configs/jobs.ini Executable file
View File

@ -0,0 +1,71 @@
[CpuTotalJob]
Group = CPU
ValueName = CPU使用率
Description = A simple job that uses the CPU
JobType = LoongPanel_Asp.Jobs.CpuTotalJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 0/3 * * * * ? *
[CpuSingleUsageJob]
Group = CPU
ValueName = CPU单核使用率
Description = A simple job that uses the CPU
JobType = LoongPanel_Asp.Jobs.CpuSingleUsageJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 5/10 * * * * ? *
[CpuSpeedJob]
Group = CPU
ValueName = CPU速度
Description = A simple job that uses the CPU
JobType = LoongPanel_Asp.Jobs.CpuSpeedJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 2/10 * * * * ? *
[ProcessTotalJob]
Group = Process
ValueName = 进程总使用数
Description = A simple job that uses the Process
JobType = LoongPanel_Asp.Jobs.ProcessTotalJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 3/10 * * * * ? *
[PhrasePatternJob]
Group = Process
ValueName = 句柄总使用数
Description = A simple job that uses the Process
JobType = LoongPanel_Asp.Jobs.PhrasePatternJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 6/30 * * * * ? *
[MemoryTotalJob]
Group = Memory
ValueName = 内存总使用率
Description = A simple job that uses the Memory
JobType = LoongPanel_Asp.Jobs.MemoryTotalJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 3/5 * * * * ? *
[DiskTotalJob]
Group = Disk
ValueName = 磁盘总使用率
Description = A simple job that uses the Disk
JobType = LoongPanel_Asp.Jobs.DiskTotalJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 2 0/10 * * * ? *
[DiskUseJob]
Group = Disk
ValueName = 磁盘总数据
Description = A simple job that uses the Disk
JobType = LoongPanel_Asp.Jobs.DiskUseJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 4/10 * * * * ? *
[NetworkTotalJob]
Group = Network
ValueName = 网络总使用率
Description = A simple job that uses the Network
JobType = LoongPanel_Asp.Jobs.NetworkTotalJob, LoongPanel-Asp
Executor = d3YT,xseg
CronExpression = 3/15 * * * * ? *

View File

@ -0,0 +1,15 @@
[d3YT]
address = 192.168.0.26
port = 22
serverName = 龙芯
password = loongnix
username = loongnix
https = false
[xseg]
address = 127.0.0.1
port = 22
serverName = 本机
password = 123123
username = zwb
https = false

View File

@ -0,0 +1,203 @@
using System.Security.Claims;
using LiteDB;
using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Models;
using LoongPanel_Asp.Types;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace LoongPanel_Asp.Controllers;
[ApiController]
[Route("Api/[controller]")]
public class AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
EmailHelper emailHelper,
TokenHelper tokenHelper,
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"));
try
{
// 获取code,email
var col = db.GetCollection<EmailCode>("EmailCode");
// 使用email查询
var userCode = col.FindOne(x => x.Email == model.Email);
// 判断结果
if (userCode == null) return BadRequest(new ApiResponse(ApiResponseState.Error, "Code not found"));
if (userCode.Code != model.Code)
return BadRequest(new ApiResponse(ApiResponseState.Error, "Code does not match"));
// 判断是否过期
if (userCode.ExpireTime < DateTime.Now)
return BadRequest(new ApiResponse(ApiResponseState.Error, "Code expired"));
// 注册用户
if (HttpContext.RequestServices.GetService(typeof(UserManager<ApplicationUser>)) is not
UserManager<ApplicationUser> manager)
return StatusCode(StatusCodes.Status500InternalServerError,
new ApiResponse(ApiResponseState.Error, "UserManager not found"));
var user = new ApplicationUser
{
UserName = model.UserName,
Email = model.Email,
PhoneNumber = model.Phone,
NickName = model.NickName,
EmailConfirmed = true,
Avatar = $"https://api.multiavatar.com/{model.UserName}.svg",
Posts = "员工",
CreateDate = DateTime.UtcNow,
ModifiedDate = DateTime.UtcNow
};
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"));
}
catch (Exception ex)
{
// Log the exception
Console.WriteLine(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request.");
}
}
[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)
{
if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request"));
try
{
ApplicationUser? user = null;
// 判断字符串是否包含@
if (model.EmailOrUserName.Contains('@'))
user = await userManager.FindByEmailAsync(model.EmailOrUserName);
else
user = await userManager.FindByNameAsync(model.EmailOrUserName);
if (user == null) return Ok(new ApiResponse(ApiResponseState.Error, "Invalid email or username"));
Console.WriteLine(user.UserName);
var result = await signInManager.CheckPasswordSignInAsync(user, model.Password, model.RememberMe);
if (!result.Succeeded) return Ok(new ApiResponse(ApiResponseState.Error, "Invalid password"));
var roles = await userManager.GetRolesAsync(user);
var roleId = roles.ToList()[0]; // 直接获取角色ID列表
var claimsIdentity = new ClaimsIdentity(new[]
{
// userId
new Claim(ClaimTypes.NameIdentifier, user.Id),
// email
new Claim(ClaimTypes.Email, user.Email!),
// role
new Claim(ClaimTypes.Role, roleId.ToLower()) // 将角色ID列表转换为逗号分隔的字符串
});
var token = tokenHelper.GenerateToken(claimsIdentity);
return Ok(new ApiResponse(ApiResponseState.Success, "Login successful", new
{
user.UserName,
Token = token
}));
;
}
catch (Exception ex)
{
// Log the exception
// logger.LogError(ex, "An error occurred while processing the login request.");
return StatusCode(StatusCodes.Status500InternalServerError,
new ApiResponse(ApiResponseState.Error, ex.Message));
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc;
namespace LoongPanel_Asp.Controllers;
[ApiController]
[Route("Api/[controller]")]
public class JobController(ApplicationDbContext dbContext) : ControllerBase
{
[HttpGet("GetJobList")]
public IActionResult GetJobList(string serverId)
{
var serverMonitoringData = dbContext.ServerMonitoringData;
//获得所有DataName类型
var dataNameTypesWithDataType = serverMonitoringData
.GroupBy(x => x.DataName) // 假设DataName是ServerMonitoringData实体中的一个属性
.Select(group => new
{
DataName = group.Key, group.First().DataType // 假设DataType是ServerMonitoringData实体中的一个属性
})
.ToList();
return Ok(dataNameTypesWithDataType); // 返回所有唯一的DataName类型及其对应的DataType
}
}

View File

@ -0,0 +1,31 @@
using System.Security.Claims;
using System.Text.RegularExpressions;
using LoongPanel_Asp.Helpers;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace LoongPanel_Asp.Controllers;
[ApiController]
[Route("Api/[controller]")]
public class RoteController(
RoleManager<ApplicationRole> roleManager,
ApplicationDbContext dbContext) : ControllerBase
{
[HttpGet("RoteVerify")]
public async Task<IActionResult> RoteVerify(string path)
{
var roleId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Role)!.Value;
//获得role
Console.WriteLine(roleId);
var role = await roleManager.FindByNameAsync(roleId);
var rotes = role!.RouterPermissions.ToList();
//获取路由列表
var apiPermissions = dbContext.RotePermissions.ToList().Select(x => x.Router);
//将path全部小写
path = path.ToLower();
//使用正则匹配
var firstOrDefault = apiPermissions.FirstOrDefault(x => Regex.IsMatch(path,x));
return Ok(firstOrDefault != null ? new ApiResponse(ApiResponseState.Success) : new ApiResponse(ApiResponseState.Forbidden));
}
}

View File

@ -0,0 +1,278 @@
using System.Collections.Concurrent;
using System.Collections.Specialized;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LoongPanel_Asp.Controllers;
[ApiController]
[Route("Api/[controller]")]
public class ServerController(IServiceProvider serviceProvider, ApplicationDbContext dbContext) : ControllerBase
{
//获取远程服务器地址列表
[HttpGet("GetServerList")]
public IActionResult GetServerList()
{
var serverConfigs=JobConfigHelper.GetServers();
//读取每一个配置项转换为列表
var serverList = serverConfigs.Select(section => new ServerInfo
{
Name = section.ServerName,
Id = section.Id
}).ToList();
return Ok(serverList);
}
[HttpGet("GetServerInfo")]
public async Task<IActionResult> GetServerInfo(string serverId)
{
var sshClient = serviceProvider.GetService<SshService>();
var output = await sshClient?.ExecuteCommandAsync(serverId, "neofetch", "--off", "|", "sed",
@"'s/\x1B\[[0-9;]*[a-zA-Z]//g'")!;
if (string.IsNullOrEmpty(output)) return BadRequest();
Dictionary<string, string> paresData = new();
var pattern = @"^(.*?)\s*:\s*(.*?)\s*$";
var matches = Regex.Matches(output ?? "", pattern, RegexOptions.Multiline);
foreach (Match match in matches) paresData[match.Groups[1].Value] = match.Groups[2].Value;
return Ok(paresData);
}
[HttpGet("GetServerHistoryDate")]
public async Task<IActionResult> GetServerHistoryDate([FromQuery] string serverId,
[FromQuery] List<string?>? dataTypes = null, [FromQuery] int? startIndex = 0)
{
if (string.IsNullOrWhiteSpace(serverId)) return BadRequest("ServerId is required.");
var query = dbContext.ServerMonitoringData
.Where(s => s.ServerId == serverId && s.DataType != null);
if (dataTypes != null && dataTypes.Any()) query = query.Where(s => dataTypes.Contains(s.DataType));
var allData = await query
.OrderByDescending(s => s.Time)
.Skip(startIndex ?? 0)
.Take(1000)
.ToListAsync();
if (allData.Count == 0) return Ok(new { done = true });
// 获取时间集合并排序
var timeList = allData.Select(s => s.Time).Distinct().OrderBy(s => s).ToList();
// 获取DataType
var groupedData = new ConcurrentDictionary<string, List<double>>();
var distinctDataTypes = dataTypes ?? allData.Select(s => s.DataType).Distinct().ToList();
foreach (var dataType in distinctDataTypes)
{
var dataList = allData.Where(s => s.DataType == dataType).ToList();
var backDataList = new List<double>();
var temp = double.Parse(dataList[0].Data ?? string.Empty);
foreach (var time in timeList)
{
var currentData = dataList.Where(d => d.Time == time).ToList();
if (currentData.Count > 0)
{
//计算data平均值
var dataSum = currentData.Sum(d => double.Parse(d.Data ?? string.Empty));
temp = dataSum;
}
backDataList.Add(temp);
}
if (dataType != null) groupedData[dataType] = backDataList;
}
//timelist转换为当前时间 月 日 分 秒
var timeList2 = timeList.Select(time => time?.ToLocalTime().ToString("MM-dd HH:mm:ss")).ToList();
// 返回结果
var returnValue = new
{
times = timeList2,
data = groupedData,
endIndex = dataTypes is { Count: > 0 } ? startIndex + timeList2.Count * dataTypes.Count :
startIndex + allData.Count < 1000 ? allData.Count : 1000,
done = false
};
return Ok(returnValue);
}
[HttpGet("GetServerCpuInfo")]
public async Task<IActionResult> GetServerCpuInfo(string serverId)
{
var sshClient = serviceProvider.GetService<SshService>();
var output = await sshClient?.ExecuteCommandAsync(false, serverId, "lscpu", "-J")!;
if (string.IsNullOrEmpty(output)) return BadRequest();
return Ok(output);
}
[HttpGet("GetServerHistoryStep")]
public async Task<IActionResult> GetServerHistoryStep([FromQuery] string serverId, [FromQuery] string dataType,
[FromQuery] string timeRange)
{
var query = dbContext.ServerMonitoringData
.Where(s => s.ServerId == serverId && s.DataType != null && s.DataType == dataType);
int timeSpan;
int timeNum;
switch (timeRange.ToLower())
{
case "1m":
timeSpan = 1;
timeNum = 60;
break;
case "1h":
timeSpan = 60;
timeNum = 60;
break;
case "1d":
timeSpan = 60 * 12;
timeNum = 12;
break;
default:
return BadRequest("Invalid time range.");
}
var time = DateTime.UtcNow;
var timeList = new List<DateTime>();
var dataList = new List<string>();
for (var i = 0; i < timeNum; i++)
{
var endTime = time.AddSeconds(-(timeSpan * i) + 3 * timeSpan);
var startTime = time.AddSeconds(-(timeSpan * i) - 3 * timeSpan);
var data = query.FirstOrDefault(d => d.Time <= endTime && d.Time >= startTime);
var value = "0";
if (data is { Data: not null }) value = data.Data;
timeList.Add(startTime);
dataList.Add(value);
}
dataList.Reverse();
var backData = new
{
data = dataList,
time = timeList.Select(dateTime => dateTime.ToLocalTime().ToString("MM-dd HH:mm:ss")).ToList()
};
return Ok(backData);
}
[HttpGet("GetServerMemoryInfo")]
public async Task<IActionResult> GetServerMemoryInfo([FromQuery] string serverId)
{
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(false, serverId,"echo",$"'{server.Password}'","|","sudo","-S","lshw","-class","memory","-json")!;
if (string.IsNullOrEmpty(output)) return BadRequest();
return Ok(output);
}
[HttpGet("GetServerDiskList")]
public async Task<IActionResult> GetServerDiskList([FromQuery] string serverId)
{
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 fdisk -l","|","grep 'Disk /'","|","awk '{print $2,$3}'")!;
if (string.IsNullOrEmpty(output)) return BadRequest();
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);
}
[HttpGet("GetServerNetworkEquipmentList")]
public async Task<IActionResult> GetServerNetworkEquipmentList([FromQuery] string serverId)
{
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,"ip link show" ,"|","grep '^[0-9]'","|","awk -F': ' '{print $2}'")!;
if (string.IsNullOrEmpty(output)) return BadRequest();
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
return Ok(data);
}
[HttpGet("GetServerGpuList")]
public async Task<IActionResult> GetServerGpuList([FromQuery] string serverId)
{
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,"lspci" ,"|","awk '/VGA/ {print $1}' ")!;
if (string.IsNullOrEmpty(output)) return BadRequest();
var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
return Ok(data);
}
[HttpGet("GetServerDiskInfo")]
public async Task<IActionResult> GetServerDiskInfo([FromQuery] string serverId,[FromQuery] string diskId)
{
var sshClient = serviceProvider.GetService<SshService>();
var serverConfigs = JobConfigHelper.GetServers().ToList();
var server = serverConfigs.Find(x =>x.Id == serverId);
if (server == null) return BadRequest();
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 diskInfo = output
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim().Split(':'))
.ToList().Select(x => new
{
name=x[0].Replace(" ","_"),
value=x[1]
}).ToList();
return Ok(diskInfo);
}
[HttpGet("GetServerTerminalPath")]
public async Task<IActionResult> GetServerTerminalPath([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))
{
await socket.ConnectAsync("8.8.8.8", 65530);
var endPoint = socket.LocalEndPoint as IPEndPoint;
localIp = endPoint?.Address.ToString();
}
//拼接网络路径
var uriBuilder = new UriBuilder
{
Scheme = "http",
Host = localIp,
Port = 8888,
};
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
query["hostname"]=serverConfig.Address;
query["username"] = serverConfig.Username;
query["password"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(serverConfig.Password));
query["port"] = serverConfig.Port.ToString();
uriBuilder.Query = query.ToString();
var url = uriBuilder.Uri.ToString();
return Ok(url);
}
}
public class ServerInfo
{
public required string Name { get; init; }
public required string Id { get; init; }
}

View File

@ -0,0 +1,97 @@
using System.Security.Claims;
using LoongPanel_Asp.Helpers;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LoongPanel_Asp.Controllers;
[ApiController]
[Route("Api/[controller]")]
public class UserController(UserManager<ApplicationUser> userManager) : ControllerBase
{
[HttpGet("Info")]
public async Task<IActionResult> Info()
{
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
// 获取用户的信息
var user = await userManager.FindByIdAsync(userId);
// 创建一个匿名对象,只包含您想要公开的信息
var userInfo = new
{
user!.NickName,
user.Id,
user.UserName,
user.NormalizedUserName,
user.Email,
user.NormalizedEmail,
user.EmailConfirmed,
user.PhoneNumber,
user.PhoneNumberConfirmed,
user.TwoFactorEnabled,
user.Avatar,
user.Desc,
user.Posts,
};
// 返回用户信息
return Ok(new ApiResponse(ApiResponseState.Success, "获取成功", userInfo));
}
[HttpPost("GetLayoutConfig")]
public async Task<IActionResult> GetLayoutConfig([FromBody] LayoutModel model)
{
//从session中获取用户信息
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
//从UserConfigs/Layouts/{userId中获取用户配置信息
var fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserConfigs", "Layouts", userId + ".json");
//判断是否存在
if (!System.IO.File.Exists(fullPath))
fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserConfigs", "Layouts", "default.json");
//读取文件内容
var content = await System.IO.File.ReadAllTextAsync(fullPath);
return Ok(model.Layout != content ? content : "null");
}
[HttpPut("PutLayoutConfig")]
public async Task<IActionResult> PutLayoutConfig([FromBody] LayoutModel model)
{
//从session中获取用户信息
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
var fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserConfigs", "Layouts", userId + ".json");
//覆盖文件
await System.IO.File.WriteAllTextAsync(fullPath, model.Layout);
return Ok();
}
[HttpGet("GetUserInfoList")]
public async Task<IActionResult> GetUserInfoList()
{
var users = await userManager.Users.ToListAsync();
// 创建一个匿名对象,只包含您想要公开的信息
var userInfoList = users.Select(user => new
{
user.Address,
user.Avatar,
CreateDate=user.CreateDate.ToLocalTime().ToString("U"),
user.Desc,
user.Email,
user.Id,
user.NickName,
user.PhoneNumber,
LastLoginTime = user.LastLoginTime?.ToLocalTime().ToString("U"),
user.Posts,
ModifiedDate=user.ModifiedDate.ToLocalTime().ToString("U"),
user.UserName,
user.PhysicalAddress,
IsLock=user.LockoutEnd > DateTimeOffset.UtcNow
}).ToList();
return Ok(userInfoList);
}
}
public class LayoutModel
{
public string? Layout { get; set; }
}

View File

@ -0,0 +1,48 @@
namespace LoongPanel_Asp.Helpers;
public enum ApiResponseState
{
Success = 200,
NotFound = 404,
Error = 500,
Forbidden = 403
// 您可以添加更多的响应状态
}
public class ApiResponse
{
public ApiResponse(ApiResponseState state, string message = "", object? data = null, object? error = null)
{
Code = (int)state;
Data = data;
Message = string.IsNullOrEmpty(message) ? state.ToString() : message;
Error = error;
// 根据状态设置默认的消息和错误
switch (state)
{
case ApiResponseState.Success:
Message = message ?? "操作成功";
break;
case ApiResponseState.NotFound:
Message = message ?? "未找到资源";
Error = error ?? new { Message = "请求的资源不存在" };
break;
case ApiResponseState.Forbidden:
Message = message ?? "禁止访问";
Error = error ?? new { Message = "您没有权限访问该资源" };
break;
case ApiResponseState.Error:
default:
Message = message ?? "服务器错误";
Error = error;
break;
}
}
public int Code { get; set; }
public object? Data { get; set; }
public string Message { get; set; }
public object? Error { get; set; }
}

View File

@ -0,0 +1,37 @@
namespace LoongPanel_Asp.Helpers;
public class DataHelper(ApplicationDbContext dbContext)
{
public async Task SaveData(ServerMonitoringData data)
{
// 保存数据到数据库
var dataDb = dbContext.ServerMonitoringData;
//获取当前时间
var time = DateTime.UtcNow;
data.Time = time;
dataDb.Add(data);
//提交
await dbContext.SaveChangesAsync();
}
//批量添加
public async Task SaveData(List<ServerMonitoringData> data)
{
var dataDb = dbContext.ServerMonitoringData;
var time = DateTime.UtcNow;
foreach (var i in data)
{
i.Time = time;
dataDb.Add(i);
}
await dbContext.SaveChangesAsync();
}
public static async Task CheckData(string serverId,string dataType,double value)
{
var alertConfigs = JobConfigHelper.GetAlerts();
var alert=alertConfigs[serverId][dataType];
}
}

View File

@ -0,0 +1,44 @@
using LiteDB;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using MimeKit.Text;
namespace LoongPanel_Asp.Helpers;
public class EmailHelper(
ILiteDatabase? database,
string? smtpHost,
int smtpPort,
string smtpUsername,
string smtpPassword)
{
private readonly string? _smtpHost = smtpHost;
private readonly string _smtpPassword = smtpPassword;
private readonly int _smtpPort = smtpPort;
private readonly string _smtpUsername = smtpUsername;
private ILiteDatabase? _database = database;
public async Task SendVerificationEmailAsync(string toEmail, string code)
{
var emailMessage = new MimeMessage();
emailMessage.From.Add(new MailboxAddress("龙腾云御", _smtpUsername));
emailMessage.To.Add(new MailboxAddress("", toEmail));
var body = $"<h1>请验证您的邮箱</h1><p>这是你的验证码</p><p>{code}</p>";
emailMessage.Body = new TextPart(TextFormat.Html)
{
Text = body
};
using var client = new SmtpClient();
await client.ConnectAsync(_smtpHost, _smtpPort, SecureSocketOptions.SslOnConnect);
await client.AuthenticateAsync(_smtpUsername, _smtpPassword);
await client.SendAsync(emailMessage);
await client.DisconnectAsync(true);
}
public string GenerateVerificationCode()
{
return Guid.NewGuid().ToString()[..6];
}
}

View File

@ -0,0 +1,146 @@
using System.Text;
using IniParser;
using LoongPanel_Asp.Models;
namespace LoongPanel_Asp.Helpers;
public static class JobConfigHelper
{
private static List<ServerModel>? _serverConfigs;
private static Dictionary<string, Dictionary<string, Dictionary<string, double>>>? _alertsConfigs;
public static IEnumerable<JobConfiguration> ReadJobConfigurations()
{
// 创建ini文件读取器
var parser = new FileIniDataParser();
var fullPath = Path.Combine(Environment.CurrentDirectory, "Configs", "jobs.ini");
// 读取ini文件
var data = parser.ReadFile(fullPath, Encoding.UTF8);
// 获取所有section
var sections = data.Sections;
var serverConfigs = GetServers();
// 创建一个jobConfig列表
var jobConfigs = new List<JobConfiguration>();
foreach (var section in sections)
jobConfigs.Add(new JobConfiguration
{
Name = section.SectionName,
Group = section.Keys["Group"],
JobType = section.Keys["JobType"],
Description = section.Keys["Description"],
CronExpression = section.Keys["CronExpression"],
ValueName = section.Keys["ValueName"],
Executor = section.Keys["Executor"].Split(',').Select(executor =>
{
// 查找serverConfigs 中id匹配 未找到则跳过
var server = serverConfigs.FirstOrDefault(server => server.Id == executor);
// 如果找到则返回server对象否则不添加
return server;
}).Where(server => server != null).ToList()
});
// 返回
return jobConfigs;
}
public static List<ServerModel> GetServers()
{
// 检查是否已经读取过配置,如果有,直接返回
if (_serverConfigs != null) return _serverConfigs;
// 创建ini文件读取器
var parser = new FileIniDataParser();
var serverFullPath = Path.Combine(Environment.CurrentDirectory, "Configs", "servers.ini");
var serverData = parser.ReadFile(serverFullPath, Encoding.UTF8);
var serverSections = serverData.Sections;
// 读取配置信息
_serverConfigs = serverSections.Select(section => new ServerModel
{
Id = section.SectionName,
Address = section.Keys["address"],
Port = int.Parse(section.Keys["port"]),
ServerName = section.Keys["serverName"],
Password = section.Keys["password"],
Username = section.Keys["username"],
Http = bool.Parse(section.Keys["https"])
}).ToList();
return _serverConfigs;
}
public static Dictionary<string, Dictionary<string, Dictionary<string, double>>> GetAlerts()
{
if (_alertsConfigs != null) return _alertsConfigs;
// 创建_alertsConfigs
_alertsConfigs = new Dictionary<string, Dictionary<string, Dictionary<string, double>>>();
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 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)
{
// 解析每个section为AlertsModel
var type = section.SectionName;
var value = double.Parse(section.Keys["Value"]);
// 添加到字典
if (serverId != null && !(_alertsConfigs.ContainsKey(serverId)))
{
_alertsConfigs[serverId] = new Dictionary<string, Dictionary<string, double>>();
}
if (serverId != null && !_alertsConfigs[serverId].ContainsKey(type))
{
_alertsConfigs[serverId][type] = new Dictionary<string, double>();
}
if (userId == null) continue;
if (serverId != null) _alertsConfigs[serverId][type][userId] = value;
}
}
return _alertsConfigs;
}
}
public class JobConfiguration
{
//名称
public required string Name { get; init; }
//分组
public required string Group { get; init; }
//描述
public string? Description { get; init; }
//执行者
public required List<ServerModel?> Executor { get; init; }
//任务类型
public required string JobType { get; init; }
//cron表达式
public required string CronExpression { get; init; }
//ValueName
public string? ValueName { get; init; }
}

View File

@ -0,0 +1,50 @@
using System.Text.Json;
using LoongPanel_Asp.Models;
namespace LoongPanel_Asp.Helpers;
public class PrometheusQueryHelper
{
private readonly HttpClient _httpClient = new();
public async Task<string?> QueryPrometheus(string query, string prometheusAddress, int prometheusPort, bool https)
{
try
{
var uriBuilder = new UriBuilder
{
Scheme = https ? "https" : "http", // 这里可以根据需要设置为 http 或 https
Host = prometheusAddress,
Port = prometheusPort,
Path = "/api/v1/query",
Query = $"query={Uri.EscapeDataString(query)}"
};
Console.WriteLine(uriBuilder.ToString());
var response = await _httpClient.GetAsync(uriBuilder.ToString());
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<PrometheusQueryResult>(content);
return result.Data.Result.Last().Value.Last().ToString();
}
catch (JsonException ex)
{
// 这里处理反序列化错误
Console.WriteLine($"Failed to deserialize Prometheus response: {ex.Message}");
return null;
}
catch (HttpRequestException ex)
{
// 这里处理请求错误
Console.WriteLine($"Request to Prometheus failed: {ex.Message}");
return null;
}
catch (Exception ex)
{
// 这里处理其他错误
Console.WriteLine($"An error occurred: {ex.Message}");
return null;
}
}
}

View File

@ -0,0 +1,65 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace LoongPanel_Asp.Helpers;
public class TokenHelper(IConfiguration configuration)
{
public string GenerateToken(ClaimsIdentity claimsIdentity)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Secret"] ?? throw new InvalidOperationException()));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
Issuer = configuration["Issuer"],
Audience = configuration["Audience"],
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public ClaimsPrincipal? ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["Secret"] ?? throw new InvalidOperationException()));
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = configuration["Issuer"],
ValidateAudience = true,
ValidAudience = configuration["Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out var validatedToken);
return tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = configuration["Issuer"],
ValidateAudience = true,
ValidAudience = configuration["Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out _);
}
catch
{
// 如果token无效返回null或者抛出异常
return null;
}
}
}

View File

@ -0,0 +1,90 @@
using System.Security.Claims;
using LiteDB;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
namespace LoongPanel_Asp.Hubs;
public class SessionHub(UserManager<ApplicationUser> userManager, ILiteDatabase database)
: Hub
{
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);
user!.Address = ip?.ToString();
user!.LastLoginTime = DateTime.UtcNow;
// 更新用户信息
await userManager.UpdateAsync(user);
//获取role
var role = await userManager.GetRolesAsync(user!);
// 存储用户对象
var userInfo = new UserInfo
{
Id = userId,
UserName = user!.UserName!,
NickName = user.NickName!,
Avatar = user.Avatar,
Role = role.ToList(),
Posts = user.Posts,
};
var collection = database.GetCollection<UserInfo>("UsersContent");
var existingUserInfo = collection.FindOne(x => x.Id == userId);
if (existingUserInfo != null)
// 更新现有记录
collection.Update(userInfo);
else
// 插入新记录
collection.Insert(userInfo);
var users = collection.FindAll().ToList();
await Clients.All.SendAsync("userJoined", users);
Console.WriteLine($"用户加入:{userId}");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
Console.WriteLine($"用户退出:{userId}");
// 移除用户
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId);
Console.WriteLine($"删除用户:{userId}");
//从数据库中移除
database.GetCollection<UserInfo>("UsersContent").DeleteMany(x => x.Id == userId);
// 通知所有用户有链接离开
await Clients.All.SendAsync("userLeft", userId);
await base.OnDisconnectedAsync(exception);
}
public async Task SendMessage(string userId, string receiver ,string message)
{
Console.WriteLine("12312312312{0},{1}", userId, message);
await Clients.Group(receiver).SendAsync("sendMessage", userId, message);
}
}
//定义类型UserInfo
public class UserInfo
{
public required string Id { get; init; }
public required string UserName { get; init; }
public required string NickName { get; init; }
public required string Avatar { get; init; }
public required List<string> Role { get; init; }
public required string Posts { get; init;}
}

175
LoongPanel-Asp/Jobs/CpuJob.cs Executable file
View File

@ -0,0 +1,175 @@
using System.Globalization;
using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Models;
using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.SignalR;
using Quartz;
using Property = LoongPanel_Asp.Models.Property;
namespace LoongPanel_Asp.Jobs;
public class CpuTotalJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
var cpuDataListAll = new List<ServerMonitoringData>();
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;
output = output.Replace("Average:", "").Replace("all", "").TrimStart();
var cpuList = output.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var cpuProperties = new List<Property>
{
new() { Name = "CpuUserUsage", DisplayName = "CPU用户使用率", Order = 0 },
new() { Name = "CpuSystemUsage", DisplayName = "CPU系统使用率", Order = 1 },
new() { Name = "CpuIOWaitUsage", DisplayName = "CPUIO等待使用率", Order = 2 }
};
var cpuDataList = cpuProperties.Select(property => new ServerMonitoringData { ServerId = server.Id, Data = cpuList[(int)property.Order!], DataName = property.DisplayName, DataType = property.Name }).ToList();
// Calculate CpuTotalUsage separately
var idleUsage = double.Parse(cpuList[5]);
var totalUsage = new ServerMonitoringData
{
ServerId = server.Id,
Data = (100 - idleUsage).ToString(CultureInfo.InvariantCulture),
DataName = "CPU总使用率",
DataType = "CpuTotalUsage"
};
cpuDataList.Add(totalUsage);
cpuDataList.ForEach(data =>
{
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
});
cpuDataListAll.AddRange(cpuDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
// Add to database
await dataHelper.SaveData(cpuDataListAll);
}
}
public class CpuSpeedJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
var cpuDataListAll = new List<ServerMonitoringData>();
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;
//切分每行
var cpuSpeedList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
//获取第一行
var cpuTotalSpeed = cpuSpeedList[0].Split(':', StringSplitOptions.RemoveEmptyEntries)[1].Trim();
var cpuDataList = new List<ServerMonitoringData>();
var totalSpeed = new ServerMonitoringData
{
ServerId = server.Id,
Data = cpuTotalSpeed,
DataName = "CPU总速度",
DataType = "CpuTotalSpeed"
};
cpuDataList.Add(totalSpeed);
//遍历剩下的行
foreach (var (cpuSpeed, index) in cpuSpeedList.Skip(1).Select((x, index) => (x, index)))
{
var speed = cpuSpeed.Split(':', StringSplitOptions.RemoveEmptyEntries)[1].Trim();
var singleSpeed = new ServerMonitoringData
{
ServerId = server.Id,
Data = speed,
DataName = $"CPU单核速度-{index}",
DataType = $"CpuSingleSpeed-{index}"
};
cpuDataList.Add(singleSpeed);
}
cpuDataListAll.AddRange(cpuDataList);
cpuDataList.ForEach(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));
});
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(cpuDataListAll);
}
}
public class CpuSingleUsageJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
var cpuDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var sshClient = serviceProvider.GetService<SshService>();
var output =
await sshClient?.ExecuteCommandAsync(server.Id, "sar -P ALL 3 1", "|", "grep Average", "|",
"awk 'NR>2 {print 100-$NF}'")!;
if (string.IsNullOrEmpty(output)) continue;
var cpuDataList = new List<ServerMonitoringData>();
foreach (var (cpuUsage, index) in output.Split("\n", StringSplitOptions.RemoveEmptyEntries)
.Select((x, index) => (x, index)))
{
var singleUsage = new ServerMonitoringData
{
ServerId = server.Id,
Data = cpuUsage,
DataName = $"CPU单核使用率-{index}",
DataType = $"CpuSingleUsage-{index}"
};
cpuDataList.Add(singleUsage);
}
cpuDataList.ForEach(data =>
{
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
});
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(cpuDataListAll);
}
}

131
LoongPanel-Asp/Jobs/DiskJob.cs Executable file
View File

@ -0,0 +1,131 @@
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 DiskTotalJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
//获得cpu信息
var diskDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var sshClient = serviceProvider.GetService<SshService>();
var diskDataList = new List<ServerMonitoringData>();
var output = await sshClient?.ExecuteCommandAsync(server.Id, "df", "--total", "|", "grep", "total")!;
if (string.IsNullOrEmpty(output)) return;
var disk = output.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var diskTotalUsage = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[4].Replace("%", ""),
DataName = "磁盘总使用率",
DataType = "DiskTotalUsage"
};
await hubContext.Clients.All.SendAsync("ReceiveData", diskTotalUsage.ServerId, diskTotalUsage.DataType,
diskTotalUsage.Data);
diskDataList.Add(diskTotalUsage);
diskDataListAll.AddRange(diskDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(diskDataListAll);
}
}
public class DiskUseJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
//获得cpu信息
var diskDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var sshClient = serviceProvider.GetService<SshService>();
var diskDataList = new List<ServerMonitoringData>();
var output =
await sshClient?.ExecuteCommandAsync(server.Id, "sar -d 3 1", "|", "grep Average:", "|", "awk 'NR>1'","|","awk '{$1=\"\";print $0}'")!;
if (string.IsNullOrEmpty(output)) return;
var lines = output.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
foreach (var line in lines)
{
var disk = line.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var dev = disk[0];
// 每秒传输数
var diskTps = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[1],
DataName = $"磁盘每秒传输数-{dev}" ,
DataType = $"diskTps-{dev}"
};
diskDataList.Add(diskTps);
var diskReadKb = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[2],
DataName = $"磁盘每秒读取数据量-{dev}" ,
DataType = $"diskReadKB-{dev}"
};
diskDataList.Add(diskReadKb);
var diskWriteKb = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[3],
DataName = $"磁盘每秒写入数据量-{dev}" ,
DataType = $"diskWriteKB-{dev}"
};
diskDataList.Add(diskWriteKb);
var diskAwait = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[7],
DataName = $"磁盘平均等待时间-{dev}" ,
DataType = $"diskAwait-{dev}"
};
diskDataList.Add(diskAwait);
var diskUtil = new ServerMonitoringData
{
ServerId = server.Id,
Data = disk[8],
DataName = $"磁盘利用率-{dev}" ,
DataType = $"diskUtil-{dev}"
};
diskDataList.Add(diskUtil);
diskDataList.ForEach(data =>
{
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
});
}
diskDataListAll.AddRange(diskDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(diskDataListAll);
}
}

View File

@ -0,0 +1,94 @@
using System.Globalization;
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 MemoryTotalJob(
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);
// 从JobDataMap中获取参数
var serverList = (List<ServerModel>)dataMap["executor"];
var serverDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var sshClient = serviceProvider.GetService<SshService>();
var output = await sshClient?.ExecuteCommandAsync(server.Id, "free -w", "|", "awk 'NR>1'", "|",
"awk '{$1=\"\";print $0}'", "|", "xargs")!;
if (string.IsNullOrEmpty(output)) continue;
var serverDataList = new List<ServerMonitoringData>();
var dataList = output.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var memoryProperties = new List<Property>
{
new() { Name = "MemoryTotal", DisplayName = "内存总量", Order = 0 },
new() { Name = "MemoryUsed", DisplayName = "内存使用量", Order = 1 },
new() { Name = "MemoryFree", DisplayName = "内存空闲量", Order = 2 },
new() { Name = "MemoryCache", DisplayName = "内存缓存量", Order = 5 },
new() { Name = "SwapTotal", DisplayName = "Swap总量", Order = 7 },
new() { Name = "SwapUsed", DisplayName = "Swap使用量", Order = 8 },
new() { Name = "SwapFree", DisplayName = "Swap空闲量", Order = 9 }
};
memoryProperties.ForEach(data =>
{
var d = new ServerMonitoringData
{
ServerId = server.Id,
Data = double.Parse(dataList[(int)data.Order!]).ToString(CultureInfo.CurrentCulture),
DataName = data.DisplayName,
DataType = data.Name
};
serverDataList.Add(d);
});
//计算内存使用率
var memoryUsed = double.Parse(dataList[1]);
var memoryTotal = double.Parse(dataList[0]);
if (memoryTotal <= 0) memoryTotal = 1;
var memoryUsedRate = memoryUsed / memoryTotal * 100;
var memoryData = new ServerMonitoringData
{
ServerId = server.Id,
Data = memoryUsedRate.ToString(CultureInfo.InvariantCulture),
DataName = "内存总使用率",
DataType = "MemoryTotalUsage"
};
serverDataList.Add(memoryData);
//计算交换使用率
var swapUsed = double.Parse(dataList[8]);
var swapTotal = double.Parse(dataList[7]);
if (swapTotal <= 0) swapTotal = 1;
var swapUsedRate = swapUsed / swapTotal * 100;
var swapData = new ServerMonitoringData
{
ServerId = server.Id,
Data = swapUsedRate.ToString(CultureInfo.InvariantCulture),
DataName = "Swap使用率",
DataType = "SwapTotalUsage"
};
serverDataList.Add(swapData);
serverDataList.ForEach(data =>
{
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
});
serverDataListAll.AddRange(serverDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(serverDataListAll);
}
}

View File

@ -0,0 +1,79 @@
using System.Globalization;
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 NetworkTotalJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
var netWorkDataListAll = new List<ServerMonitoringData>();
foreach (var server in serverList)
{
var sshClient = serviceProvider.GetService<SshService>();
var netWorkDataList = new List<ServerMonitoringData>();
var output = await sshClient?.ExecuteCommandAsync(server.Id, "sar", "-n", "DEV", "3 1", "|", "grep",
"Average:", "|", "awk 'NR>1'", "|", "awk '{$1=\"\";print $0}'")!;
if (string.IsNullOrEmpty(output)) continue;
var lines = output.Split("\n");
var totalUsage = 0.0;
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line)) continue;
var data = line.Split(" ", StringSplitOptions.RemoveEmptyEntries);
var iFace = data[0];
var dataNum = data.Skip(1).Select(double.Parse).ToList();
var netWorkProperties = new List<Property>
{
new() { Name = "ReceivedPacketsPerSecond", DisplayName = "每秒钟接收到的数据包数量", Order = 0 },
new() { Name = "TransmittedPacketsPerSecond", DisplayName = "每秒钟发送的数据包数量", Order = 1 },
new() { Name = "InterfaceUtilizationPercentage", DisplayName = "网络接口的使用率", Order = 7 }
};
netWorkProperties.ForEach(property =>
{
var d = new ServerMonitoringData
{
ServerId = server.Id,
Data = dataNum[(int)property.Order!].ToString(CultureInfo.InvariantCulture),
DataName = $"{property.DisplayName}-{iFace}",
DataType = $"{property.Name}-{iFace}"
};
netWorkDataList.Add(d);
});
totalUsage += dataNum[7];
}
var d = new ServerMonitoringData
{
ServerId = server.Id,
Data = totalUsage.ToString(CultureInfo.InvariantCulture),
DataName = "网络接口总体使用率",
DataType = "InterfaceTotalUtilizationPercentage"
};
netWorkDataList.Add(d);
netWorkDataListAll.AddRange(netWorkDataList);
netWorkDataList.ForEach(data =>
{
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
});
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(netWorkDataListAll);
}
}

View File

@ -0,0 +1,99 @@
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 ProcessTotalJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
var processDataListAll = new List<ServerMonitoringData>();
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>();
var count = int.Parse(output);
var processTotalCount = new ServerMonitoringData
{
ServerId = server.Id,
Data = count.ToString(),
DataName = "进程总数",
DataType = "ProcessTotalCount"
};
processDataList.Add(processTotalCount);
output = await sshClient?.ExecuteCommandAsync(server.Id, "ps", "-eLf", "|", "wc", "-l")!;
if (string.IsNullOrEmpty(output)) continue;
count = int.Parse(output);
var threadsTotalCount = new ServerMonitoringData
{
ServerId = server.Id,
Data = count.ToString(),
DataName = "线程总数",
DataType = "ThreadsTotalCount"
};
processDataList.Add(threadsTotalCount);
processDataList.ForEach(data =>
{
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
});
processDataListAll.AddRange(processDataList);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(processDataListAll);
}
}
public class PhrasePatternJob(
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);
var serverList = (List<ServerModel>)dataMap["executor"];
var processDataListAll = new List<ServerMonitoringData>();
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);
var phrasePatternCount = new ServerMonitoringData
{
ServerId = server.Id,
Data = count.ToString(),
DataName = "句柄",
DataType = "PhrasePatternCount"
};
processDataListAll.Add(phrasePatternCount);
await hubContext.Clients.All.SendAsync("ReceiveData", server.Id, phrasePatternCount.DataType,
phrasePatternCount.Data);
}
_count++;
if (_count <= 10) return;
_count = 0;
await dataHelper.SaveData(processDataListAll);
}
}

View File

@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>LoongPanel_Asp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ini-parser-new" Version="2.6.2"/>
<PackageReference Include="LiteDB" Version="5.0.19"/>
<PackageReference Include="MailKit" Version="4.6.0"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
<PackageReference Include="MimeKit" Version="4.6.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="Quartz" Version="3.9.0"/>
<PackageReference Include="Quartz.AspNetCore" Version="3.9.0"/>
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.9.0"/>
<PackageReference Include="SSH.NET" Version="2024.0.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.0"/>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<None Remove="Migrations\**"/>
<None Update="Configs\jobs.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Configs\servers.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,6 @@
@LoongPanel_Asp_HostAddress = http://localhost:5253
GET {{LoongPanel_Asp_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,122 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
namespace LoongPanel_Asp.Middlewares;
public class ApiPermissionMiddleware(
RequestDelegate next,
ILogger<ApiPermissionMiddleware> logger,
IServiceProvider serviceProvider,
IConfiguration configuration)
{
public async Task InvokeAsync(HttpContext context)
{
try
{
// 获取配置中定义的公开API列表
var publicApis = configuration["PublicApi"]?.Split(";", StringSplitOptions.RemoveEmptyEntries) ??
new string[0];
// 如果请求路径在公开API列表中则直接调用下一个中间件
if (publicApis.Any(api => api == context.Request.Path.Value))
{
await next(context);
return;
}
// 验证Token
var payload = context.User;
string[] hubKeywords = { "ServerHub", "MessageHub", "SessionHub", "TermHub" };
//如果请求的地址是 (*Hub/*)
if (hubKeywords.Any(keyword => context.Request.Path.Value!.Contains(keyword)))
{
await next(context);
return;
}
// 获取用户信息
using var scope = serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userId = payload.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
if (userId == null)
{
await UnauthorizedResponse(context);
return;
}
var user = await userManager.FindByIdAsync(userId);
if (user == null)
{
await UnauthorizedResponse(context);
return;
}
// 获取用户角色
var userRoles = await userManager.GetRolesAsync(user);
var roleId = userRoles.FirstOrDefault();
if (roleId == null)
{
await UnauthorizedResponse(context);
return;
}
// 获取角色信息
var role = await roleManager.FindByIdAsync(roleId.ToLower());
if (role == null)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Role not found");
return;
}
// 获取角色的API权限
var apiPermissions = dbContext.ApiPermissions.ToList()
.Select(x => $"/Api/{x.Controller.Replace("Controller", "")}/{x.Name}");
// 判断请求是否拥有权限
if (!apiPermissions.Any(x =>
context.Request.Path.Value != null && context.Request.Path.Value.StartsWith(x)))
{
await ForbiddenResponse(context);
}
else
{
// 请求拥有权限,调用下一个中间件
await next(context);
}
}
catch (Exception ex)
{
// 记录异常信息
logger.LogError(ex, "An error occurred in ApiPermissionMiddleware");
await InternalServerErrorResponse(context);
}
}
private static async Task UnauthorizedResponse(HttpContext context)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
}
private static async Task ForbiddenResponse(HttpContext context)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
}
private static async Task InternalServerErrorResponse(HttpContext context)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("An internal server error occurred.");
}
private static string ExtractToken(IEnumerable<string> authHeader)
{
return authHeader.FirstOrDefault()?.Split(" ").Last() ?? string.Empty;
}
}

View File

@ -0,0 +1,71 @@
using LoongPanel_Asp.Helpers;
namespace LoongPanel_Asp.Middlewares;
public class PermissionMiddleware(
RequestDelegate next,
TokenHelper tokenHelper,
IConfiguration configuration,
ILogger<PermissionMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var publicApis = configuration["PublicApi"]?.Split(";") ?? [];
// 如果请求路径在公开API列表中则直接调用下一个中间件
if (publicApis.Any(api => api == context.Request.Path.Value))
{
await next(context);
return;
}
Console.WriteLine(context.Request.Path.Value!);
// 获取请求头中的Authorization信息
var authorizationHeader = context.Request.Headers["Authorization"];
// 提取Token
var token = authorizationHeader.ToString().Replace("Bearer ", "");
//如果Token不存在则尝试从Cookie中获取
if (string.IsNullOrEmpty(token))
{
//Cookie token
var cookieToken = context.Request.Cookies["token"];
if (string.IsNullOrEmpty(cookieToken))
{
//如果Cookie中也没有Token则返回401 Unauthorized
logger.LogWarning("Token not found in Authorization header or cookie.");
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
token = cookieToken;
}
try
{
// 验证Token并获取声明
var claims = tokenHelper.ValidateToken(token);
if (claims != null)
{
context.User = claims;
// Token验证成功继续处理请求
logger.LogInformation("Token validated successfully.");
await next(context);
}
else
{
// Token验证失败返回401 Unauthorized
logger.LogError("Token validation failed.");
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}
catch (Exception ex)
{
// Token解析过程中发生异常返回500 Internal Server Error
logger.LogError(ex, "An error occurred while validating the token.");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

View File

@ -0,0 +1,27 @@
namespace LoongPanel_Asp.Models;
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 Phone { get; set; }
public required string Password { get; set; }
}
public class LoginModel
{
public required string EmailOrUserName { get; set; }
public required string Password { get; set; }
public bool RememberMe { get; set; } = false;
}

View File

@ -0,0 +1,20 @@
namespace LoongPanel_Asp.Models;
public class Property
{
public required string Name { get; set; }
public required string DisplayName { get; set; }
public int? Order { get; set; }
}
public class ServerModel
{
public required string Id { get;init; }
public required string Address { get; set; }
public required int Port { get; set; }
public required string ServerName { get; set; }
public required string Password { get; set; }
public required string Username { get; set; }
public required bool Http { get; set; }
}

View File

@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
namespace LoongPanel_Asp.Models;
public struct PrometheusQueryResult
{
[JsonPropertyName("status")] public required string Status { get; init; }
[JsonPropertyName("data")] public required PrometheusQueryData Data { get; init; }
}
public struct PrometheusQueryData
{
[JsonPropertyName("resultType")] public required string ResultType { get; init; }
[JsonPropertyName("result")] public required List<PrometheusQueryResultValue> Result { get; init; }
}
public struct PrometheusQueryResultValue
{
[JsonPropertyName("metric")] public required Dictionary<string, string> Metric { get; init; }
[JsonPropertyName("value")] public required List<object> Value { get; init; }
}

134
LoongPanel-Asp/Program.cs Executable file
View File

@ -0,0 +1,134 @@
using LiteDB;
using LoongPanel_Asp;
using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Hubs;
using LoongPanel_Asp.Middlewares;
using LoongPanel_Asp.Servers;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Quartz;
using Quartz.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
const string myAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 8;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = false;
});
builder.Services.AddSignalR();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var sqlConnectionString = builder.Configuration.GetConnectionString("SqliteConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));
// builder.Services.AddDbContext<ApplicationDbContext>(options =>
// options.UseSqlite(sqlConnectionString));
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddSingleton<TokenHelper>();
var emailSettings = builder.Configuration.GetSection("EmailSettings");
builder.Services.AddSingleton<EmailHelper>(sp =>
new EmailHelper(
sp.GetService<ILiteDatabase>(),
emailSettings["Host"] ?? throw new InvalidOperationException(),
int.Parse(emailSettings["Port"] ?? throw new InvalidOperationException()),
emailSettings["Username"] ?? throw new InvalidOperationException(),
emailSettings["Password"] ?? throw new InvalidOperationException()
));
builder.Services.AddSingleton<ILiteDatabase, LiteDatabase>(
sp => new LiteDatabase("Filename=temp.db;Connection=shared;"));
// 跨域
builder.Services.AddCors(options =>
{
options.AddPolicy(myAllowSpecificOrigins,
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();
});
});
builder.Services.AddQuartz(q =>
{
var jobList = JobConfigHelper.ReadJobConfigurations().ToList();
foreach (var jobConfig in jobList)
{
var jobType = Type.GetType(jobConfig.JobType);
if (jobType == null) continue;
var jobDataMap = new JobDataMap { { "executor", jobConfig.Executor } };
var jobKey = new JobKey(jobConfig.Name, jobConfig.Group);
var triggerKey = new TriggerKey($"Trigger_for_{jobConfig.Name}", jobConfig.Group);
q.AddJob(jobType, jobKey, opts => opts.SetJobData(jobDataMap));
q.AddTrigger(
opts => opts.ForJob(jobKey).WithIdentity(triggerKey).WithCronSchedule(jobConfig.CronExpression));
}
});
builder.Services.AddQuartzServer(options =>
{
// when shutting down we want jobs to complete gracefully
options.WaitForJobsToComplete = true;
});
builder.Services.AddScoped<SshService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors(myAllowSpecificOrigins);
app.UseMiddleware<PermissionMiddleware>();
app.UseMiddleware<ApiPermissionMiddleware>();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapHub<SessionHub>("/SessionHub");
app.Run();

View File

@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://192.168.0.13:58826",
"sslPort": 44304
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://192.168.0.13:5253",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://192.168.0.13:7233;http://192.168.0.13:5253",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,96 @@
using LoongPanel_Asp.Helpers;
using LoongPanel_Asp.Models;
using Renci.SshNet;
using ConnectionInfo = Renci.SshNet.ConnectionInfo;
namespace LoongPanel_Asp.Servers;
public class SshService : IDisposable
{
private readonly CancellationTokenSource _cts = new();
private readonly ApplicationDbContext _db;
private readonly ILogger<SshService> _logger;
private readonly Dictionary<string, ConnectionInfo> _serverConnectionInfos = new();
private readonly Dictionary<string, SshClient> _serverSshClients = new();
public SshService(ApplicationDbContext db, ILogger<SshService> logger)
{
_db = db;
_logger = logger;
LoadServerConfigurations();
}
public void Dispose()
{
_cts.Cancel();
foreach (var sshClient in _serverSshClients.Values) sshClient.Dispose();
_serverSshClients.Clear();
}
private void LoadServerConfigurations()
{
// 从数据库中加载服务器配置信息
var serverConfigurations=JobConfigHelper.GetServers();
foreach (var serverConfiguration in serverConfigurations)
{
var connectionInfo = new ConnectionInfo(serverConfiguration.Address, serverConfiguration.Port,
serverConfiguration.Username, new PasswordAuthenticationMethod(serverConfiguration.Username, serverConfiguration.Password));
_serverConnectionInfos[serverConfiguration.Id] = connectionInfo;
var sshClient = new SshClient(connectionInfo);
// 设置超时时间
sshClient.ConnectionInfo.Timeout = TimeSpan.FromSeconds(10);
_serverSshClients[serverConfiguration.Id] = sshClient;
}
}
public async Task<string> ExecuteCommandAsync(string serverId, string command, params string[] arguments)
{
var sshClient = _serverSshClients[serverId];
if (sshClient == null) throw new InvalidOperationException($"SSH client for server ID '{serverId}' not found.");
var output = "";
try
{
// 确保在执行命令前连接到服务器
if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token);
var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments));
using var commandResult = sshClient.RunCommand(commandString);
output = commandResult.Result;
if (commandResult.ExitStatus != 0) output = commandResult.Error;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return output;
}
public async Task<string> ExecuteCommandAsync(bool langC, string serverId, string command,
params string[] arguments)
{
var sshClient = _serverSshClients[serverId];
if (sshClient == null) throw new InvalidOperationException($"SSH client for server ID '{serverId}' not found.");
var output = "";
try
{
// 确保在执行命令前连接到服务器
if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token);
var result = langC ? "LANG=C" : "";
var commandString = string.Join(" ", result, command, string.Join(" ", arguments));
using var commandResult = sshClient.RunCommand(commandString);
output = commandResult.Result;
if (commandResult.ExitStatus != 0) output = commandResult.Error;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return output;
}
}

View File

@ -0,0 +1,12 @@
using LiteDB;
namespace LoongPanel_Asp.Types;
public class EmailCode
{
public ObjectId? Id { get; set; }
public required string Email { get; set; }
public required string Code { get; set; }
public required DateTime ExpireTime { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

23
LoongPanel-Asp/appsettings.json Executable file
View File

@ -0,0 +1,23 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.0.26;Port=54321;Username=SYSTEM;Password=loongnix;Database=app",
"SqliteConnection": "Data Source=app.db;"
},
"EmailSettings": {
"Host": "smtp.qq.com",
"Port": "465",
"Username": "70975268@qq.com",
"Password": "udyligofifdibhdc"
},
"Secret": "p4Qzf/+GPP/XNLalZGCzwlelOl6skiFZscj6iZ6rZZE=",
"Issuer": "LoongPanel",
"Audience": "LoongPanel",
"PubLicApi": "/Api/Account/SendVerificationCode;/Api/Account/Register;/Api/Account/Login;/Api/Account/VerifyEmailName;"
}

View File

@ -0,0 +1,42 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
namespace LoongPanel_Asp.utils;
public static class ControllerScanner
{
public static List<ApiPermission> GetApiPermissions()
{
var apiPermissions = new List<ApiPermission>();
var allTypes = Assembly.GetExecutingAssembly().GetTypes();
var controllerTypes = allTypes
.Where(t => t.Namespace != null && t.Namespace.StartsWith("LoongPanel_Asp.Controllers") &&
t.BaseType == typeof(ControllerBase))
.ToList();
var index = 0;
foreach (var controller in controllerTypes)
{
//获取控制器所有路由
var routes = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance);
;
foreach (var route in routes)
{
//判断返回类型是否是System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult]
if (route.ReturnType != typeof(Task<IActionResult>) &&
route.ReturnType != typeof(IActionResult)) continue;
index++;
var newPermission = new ApiPermission
{
Id = index,
Name = route.Name,
Controller = controller.Name
};
apiPermissions.Add(newPermission);
}
}
return apiPermissions;
}
}

1
web/.env Executable file
View File

@ -0,0 +1 @@
API_SERVER="http://192.168.0.13:5253"

24
web/.gitignore vendored Executable file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
#.env
#.env.*
#!.env.example

75
web/README.md Executable file
View File

@ -0,0 +1,75 @@
# Nuxt 3 Minimal Starter
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

18
web/app.vue Executable file
View File

@ -0,0 +1,18 @@
<script lang="ts" setup>
</script>
<template>
<div>
<NuxtPwaManifest />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style lang="scss">
@import "base";
:root {
--primary-color: $primary-color
}
</style>

BIN
web/assets/diagonal1.cur Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
web/assets/link.cur Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

23
web/assets/min.scss Executable file
View File

@ -0,0 +1,23 @@
@import "base";
* {
margin: 0;
box-sizing: border-box;
transition: color .2s, background-color .2s, border-color .2s, box-shadow .2s, stork .2s, fill .2s, opacity .2s;
cursor: url("assets/normal.cur"), auto;
}
body {
background: $light-bg-underline-color;
color: $light-text-color;
.dark-mode & {
background: $dark-bg-underline-color;
color: $dark-text-color;
}
}
// 去除a标签的默认下划线
a {
text-decoration: none;
}

BIN
web/assets/normal.cur Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
web/assets/normalshowel.cur Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

35
web/base.scss Executable file
View File

@ -0,0 +1,35 @@
$light-bg-color: #FFFFFF;
$light-bg-underline-color: #e6e9ed;
$dark-bg-underline-color: #141723;
$dark-bg-color: #222738;
//$primary-color: #64a77c;
$primary-color: #002EA6;
$secondary-color: #ce5230;
$tertiary-color: #c4c744;
$bg-color-1: #3B8AFF;
$bg-color-2: #0051B5;
$unfocused-color: #8A92A6;
$light-text-color: #04040B;
$light-unfocused-color: $unfocused-color;
$dark-text-color: #D3D3D3;
$dark-unfocused-color: $unfocused-color;
$gap: 8px;
$padding: 16px;
$radius: 8px;
.dark-mode html {
$dark-bg-color: red;
}
@mixin SC_Font {
font-family: "Noto Sans SC", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
}

View File

@ -0,0 +1,61 @@
<script lang="ts" setup>
import {cards} from "~/config/cards";
import {type GridCardItem, useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {v4 as uuidv4} from 'uuid';
import ICard from "~/components/Cards/ICard.vue";
const props = defineProps({
closeCallback: {
type: Function,
required: true
},
})
const mainLayoutStore = useMainLayoutStore()
const addCard = (selectCardId: string) => {
const newCard: GridCardItem = {
type: "card",
h: 5, w: 5, x: 1, y: 1,
i: uuidv4(),
selectCard: selectCardId
}
mainLayoutStore.addLayout(newCard)
props.closeCallback()
}
</script>
<template>
<div class="add-card-box">
<div v-for="card in cards" class="add-card-item" @click="addCard(card.id)">
<ICard id="" :card-id="card.id"/>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.add-card-box {
padding: $padding*2;
min-width: 200px;
min-height: 100px;
display: grid;
background: $light-bg-color;
border-radius: $radius;
grid-template-columns: 1fr 1fr;
gap: $gap*2;
grid-template-rows: repeat(auto-fill, 1fr);
.dark-mode & {
background: $dark-bg-underline-color;
}
}
.add-card-item {
border-radius: $radius;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);;
min-width: 400px;
min-height: 200px;
}
</style>

View File

@ -0,0 +1,133 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {type cardItem, cards} from "~/config/cards";
const mainLayoutStore = useMainLayoutStore()
const props = defineProps({
id: {
type: String,
required: true
},
cardId: {
type: String,
required: true
}
})
const items = [{
label: '刷新',
command: () => {
}
}, {
label: '弹出',
command: () => {
}
}, {
label: '删除',
command: () => {
mainLayoutStore.deleteLayout(props.id)
}
}
];
let card: cardItem | undefined = undefined
onBeforeMount(() => {
card = cards.find(x => x.id === props.cardId)
})
</script>
<template>
<div :id="`card_${id}`" class="card-layout">
<SplitButton :model="items" class="SplitButton"/>
<div :id="'item' + id" class="card-title vue-draggable-handle">
<div></div>
<h3 v-if="card && card.name">{{ card?.name ?? "默认标题" }}</h3>
</div>
<div class="card-content">
<component :is="card?.component" v-if="card && card.component"/>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.card-layout {
width: 100%;
height: 100%;
background: $light-bg-color;
border-radius: $radius*2;
padding-bottom: $padding;
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
position: relative;
display: flex;
flex-direction: column;
gap: $gap*1.5;
will-change: scroll-position, contents;
.dark-mode & {
background: $dark-bg-color;
}
}
.SplitButton {
position: absolute;
right: $padding*.5;
top: $padding*.5;
:deep(.p-button) {
background: unset;
border: unset;
padding: unset;
> svg {
stroke: $light-unfocused-color;
.dark-mode & {
stroke: $dark-unfocused-color;
}
&:hover {
stroke: $light-text-color;
.dark-mode & {
stroke: $dark-text-color;
}
}
}
}
}
.card-content {
padding: 0 $padding*1.5;
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0
}
}
.card-title {
display: flex;
align-items: center;
gap: $gap;
color: $light-text-color;
padding: $padding*1.5;
padding-bottom: 0;
&,
h3:hover {
cursor: move;
}
.dark-mode & {
color: $dark-text-color;
}
div {
width: 6px;
height: 20px;
border-radius: $radius;
background: $primary-color;
}
}
</style>

178
web/components/Cards/IChart.vue Executable file
View File

@ -0,0 +1,178 @@
<script lang="ts" setup>
import {charts} from "~/config/charts";
import type {PropType} from "vue";
import type {serverValueItem} from "~/components/SettingCard.vue";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
const mainLayoutStore = useMainLayoutStore()
const visible = ref(false)
const props = defineProps({
id: {
type: String,
required: true
},
title: {
type: String,
required: true
},
chart: {
type: String,
required: true
},
values: {
type: Array as PropType<serverValueItem[]>,
required: true
},
isDraggable: {
type: Boolean,
required: false
}
})
const cardRef = ref(null)
const {width} = useElementBounding(cardRef)
const rangeNum = ref(6)
const targetIsVisible = useElementVisibility(cardRef)
const isSwitched = ref(false)
watch(() => mainLayoutStore.SelectServer.id, () => {
isSwitched.value = true
setTimeout(() => {
isSwitched.value = false
})
})
watchDebounced(
width,
() => {
//
rangeNum.value = Math.floor(width.value / 40)
//12
rangeNum.value = rangeNum.value > 24 ? 24 : rangeNum.value
},
{debounce: 500, maxWait: 1000},
)
const valueIds = computed(() => props.values.map(x => x.dataType))
const valueNames = computed(() => props.values.map(x => x.dataName))
const items = [
{
label: '全屏查看',
command: () => {
}
}, {
label: '刷新',
command: () => {
}
}, {
label: '弹出',
command: () => {
}
}, {
label: '删除',
command: () => {
mainLayoutStore.deleteLayout(props.id)
}
}, {
label: '设置',
command: () => {
visible.value = true
}
}
];
</script>
<template>
<div :id="`card_${id}`" ref="cardRef" class="card-layout ">
<Dialog v-model:visible="visible" :pt="{
root: 'border-none',
mask: {
style: 'backdrop-filter: blur(10px)'
}
}" modal>
<template #container="{ closeCallback }">
<SettingCard :card-id="id" :close-callback="closeCallback" is-setting/>
</template>
</Dialog>
<SplitButton :model="items" class="SplitButton"/>
<div :id="'item' + id" class="card-title vue-draggable-handle">
<div></div>
<h3>{{ title ?? "默认标题" }}</h3>
</div>
<component :is="charts.find(x => x.id === chart)?.component" v-if="targetIsVisible&&!isSwitched"
:rangeNum="rangeNum"
:valueIds="valueIds"
:valueNames="valueNames"/>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.card-layout {
width: 100%;
height: 100%;
background: $light-bg-color;
border-radius: $radius*2;
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
padding: $padding*1.5;
position: relative;
display: flex;
flex-direction: column;
gap: $gap*1.5;
will-change: scroll-position, contents;
.dark-mode & {
background: $dark-bg-color;
}
}
.SplitButton {
position: absolute;
right: $padding*.5;
top: $padding*.5;
:deep(.p-button) {
background: unset;
border: unset;
padding: unset;
> svg {
stroke: $light-unfocused-color;
.dark-mode & {
stroke: $dark-unfocused-color;
}
&:hover {
stroke: $light-text-color;
.dark-mode & {
stroke: $dark-text-color;
}
}
}
}
}
.card-title {
display: flex;
align-items: center;
gap: $gap;
color: $light-text-color;
&,
h3:hover {
cursor: move;
}
.dark-mode & {
color: $dark-text-color;
}
div {
width: 6px;
height: 20px;
border-radius: $radius;
background: $primary-color;
}
}
</style>

171
web/components/Cards/MiniCard.vue Executable file
View File

@ -0,0 +1,171 @@
<script lang="ts" setup>
import {useDataStore} from "~/strores/DataStore";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
const {$gsap} = useNuxtApp()
const props = defineProps({
title: {
type: String,
default: "CPU使用率"
},
unit: {
type: String,
default: "%"
},
watcher: {
type: String,
default: "cpuTotalUsage"
},
reverse: {
type: Boolean,
default: false
}
})
const value = ref("0");
const trueValue = ref("0");
const dataStore = useDataStore()
const mainStore = useMainLayoutStore()
//dataStore
dataStore.$subscribe((_, state) => {
//d3YT#cpuUserUsagekeyvalue
const data = state.data[props.watcher]
//data
if (!data) {
value.value = "0"
updateProgress(0)
if (props.reverse) {
value.value = "100"
updateProgress(100)
}
return
}
//data.value
const newValue = (Math.floor(Number(data) * 100) / 100).toFixed(2)
if (newValue !== value.value) {
value.value = newValue
trueValue.value = data.toString()
//
updateProgress(parseFloat(value.value))
}
})
watch(() => mainStore.SelectServer, () => {
//
value.value = "0"
updateProgress(0)
})
const circumference = 2 * Math.PI * 36;
const updateProgress = (value: number) => {
let progress = (100 - value) / 100;
if (props.reverse) {
progress = 1 - progress
}
$gsap.to("#" + props.title, {
duration: 0.5,
ease: 'power1.out',
strokeDashoffset: circumference * progress,
})
$gsap.to("#" + props.title + "Arrow", {
duration: 0.4,
ease: 'bounce.inOut',
rotation: 360 * (value / 100) - 30,
transformOrigin: "center center"
})
let color;
if (value >= 0 && value < 20) {
color = "#00C853"; // 绿
} else if (value >= 20 && value < 40) {
color = "#3A57E8"; //
} else if (value >= 40 && value < 60) {
color = "#FBBD08"; //
} else if (value >= 60 && value < 80) {
color = "#FF5722"; //
} else if (value >= 80 && value <= 100) {
color = "#FF0000"; // 80-100
}
if (color) {
$gsap.to("#" + props.title, {
duration: 0.5,
ease: 'power1.out',
stroke: color,
});
}
}
</script>
<template>
<div class="mini-card-box">
<svg fill="none" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<circle cx="36" cy="36" r="32" stroke="#E9ECEF" stroke-width="2"/>
<circle :id="title" :stroke-dasharray="circumference" cx="36"
cy="36" r="32" stroke="red"
stroke-linecap="round"
stroke-width="4" transform="rotate(-90, 36, 36)"/>
<path
:id="title+'Arrow'"
d="M26.1904 44.784C25.8209 45.1944 25.854 45.8267 26.2645 46.1963C26.6749 46.5658 27.3072 46.5327 27.6767 46.1223L26.1904 44.784ZM43.7763 27.8042C43.7474 27.2527 43.2768 26.829 42.7253 26.8579L33.7376 27.3289C33.1861 27.3578 32.7624 27.8284 32.7913 28.3799C32.8202 28.9314 33.2908 29.3551 33.8423 29.3262L41.8313 28.9075L42.25 36.8965C42.2789 37.4481 42.7495 37.8717 43.301 37.8428C43.8525 37.8139 44.2762 37.3434 44.2473 36.7919L43.7763 27.8042ZM27.6767 46.1223L43.5208 28.5257L42.0345 27.1874L26.1904 44.784L27.6767 46.1223Z"
fill="#ADB5BD"/>
</svg>
<div class="text">
<h3>
{{ title }}
</h3>
<h2>
<span v-if="props.reverse" v-tooltip="`${100-trueValue}${unit}`">{{ 100 - value }}{{ unit }}</span>
<span v-else v-tooltip="`${trueValue}${unit}`">{{ value }}{{ unit }}</span>
</h2>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "../../base";
.mini-card-box {
display: flex;
align-items: center;
background: $light-bg-color;
padding: $padding*1.5;
border-radius: $radius*2;
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
gap: $gap*3;
> svg {
width: 68px;
height: 68px;
}
.dark-mode & {
background: $dark-bg-color;
}
}
.text {
display: flex;
flex-direction: column;
gap: $gap;
h2, h3 {
color: $light-unfocused-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%; /* 28px */
//
text-wrap: nowrap;
}
h2 {
font-size: 19px;
font-weight: 500;
color: $light-text-color;
.dark-mode & {
color: $dark-text-color;
}
}
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {useSessionSignalRStore} from "~/strores/HubStore";
import {POSITION, useToast} from 'vue-toastification'
const toast = useToast()
const mainLayoutStore = useMainLayoutStore()
const signalRStore = useSessionSignalRStore()
const currentUserId = ref("")
const sendMessageModel = ref(false)
const message = ref("")
const items = ref([
{
label: '发送消息', command: () => {
sendMessageModel.value = true
}
},
{label: '通知',}
]);
const sendMessage = () => {
signalRStore.sendMessage("SendMessage", mainLayoutStore.UserInfo.id, currentUserId.value, message.value)
sendMessageModel.value = false
}
const onRightClick = (event: MouseEvent, userId: string) => {
menu.value.show(event);
currentUserId.value = userId;
};
const menu = ref();
</script>
<template>
<div class="online-user-layout">
<Dialog v-model:visible="sendMessageModel" header="发送消息" modal>
<Textarea v-model="message" cols="30" rows="5"/>
<template #footer>
<Button autofocus label="取消" severity="secondary" text @click="sendMessageModel = false"/>
<Button autofocus label="发送" outlined severity="secondary" @click="sendMessage"/>
</template>
</Dialog>
<ContextMenu ref="menu" :model="items"/>
<div v-for="user in mainLayoutStore.OnlineUsers" class="user-item" @click="onRightClick($event,user.id)">
<Avatar :image="user.avatar" shape="circle" size="xlarge"/>
<div class="user-info">
<h4>{{ user.nickName }}</h4>
<div>
<Tag :value="user.posts"/>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import 'base';
.online-user-layout {
display: flex;
flex-direction: column;
gap: $gap*1.5;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: $gap*3;
padding: $padding*.2;
border-radius: $radius;
&:hover {
background: rgba(212, 212, 212, 0.45);
}
> .user-info {
display: flex;
flex-direction: column;
justify-content: center;
gap: $gap;
flex: 1;
}
}
</style>

View File

@ -0,0 +1,89 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
defineProps({
id: {
type: String,
required: true
}
})
const mainLayoutStore = useMainLayoutStore()
watch(() => mainLayoutStore.SelectServer, () => {
getServerConfig()
})
onMounted(() => {
getServerConfig()
})
const getServerConfig = () => {
$fetch(`/Api/Server/GetServerInfo`, {
method: 'GET',
params: {
serverId: mainLayoutStore.SelectServer.id
},
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${useCookie('token').value}`
},
baseURL: useRuntimeConfig().public.baseUrl
}).then((res) => {
const data = res as any;
const result: { key: string, value: string }[] = [];
for (const key in data) {
if (data.hasOwnProperty(key)) {
result.push({key, value: data[key]});
}
}
mainLayoutStore.ServerConfig = result;
})
}
const I18N: { [key: string]: string } = {
'os': '系统',
'host': '主机',
'kernel': '内核',
'uptime': '运行时间',
'packages': '软件包',
'shell': '终端',
'cpu': '处理器',
'gpu': '显卡',
'memory': '内存',
}
</script>
<template>
<div class="system-card-container">
<div v-for="m in mainLayoutStore.ServerConfig" :key="m.key" class="info-item">
<p>{{ I18N[m.key.toLowerCase()] ?? m.key }}</p>
<p>{{ m.value.split('/').pop()?.trim() }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.system-card-container {
display: flex;
flex-direction: column;
gap: $gap;
color: $light-text-color;
justify-content: space-between;
height: 100%;
.dark-mode & {
color: $dark-text-color;
}
}
.info-item {
display: flex;
gap: $gap*2;
p:first-of-type {
font-weight: 600;
min-width: 100px;
}
}
</style>

View File

@ -0,0 +1,214 @@
<script lang="ts" setup>
import {useDataStore} from "~/strores/DataStore";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import type {PropType} from "vue";
import {ref} from 'vue';
import dayjs from "dayjs";
import VChart from 'vue-echarts';
import _ from "lodash";
const dataStore = useDataStore()
const mainLayoutStore = useMainLayoutStore()
type ArbitraryKeyValuePairs = {
[key: string]: (number | string)[];
};
const values = ref<ArbitraryKeyValuePairs>({});
const chartRet = ref<any>(null)
const isLoading = ref<boolean>(true)
const props = defineProps({
valueIds: {
type: Array as PropType<string[]>,
required: true,
},
valueNames: {
type: Array as PropType<string[]>,
required: true,
},
rangeNum: {
type: Number,
default: 6,
},
})
const option = computed(() => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
left: '2%',
right: '50',
bottom: '10%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: []
}
],
yAxis: [
{
type: 'value',
}
],
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
saveAsImage: {}
}
},
dataZoom: [
{
type: 'inside',
start: 90,
end: 100,
},
{
type: 'slider',
throttle: 500,
},
{
type: 'slider',
yAxisIndex: 0,
startValue: 0,
endValue: 100,
},
],
legend: {
data: props.valueNames,
textStyle: {
fontSize: 16,
},
},
series: props.valueNames?.map((id) => {
return {
name: id,
type: 'line',
areaStyle: {},
large: true,
smooth: true,
emphasis: {
focus: 'series'
},
data: [],
}
}) ?? []
}
});
let interval: NodeJS.Timeout;
onUnmounted(() => {
clearInterval(interval);
})
onMounted(() => {
let history = dataStore.dataHistory
setTimeout(() => {
isLoading.value = false;
}, 5000)
nextTick(() => {
props.valueIds?.forEach((id, index) => {
chartRet.value.appendData({
seriesIndex: index,
data: history.data[id]
})
})
const currentOption = chartRet.value.getOption();
currentOption.xAxis[0].data = history.times
chartRet.value.setOption(currentOption)
isLoading.value = false
interval = setInterval(() => {
const data = dataStore.data
props.valueIds?.forEach((id, index) => {
const newData = data[id] ?? 0
if (!values.value[id]) {
values.value[id] = []
}
chartRet.value.appendData({
seriesIndex: index,
data: [newData]
})
})
const currentOption = chartRet.value.getOption();
currentOption.xAxis[0].data.push(dayjs().format('MM-DD HH:mm:ss'))
chartRet.value.setOption(currentOption)
}, 4000 + 1000 * Math.random())
})
})
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) => {
console.log(e)
let start = e.start ?? e.batch[0].start
if (done) return
if (start <= 50 && start !== startTemp) {
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) {
isLoading.value = false
done = true
return
}
endIndex = data.endIndex
//data
const currentOption = chartRet.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
chartRet.value.setOption(currentOption)
setTimeout(() => {
isLoading.value = false
}, 1000)
})
}
}, 1000)
</script>
<template>
<v-chart ref="chartRet" :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,66 @@
<script lang="ts" setup>
import {useDataStore} from "~/strores/DataStore";
import type {PropType} from "vue";
import VChart, {THEME_KEY} from 'vue-echarts';
const dataStore = useDataStore()
const props = defineProps({
valueIds: {
type: Array as PropType<string[]>,
required: true,
},
valueNames: {
type: Array as PropType<string[]>,
required: true,
}
})
const option = computed(() => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '0%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: true,
axisTick: {
alignWithLabel: true
},
data: props.valueNames
}
],
yAxis: [
{
type: 'value'
}
],
series: [{
name: '数据',
type: 'bar',
barWidth: '60%',
data: props.valueIds?.map((id) => {
return dataStore.data[id]
}),
}]
}
});
</script>
<template>
<v-chart :option="option" autoresize class="chart"/>
</template>

View File

@ -0,0 +1,212 @@
<script lang="ts" setup>
import {useDataStore} from "~/strores/DataStore";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import type {PropType} from "vue";
import {ref} from 'vue';
import dayjs from "dayjs";
import VChart from 'vue-echarts';
import _ from "lodash";
const dataStore = useDataStore()
const mainLayoutStore = useMainLayoutStore()
type ArbitraryKeyValuePairs = {
[key: string]: (number | string)[];
};
const values = ref<ArbitraryKeyValuePairs>({});
const chartRet = ref<any>(null)
const isLoading = ref<boolean>(true)
const props = defineProps({
valueIds: {
type: Array as PropType<string[]>,
required: true,
},
valueNames: {
type: Array as PropType<string[]>,
required: true,
},
rangeNum: {
type: Number,
default: 6,
},
})
const option = computed(() => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
grid: {
left: '2%',
right: '50',
bottom: '10%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: []
}
],
yAxis: [
{
type: 'value',
}
],
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
saveAsImage: {}
}
},
dataZoom: [
{
type: 'inside',
start: 90,
end: 100,
},
{
type: 'slider',
throttle: 500,
},
{
type: 'slider',
yAxisIndex: 0,
startValue: 0,
endValue: 100,
},
],
legend: {
data: props.valueNames,
textStyle: {
fontSize: 16,
},
},
series: props.valueNames?.map((id) => {
return {
name: id,
type: 'line',
large: true,
smooth: true,
emphasis: {
focus: 'series'
},
data: [],
}
}) ?? []
}
});
let interval: NodeJS.Timeout;
onUnmounted(() => {
clearInterval(interval);
})
onMounted(() => {
let history = dataStore.dataHistory
setTimeout(() => {
isLoading.value = false;
}, 5000)
nextTick(() => {
props.valueIds?.forEach((id, index) => {
chartRet.value.appendData({
seriesIndex: index,
data: history.data[id]
})
})
const currentOption = chartRet.value.getOption();
currentOption.xAxis[0].data = history.times
chartRet.value.setOption(currentOption)
isLoading.value = false
interval = setInterval(() => {
const data = dataStore.data
props.valueIds?.forEach((id, index) => {
const newData = data[id] ?? 0
if (!values.value[id]) {
values.value[id] = []
}
chartRet.value.appendData({
seriesIndex: index,
data: [newData]
})
})
const currentOption = chartRet.value.getOption();
currentOption.xAxis[0].data.push(dayjs().format('MM-DD HH:mm:ss'))
chartRet.value.setOption(currentOption)
}, 4000 + 1000 * Math.random())
})
})
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) => {
console.log(e)
let start = e.start ?? e.batch[0].start
if (done) return
if (start <= 50 && start !== startTemp) {
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) => {
const data = res as Data;
if (data.done) {
isLoading.value = false
done = true
return
}
endIndex = data.endIndex
//data
const currentOption = chartRet.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
chartRet.value.setOption(currentOption)
setTimeout(() => {
isLoading.value = false
}, 1000)
})
}
}, 1000)
</script>
<template>
<v-chart ref="chartRet" :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,50 @@
<script lang="ts" setup>
import {useDataStore} from "~/strores/DataStore";
import type {PropType} from "vue";
import VChart, {THEME_KEY} from 'vue-echarts';
const dataStore = useDataStore()
const props = defineProps({
valueIds: {
type: Array as PropType<string[]>,
required: true,
},
valueNames: {
type: Array as PropType<string[]>,
required: true,
},
})
const option = computed(() => {
return {
tooltip: {
trigger: 'item'
},
grid: {
left: '3%',
right: '4%',
bottom: '0%',
containLabel: true
},
legend: {
data: props.valueNames
},
series: [{
name: '数值',
type: 'pie',
data: props.valueIds?.map((id, index) => {
return {
value: dataStore.data[id],
name: props.valueNames[index]
}
}),
}]
}
});
</script>
<template>
<v-chart :option="option" autoresize class="chart"/>
</template>

View File

@ -0,0 +1,11 @@
<script lang="ts" setup>
</script>
<template>
</template>
<style lang="scss" scoped>
</style>

81
web/components/ColorSwitch.vue Executable file
View File

@ -0,0 +1,81 @@
<script lang="ts" setup>
</script>
<template>
<label class="switch">
<input :checked="$colorMode.value == 'dark'" type="checkbox"
@click="$colorMode.preference = $colorMode.value == 'dark' ? 'light' : 'dark'">
<span class="slider"></span>
<img alt="" class="off"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAQABADASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIG/8QAIxAAAgIABQQDAAAAAAAAAAAAAQMCBAAREiExBUFRcROBsf/EABQBAQAAAAAAAAAAAAAAAAAAAAX/xAAWEQADAAAAAAAAAAAAAAAAAAAAEiL/2gAMAwEAAhEDEQA/AMBTp03dNglMVuttjqnKQ2UPOfntkOThbqVVUJ12BKnogZQZpy+Ucc8knwePWJrWqyqEHVmrTahEBqpbBoAH1n635wt3a9mjN1p8X2pw0qVEbKB/CO/c4OphSVP/2Q==">
<img :alt="''" class="on"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAQABADASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQIEBf/EACMQAAEDAwQDAQEAAAAAAAAAAAQBAgUDESEAEjFBBlFhMkL/xAAUAQEAAAAAAAAAAAAAAAAAAAAF/8QAGBEAAwEBAAAAAAAAAAAAAAAAABIiMUH/2gAMAwEAAhEDEQA/AM+Bg4mS8coRccMOdNG01qVyH/kRvHPKKmMdr8uujPwUTG+NkRpw1AKWCbvHKa2zTGphc9u9p0q+rLqeMl4kSCGkYgtoE0HTahIz3bWGNanPrdyqWzn7p5ibh5CArnyZNMyVLpK0QSkt2BNXtVX+7ol1wuLJiyaHt+6Kyp//2Q==">
</label>
</template>
<style lang="scss" scoped>
/* Minecraft switch made by: csozi | Website: english.csozi.hu*/
/* The switch - the box around the slider */
.switch {
font-size: 17px;
position: relative;
display: inline-block;
width: 2rem;
height: 1rem;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 1;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.off {
position: absolute;
content: "";
height: 1rem;
width: 1rem;
background-color: white;
transition: .4s;
image-rendering: pixelated;
opacity: 1;
}
.on {
position: absolute;
content: "";
height: 1rem;
width: 1rem;
background-color: white;
transition: 4s;
background-color: #ccc;
}
.switch input:focus+.slider {
box-shadow: 0 0 1px #ccc;
}
.switch input:checked~.off {
transform: translateX(1rem);
}
.switch input:checked~.on {
transform: translateX(1rem);
opacity: 1;
}
</style>

View File

@ -0,0 +1,219 @@
<script lang="ts" setup>
import {GridItem, GridLayout} from 'grid-layout-plus'
import ICard from "~/components/Cards/ICard.vue";
import IChart from "~/components/Cards/IChart.vue";
import {type IGridItem, type Layouts, useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {UseDraggable} from '@vueuse/components'
import AddCard from "~/components/AddCard.vue";
import {type dataHistoryType, useDataStore} from "~/strores/DataStore";
import _ from "lodash";
import {useLoadStore} from "~/strores/LoadStore";
import type {HttpType} from "~/types/baseType";
import type {UserInfoType} from "~/types/UserType";
import {deepEqual} from "~/utils";
const layout = ref<IGridItem[]>([])
const presetLayouts = ref<Layouts>(<Layouts>{})
const {width, height} = useWindowSize()
const el = ref(null)
const {width: gridWidth, height: gridHeight} = useElementSize(el)
const mainLayoutStore = useMainLayoutStore()
const loadStore = useLoadStore()
const dataStore = useDataStore()
onMounted(() => {
layout.value = mainLayoutStore.LayoutsConfig.lg
presetLayouts.value = mainLayoutStore.LayoutsConfig
console.log(layout.value)
console.log(presetLayouts.value)
})
const visible = ref(false)
const cardVisible = ref(false)
const items = ref([
{
label: '添加图表',
icon: 'pi pi-chart-line',
command: () => {
visible.value = true
}
},
{
label: '添加信息卡片',
icon: 'pi pi-plus',
command: () => {
cardVisible.value = true
}
},
{
label: 'Delete',
icon: 'pi pi-trash',
command: () => {
toast.add({severity: 'error', summary: 'Delete', detail: 'Data Deleted'});
}
},
{
label: 'Upload',
icon: 'pi pi-upload',
command: () => {
router.push('/fileupload');
}
},
{
label: 'Vue Website',
icon: 'pi pi-external-link',
command: () => {
window.location.href = 'https://vuejs.org/'
}
}
])
const layoutChangedEvent = _.throttle((newLayout: IGridItem[]) => {
let breakPoint = 'lg';
//breakPoint
if (gridWidth.value <= 1200) {
breakPoint = 'md';
}
if (gridWidth.value <= 992) {
breakPoint = 'sm';
}
if (gridWidth.value <= 768) {
breakPoint = 'xs';
}
mainLayoutStore.LayoutsConfig[breakPoint] = newLayout;
presetLayouts.value[breakPoint] = newLayout;
}, 500)
onMounted(() => {
$fetch('/Api/User/GetLayoutConfig', {
method: 'POST',
responseType: "json",
body: {
layout: JSON.stringify(mainLayoutStore.LayoutsConfig),
},
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
},
baseURL: useRuntimeConfig().public.baseUrl,
}).then((res) => {
if (res === 'null') return;
const data = JSON.parse(res as string) as Layouts
console.log(data)
mainLayoutStore.LayoutsConfig = data;
presetLayouts.value = data;
if (loadStore.LayoutLoad) return;
loadStore.LayoutLoad = true
window.location.reload()
})
})
watchDebounced(
layout,
() => {
$fetch('/Api/User/PutLayoutConfig', {
method: 'PUT',
body: {
Layout: JSON.stringify(mainLayoutStore.LayoutsConfig),
},
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
},
baseURL: useRuntimeConfig().public.baseUrl,
}).catch((err) => {
console.log(err)
})
},
{debounce: 15000, maxWait: 60000}
)
</script>
<template>
<div class="main-grid-layout">
<UseDraggable v-slot="{x,y}"
:initial-value="{ x: width-150, y: height-350 }"
class="draggable-action"
>
<SpeedDial :model="items"
:radius="80"
:tooltipOptions="{ position: 'right' }"
type="circle"/>
</UseDraggable>
<Dialog
v-model:visible="visible"
:pt="{
root: {
style:'border:unset;background-color:unset;'
},
mask: {
style: 'backdrop-filter: blur(20px)'
}
}"
modal
>
<template #container="{ closeCallback }">
<SettingCard :close-callback="closeCallback"/>
</template>
</Dialog>
<Dialog
v-model:visible="cardVisible"
:pt="{
root: {
style:'border:unset;background-color:unset;'
},
mask: {
style: 'backdrop-filter: blur(20px)'
}
}"
modal
>
<template #container="{ closeCallback }">
<AddCard :close-callback="closeCallback"/>
</template>
</Dialog>
<GridLayout
ref="el"
v-model:layout="layout"
:breakpoints="{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }"
:col-num="16"
:cols="{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }"
:responsive-layouts="presetLayouts"
:row-height="30"
is-draggable
is-resizables
responsive
use-css-transforms
vertical-compact
@layoutUpdated="layoutChangedEvent"
>
<GridItem
v-for="item in layout"
:key="item.i"
:h="item.h"
:i="item.i"
:w="item.w"
:x="item.x"
:y="item.y"
drag-allow-from=".vue-draggable-handle"
drag-ignore-from=".no-drag"
>
<IChart v-if="item.type==='chart'&&item.serverValues&&item.selectChart" :id="String(item.i)"
:chart="item.selectChart"
:title="item.cardConfig?.['name'].value??'默认标题'" :values="item.serverValues"/>
<ICard v-if="item.type==='card'&&item.selectCard" :id="String(item.i)" :card-id="item.selectCard"/>
</GridItem>
</GridLayout>
</div>
</template>
<style lang="scss" scoped>
.main-grid-layout {
//min-width: 950px;
}
.draggable-action {
background: red;
position: fixed;
z-index: 100;
}
.vgl-layout {
margin: -10px;
}
</style>

42
web/components/Grid/TopGrid.vue Executable file
View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import MiniCard from "~/components/Cards/MiniCard.vue";
const gridRef = ref<HTMLElement>();
</script>
<template>
<div ref="gridRef" class="top-grid-layout">
<MiniCard title="系统使用率" watcher="RequestJob"/>
<MiniCard title="CPU总使用率" watcher="CpuTotalUsage"/>
<MiniCard title="内存总使用率" watcher="MemoryTotalUsage"/>
<MiniCard title="读写使用率" watcher="CpuIOWaitUsage"/>
<MiniCard title="磁盘总使用率" watcher="DiskTotalUsage"/>
<MiniCard title="网络接口使用率" watcher="InterfaceTotalUtilizationPercentage"/>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.top-grid-layout {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: 1fr;
grid-gap: $gap*2;
overflow-x: auto;
}
@media screen and (max-width: 1800px) {
.top-grid-layout {
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (max-width: 1200px) {
.top-grid-layout {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

78
web/components/HeroBox.vue Executable file
View File

@ -0,0 +1,78 @@
<template>
<div class="circle-container">
<div
v-for="(circle, index) in circles"
:key="index"
:style="circle.style as any"
class="hero-box-circle"
></div>
</div>
</template>
<script lang="ts" setup>
import {computed} from 'vue';
//
const circleCount = 10;
//
const circles = computed(() => {
const circlesArray = [];
for (let i = 0; i < circleCount; i++) {
const size = 300 + (i + 1) * 300; //
const maxOffset = 20 + 7 * (i + 1); //
const randomOffsetX = Math.random() * (maxOffset + maxOffset) - maxOffset; // X
const randomOffsetY = Math.random() * (maxOffset + maxOffset) - maxOffset; // Y
circlesArray.push({
style: {
width: `${size}px`,
height: `${size}px`,
borderRadius: '50%',
position: 'absolute',
top: `20%`,
left: `20%`,
//
transform: `translate(${randomOffsetX - size / 2}px, ${randomOffsetY - size / 2}px)`,
zIndex: circleCount - i,
},
});
}
return circlesArray;
});
const {$gsap} = useNuxtApp()
onMounted(() => {
$gsap.fromTo('.hero-box-circle', {
opacity: 0,
scale: 0.8,
}, {
opacity: 1,
scale: 1,
duration: 0.6,
delay: 1,
ease: 'power3.out',
stagger: {
each: 0.1, //
from: 'center' //
},
});
})
</script>
<style lang="scss" scoped>
@import "base";
.circle-container {
position: relative;
width: 100%;
height: 5000px;
margin: auto;
//background: radial-gradient(50% 50% at 50% 50%, rgba($primary-color, 0.2) 0%, $primary-color 100%);
background: radial-gradient(36.28% 150.93% at 50% 50%, #3B8AFF 0%, #0048B2 100%);
}
.hero-box-circle {
//background: radial-gradient(50% 50% at 50% 50%, rgba($primary-color, 0.2) 0%, $primary-color 100%);
box-shadow: 24px 32px 184px 24px rgba(6, 8, 89, 0.75) inset;
}
</style>

34
web/components/Icon.vue Executable file
View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import { computed } from 'vue';
import * as icons from "lucide-vue-next";
const props = defineProps({
name: {
type: String,
default: 'LayoutGrid'
},
size: Number,
color: String,
strokeWidth: Number,
defaultClass: String,
fill:{
type: String,
default: 'none'
}
})
type IconsType = typeof icons;
const icon = computed(() => {
const iconName = props.name as keyof IconsType;
return icons[iconName] || icons['LayoutGrid'];
});
</script>
<template>
<component
:is="icon"
:size="size"
:color="color"
:stroke-width="strokeWidth" :default-class="defaultClass"
:fill="fill"
/>
</template>

44
web/components/Logo.vue Executable file
View File

@ -0,0 +1,44 @@
<script setup lang="ts">
</script>
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="195" height="178" viewBox="0 0 195 178" fill="none">
<path d="M168.555 175.454C169.002 175.454 169.447 175.444 169.891 175.423C161.92 175.127 154.273 170.866 149.985 163.454H26.9997C18.4692 163.454 12.7774 157.444 12.7774 151.454C12.7774 145.464 18.4692 139.454 26.9997 139.454H136.128L129.2 127.454H26.9997C13.1933 127.454 0.777428 137.562 0.777428 151.454C0.777428 165.347 13.1933 175.454 26.9997 175.454H168.555Z" fill="url(#paint0_linear_1_36)"/>
<path d="M22.7774 157.454H146.521L139.592 145.454H22.7774C19.4637 145.454 16.7774 148.141 16.7774 151.454C16.7774 154.768 19.4637 157.454 22.7774 157.454Z" fill="url(#paint1_linear_1_36)"/>
<path d="M120.325 17.086C120.102 16.6991 119.87 16.3187 119.63 15.9451C123.359 22.9959 123.493 31.7488 119.218 39.1685L180.711 145.677C184.976 153.064 182.617 160.999 177.43 163.994C172.242 166.988 164.192 165.064 159.926 157.677L105.362 63.1685L98.4337 75.1685L149.534 163.677C156.437 175.633 171.399 181.332 183.43 174.386C195.461 167.44 198.006 151.633 191.103 139.677L120.325 17.086Z" fill="url(#paint2_linear_1_36)"/>
<path d="M177.626 152.333L115.754 45.1685L108.826 57.1685L167.234 158.333C168.89 161.203 172.56 162.186 175.43 160.529C178.299 158.873 179.283 155.203 177.626 152.333Z" fill="url(#paint3_linear_1_36)"/>
<path d="M6.73101 137.041C6.50089 137.424 6.2806 137.811 6.07013 138.202C10.429 131.522 18.0196 127.162 26.5815 127.303L89.9235 21.8846C94.317 14.5726 102.4 12.7893 107.535 15.8743C112.669 18.9593 114.889 26.9335 110.496 34.2456L54.2901 127.787L68.1444 128.029L120.782 40.426C127.892 28.5916 125.623 12.7434 113.715 5.58826C101.807 -1.56686 86.7483 3.86979 79.6375 15.7042L6.73101 137.041Z" fill="url(#paint4_linear_1_36)"/>
<path d="M97.2411 21.3557L33.5087 127.424L47.363 127.666L107.527 27.5362C109.234 24.6958 108.315 21.0096 105.474 19.3029C102.634 17.5963 98.9478 18.5153 97.2411 21.3557Z" fill="url(#paint5_linear_1_36)"/>
<defs>
<linearGradient id="paint0_linear_1_36" x1="10.2766" y1="175.454" x2="178.06" y2="175.454" gradientUnits="userSpaceOnUse">
<stop stop-color="#30B56D" offset=""/>
<stop offset="1" stop-color="#2C0CEE"/>
</linearGradient>
<linearGradient id="paint1_linear_1_36" x1="10.2766" y1="175.454" x2="178.06" y2="175.454" gradientUnits="userSpaceOnUse">
<stop stop-color="#30B56D" offset=""/>
<stop offset="1" stop-color="#2C0CEE"/>
</linearGradient>
<linearGradient id="paint2_linear_1_36" x1="199.465" y1="154.159" x2="115.573" y2="8.85453" gradientUnits="userSpaceOnUse">
<stop stop-color="#30B56D" offset=""/>
<stop offset="1" stop-color="#2C0CEE"/>
</linearGradient>
<linearGradient id="paint3_linear_1_36" x1="199.465" y1="154.159" x2="115.573" y2="8.85453" gradientUnits="userSpaceOnUse">
<stop stop-color="#30B56D" offset=""/>
<stop offset="1" stop-color="#2C0CEE"/>
</linearGradient>
<linearGradient id="paint4_linear_1_36" x1="88.2506" y1="1.36972" x2="1.83564" y2="145.188" gradientUnits="userSpaceOnUse">
<stop stop-color="#30B56D" :offset="''"/>
<stop offset="1" stop-color="#2C0CEE"/>
</linearGradient>
<linearGradient id="paint5_linear_1_36" x1="88.2506" y1="1.36972" x2="1.83564" y2="145.188" gradientUnits="userSpaceOnUse">
<stop stop-color="#30B56D" :offset="''"/>
<stop offset="1" stop-color="#2C0CEE"/>
</linearGradient>
</defs>
</svg>
</template>
<style scoped lang="scss">
</style>

134
web/components/Notification.vue Executable file
View File

@ -0,0 +1,134 @@
<script lang="ts" setup>
import type {PropType} from "vue";
const props = defineProps({
label: String,
cardType: String as PropType<'Error' | 'Informative' | 'Success' | 'Warning'>,
time: Number,
text: String,
})
const notificationBoxClass = computed(() => ({
'Notification-Box': true,
'Notification-Box-Error': props.cardType === 'Error',
'Notification-Box-Informative': props.cardType === 'Informative',
'Notification-Box-Success': props.cardType === 'Success',
'Notification-Box-Warning': props.cardType === 'Warning'
}));
</script>
<template>
<div :class="notificationBoxClass">
<div class="Icon">
<NuxtImg v-if="cardType==='Success'" src="/Success.svg" width="24"></NuxtImg>
<NuxtImg v-if="cardType==='Informative'" src="/Informative.svg" width="24"></NuxtImg>
<NuxtImg v-if="cardType==='Warning'" src="/Warning.svg" width="24"></NuxtImg>
<NuxtImg v-if="cardType==='Error'" src="/Error.svg" width="24"></NuxtImg>
</div>
<div class="Text">
<h3>{{ label }}</h3>
<p>{{ text }}</p>
</div>
<Icon :stroke-width="1.2" name="X"></Icon>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.Notification-Box {
padding: $padding;
display: grid;
grid-template-columns: 24px 1fr 18px;
gap: $gap*2;
grid-template-rows: 1fr;
border-radius: 12px;
> svg {
stroke: #979FA9;
.dark-mode & {
stroke: #FFF;
}
}
}
.Icon {
.dark-mode & {
filter: grayscale(30%) brightness(100%);
}
}
.Text {
display: flex;
flex-direction: column;
gap: $gap*.5;
.dark-mode & {
> h3, p {
color: #FFF;
}
}
> h3 {
color: #27303A;
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 140%; /* 19.6px */
text-transform: capitalize;
}
> p {
color: #2F3F53;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 160%; /* 19.2px */
}
}
.Notification-Box-Success {
border: 1.3px solid #48C1B5;
background: #F6FFF9;
.dark-mode & {
border: 1.3px solid #43D590;
background: unset;
}
}
.Notification-Box-Informative {
border: 1.3px solid #9DC0EE;
background: #F5F9FF;
.dark-mode & {
border: 1.3px solid #7BCFED;
background: unset;
}
}
.Notification-Box-Warning {
border: 1.3px solid #F7D9A4;
background: #FFF8EC;
.dark-mode & {
border: 1.3px solid #FFDF8D;
background: unset;
}
}
.Notification-Box-Error {
border: 1.3px solid #F4B0A1;
background: #FFF5F3;
.dark-mode & {
border: 1.3px solid #F0863A;
background: unset;
}
}
</style>

483
web/components/SettingCard.vue Executable file
View File

@ -0,0 +1,483 @@
<script lang="ts" setup>
import {Check, ChevronRight, Settings} from "lucide-vue-next"
import {charts} from "~/config/charts";
import {type cardConfigType, type IGridItem, useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {v4 as uuidv4} from 'uuid';
const mainLayoutStore = useMainLayoutStore()
const select = ref(0)
export type serverValueItem = {
dataName: string,
dataType: string,
}
const props = defineProps({
closeCallback: {
type: Function,
required: true
},
isSetting: {
type: Boolean,
},
cardId: {
type: String,
}
})
const steps = ref([
{
label: '图表',
}, {
label: '数据',
}, {
label: '设置',
},
{
label: '预览',
end: true,
}
])
const ServerValues = ref<serverValueItem[]>([])
const ServerSelect = ref<serverValueItem[]>([])
const CardSettings = ref<cardConfigType>({
name: {
label: '卡片名称',
value: '默认卡片名称',
type: 'text',
},
description: {
label: '卡片描述',
value: '这是一张默认的卡片',
type: 'text',
},
foreground: {
label: '卡片前景色',
value: 'ffffff',
type: 'color',
},
color: {
label: '图表主色',
value: '002EA6',
type: 'color',
}
})
const ValueIds = ref<string[]>([])
const stepNext = () => {
if (select.value === 2) {
ValueIds.value = ServerSelect.value.map(item => item.dataType)
}
select.value++
}
const stepPrev = () => {
select.value--
}
const stepFinish = () => {
if (props.isSetting) {
for (const configKey in mainLayoutStore.LayoutsConfig) {
let config = mainLayoutStore.LayoutsConfig[configKey].find(x => x.i === props.cardId)
if (!config) return;
config = config as IGridItem
config.serverValues = ServerSelect.value
config.cardConfig = CardSettings.value
config.selectChart = charts[selectChart.value].id
}
props.closeCallback()
return
}
const newItem: IGridItem = {
type: "chart",
chartRage: 6,
serverValues: ServerSelect.value.filter(i => i.dataName && i.dataName !== ''),
i: uuidv4(),
x: 1,
y: 1,
h: 5,
w: 5,
selectChart: charts[selectChart.value].id,
cardConfig: CardSettings.value
};
console.log(newItem)
mainLayoutStore.addLayout(newItem)
props.closeCallback()
}
const addServerValue = () => {
console.log(ServerSelect.value)
ServerSelect.value.push(<serverValueItem>{});
}
const removeServerValue = (index: number) => {
// index 1
ServerSelect.value.splice(index, 1);
};
const selectChart = ref<number>(0);
onMounted(() => {
$fetch('/Api/Job/GetJobList', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + useCookie('token').value
},
params: {
serverId: mainLayoutStore.SelectServer.id
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
console.log(res)
ServerValues.value = res as serverValueItem[];
//
ServerValues.value.sort((a, b) => {
return a.dataName.localeCompare(b.dataName);
})
})
})
onMounted(() => {
if (!props.isSetting) {
return
}
select.value = 2
let config = mainLayoutStore.LayoutsConfig.lg.find(x => x.i === props.cardId)
// config
if (!config) return;
config = config as IGridItem
if (!config.serverValues) return;
ServerSelect.value = config.serverValues.map(x => {
return x
})
if (!config.cardConfig) return;
CardSettings.value = config.cardConfig
if (!config.selectChart) return;
selectChart.value = charts.findIndex(x => x.id == config.selectChart)
})
</script>
<template>
<div class="setting-card-layout">
<div class="card-top">
<template v-for="(step, index) in steps">
<div
:class="{'card-step-item':true,'card-step-item-active':select===index,'card-step-item-activated':select>index}">
<div><p v-if="select<=index">{{ index + 1 }}</p>
<Check v-else :size="18"/>
</div>
<h3>{{ step.label }}</h3>
</div>
<ChevronRight v-if="!step.end"/>
</template>
</div>
<div class="card-split"></div>
<div class="card-container">
<div v-if="select===0" class="step1-box">
<div v-for="(chart,index) in charts" :class="{'chart-box':true,'chart-box-active':selectChart===index}">
<NuxtImg v-tooltip="chart.description" :src="chart.image" draggable="false" @click="selectChart=index"/>
</div>
</div>
<div v-if="select===1" class="step2-box">
<div v-for="(chart,index) in ServerSelect" class="Select-box">
<p>数据槽 {{ index + 1 }}</p>
<Dropdown v-model="ServerSelect[index]" :highlightOnSelect="false" :options="ServerValues" :pt="{
root:{
style:'background:#eee'
}
}"
checkmark optionLabel="dataName" placeholder="选择一个数据来源"/>
<Icon :size="20" :stroke-width="0.7" name="X" @click="removeServerValue(index)"/>
</div>
<Button icon="pi pi-plus" label="添加一个数据槽" @click="addServerValue"/>
</div>
<div v-if="select===2" class="step3-box">
<div v-for="setting in CardSettings" class="setting-box">
<p>{{ setting.label }}</p>
<InputText v-if="setting.type==='text'" id="username" v-model="setting.value"/>
<ColorPicker v-if="setting.type==='color'" v-model="setting.value"/>
</div>
</div>
<div v-if="select===3" class="step4-box">
<div class="card-layout">
<div class="card-title">
<div></div>
<h3>{{ CardSettings['name'].value }}</h3>
</div>
<component :is="charts[selectChart].component" :valueIds="ValueIds"/>
</div>
</div>
</div>
<div class="card-action">
<button :style="{visibility:select>0?'visible':'hidden'}" @click="stepPrev">后退</button>
<button v-if="select<3" @click="stepNext">前进</button>
<button v-else @click="stepFinish">完成</button>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.setting-card-layout {
width: 100%;
display: grid;
padding: $padding*2;
grid-template-rows: auto auto 1fr auto;
gap: $gap*4;
background: $light-bg-color;
border-radius: $radius*2;
.dark-mode & {
background: $dark-bg-color;
}
}
.card-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: $gap*2;
}
.card-step-item {
display: flex;
align-items: center;
gap: $gap;
div {
display: flex;
width: 28px;
height: 28px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 100px;
font-size: 14px;
font-style: normal;
font-weight: 700;
color: #8D8D99;
background: #E1E1E6;
}
h3 {
color: #8D8D99;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 150%; /* 24px */
}
}
.card-step-item-active {
div {
color: #FFFFFF;
background: $primary-color;
}
h3 {
color: $light-text-color;
.dark-mode & {
color: $dark-text-color;
}
}
}
.card-step-item-activated {
div {
background: #1D8841;
svg {
stroke: #FFF;
margin-top: 2.5px;
}
}
h3 {
color: $light-text-color;
.dark-mode & {
color: $dark-text-color;
}
}
}
.card-split {
width: 95%;
margin-left: auto;
margin-right: auto;
height: 1px;
background: $unfocused-color;
}
.card-container {
min-height: 200px;
height: 100%;
max-height: 400px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
}
}
.card-action {
height: 56px;
gap: $gap*4;
display: flex;
justify-content: space-between;
button {
display: flex;
width: 158px;
padding: 16px 32px;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: 150%; /* 24px */
text-transform: uppercase;
border: unset;
outline: unset;
background: unset;
cursor: pointer;
border-radius: 6px;
}
button:first-of-type {
color: $primary-color;
border: 2px solid $primary-color;
}
button:last-of-type {
background: $primary-color;
color: #FFF;
}
}
.step1-box {
display: grid;
width: 100%;
height: 100%;
grid-template-columns: 1fr 1fr 1fr;
gap: $gap*2;
grid-template-rows: repeat(auto-fit, auto);
.chart-box {
min-width: 120px;
display: flex;
flex-direction: column;
gap: $gap;
border: 1px solid #E1E1E6;
height: min-content;
border-radius: $radius*2;
padding: $padding;
background: #f4f4f4;
.dark-mode & {
background: $dark-bg-underline-color;
border-color: $unfocused-color;
}
}
.chart-box-active {
border: 2px solid $primary-color;
}
}
.step2-box {
display: flex;
flex-direction: column;
gap: $gap*2;
padding: 0 0 $padding*4 0;
> button {
background: $primary-color;
cursor: pointer;
}
.Select-box {
display: flex;
align-items: center;
gap: $gap;
.p-dropdown {
flex: 1;
}
}
}
.step3-box {
display: flex;
flex-direction: column;
gap: $gap*2;
.setting-box {
display: flex;
gap: $gap;
align-items: center;
.p-inputtext {
flex: 1;
}
}
}
:deep(.p-inputtext) {
color: $light-text-color;
}
.step4-box {
background: $light-bg-underline-color;
padding: $padding;
height: 100%;
border-radius: $radius*2;
.dark-mode & {
background: $dark-bg-underline-color;
}
}
.card-layout {
width: 100%;
height: 100%;
min-height: 250px;
background: $light-bg-color;
border-radius: $radius*2;
box-shadow: 0 10px 30px 0 rgba(17, 38, 146, 0.05);
padding: $padding*1.5;
position: relative;
display: flex;
flex-direction: column;
gap: $gap*2;
&:hover {
cursor: move;
}
.dark-mode & {
background: $dark-bg-color;
}
}
.card-title {
display: flex;
align-items: center;
gap: $gap;
color: $light-text-color;
.dark-mode & {
color: $dark-text-color;
}
div {
width: 6px;
height: 20px;
border-radius: $radius;
background: $primary-color;
}
}
</style>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup>
</script>
<template>
13123
</template>
<style lang="scss" scoped></style>

46
web/components/Term.vue Normal file
View File

@ -0,0 +1,46 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
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
})
})
</script>
<template>
<div id="terminal">
<iframe :src="path"></iframe>
</div>
</template>
<style lang="scss" scoped>
@import "base";
#terminal {
min-width: 60vw;
min-height: 60vh;
padding: $padding;
background: #000;
border-radius: $radius*2;
iframe {
width: 100%;
height: 60vh;
border: none;
}
}
</style>

54
web/components/UserMini.vue Executable file
View File

@ -0,0 +1,54 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
const MainLayoutStore = useMainLayoutStore()
const userInfo = toRef(MainLayoutStore.UserInfo)
</script>
<template>
<div class="User-Mini-Box">
<NuxtImg :src="userInfo.avatar" alt="Avatar" height="45" width="45"/>
<div>
<h3>{{ userInfo.nickName }}</h3>
<p>{{ userInfo.desc ?? "荒芜之地 ~ ~" }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.User-Mini-Box {
height: min-content;
display: flex;
gap: $gap*2;
align-items: center;
justify-content: left;
padding: $padding;
h3 {
color: $light-text-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%;
}
p {
color: #8A92A6;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 130%;
//
word-break: break-all;
}
.dark-mode & {
h3, p {
color: $dark-text-color;
}
}
}
</style>

View File

@ -0,0 +1,149 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
const props = defineProps({
avatar: {
type: String,
default: ''
},
fullName: {
type: String,
default: ''
},
email: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
position: {
type: String,
default: ''
},
phone: {
type: String,
default: ''
},
physicalAddress: {
type: String,
default: '未设置'
},
loginIp: {
type: String,
default: ''
},
lastLoginTime: {
type: String,
default: ''
},
createTime: {
type: String,
default: ''
},
modifyTime: {
type: String,
default: ''
},
isLock: {
type: Boolean,
default: false
},
userId: {
type: String,
default: ''
}
})
const onlineUser = useMainLayoutStore().OnlineUsers
const isOnline = computed(() => {
return onlineUser.map(x => x.id).includes(props.userId)
})
</script>
<template>
<div class="user-item-layout">
<div class="user-start">
<Avatar :image="avatar" class="mr-2" shape="circle" size="xlarge"/>
</div>
<div class="user-item-box">
<h4>{{ name }}</h4>
<p>{{ email }}</p>
</div>
<div class="user-item-box">
<h4>{{ fullName }}</h4>
<p>{{ position }}</p>
</div>
<div class="user-item-box">
<h4>{{ phone }}</h4>
<p>{{ physicalAddress }}</p>
</div>
<div class="user-item-box">
<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>
</div>
<div class="user-item-box end">
<h4 :style="{
color: isOnline ? '#008000' : '#FF0000'
}">{{ isOnline ? '在线' : '离线' }}</h4>
<p>{{ isLock ? '锁定' : '' }}</p>
</div>
<div class="user-end">
<button>
<p>查看更多</p>
<Icon name="ChevronRight"></Icon>
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.user-item-layout {
display: flex;
background: $light-bg-color;
min-height: 100px;
border-radius: $radius;
align-items: center;
padding: $padding $padding*2;
gap: $gap*4;
}
.user-start, .user-end {
width: $padding*4;
}
.user-end {
width: $padding*6;
button {
display: flex;
align-items: center;
background: unset;
border: unset;
&:hover {
color: $primary-color;
cursor: pointer;
}
}
}
.user-item-box {
flex: 1;
overflow: hidden;
overflow-wrap: break-word;
word-break: keep-all;
}
.user-item-box.end {
flex: unset;
}
</style>

View File

@ -0,0 +1,126 @@
<script lang="ts" setup>
const fitters = [{
name: '全部',
value: 'all'
}, {
name: '已启用',
value: 'enable'
}, {
name: '禁用',
value: 'disable'
}, {
name: '未激活',
value: 'unActive'
}, {
name: '在线',
value: 'online'
}, {
name: '离线',
value: 'offline'
}]
const selectedFitters = ref([])
</script>
<template>
<div class="bar-layout">
<div class="user-num">
<Icon :size="30" :stroke-width="1.8" name="User"></Icon>
<h3>30</h3>
</div>
<div class="search-box">
<Icon name="UserRoundSearch"></Icon>
<input placeholder="搜索"/>
</div>
<div class="bar-actions">
<div class="sort-box">
<p>名称</p>
<Icon :size="16" :stroke-width="1.3" name="ArrowUpDown"/>
</div>
<MultiSelect v-model="selectedFitters" :maxSelectedLabels="3" :options="fitters" display="chip"
optionLabel="name" placeholder="过滤"></MultiSelect>
<button class="add-user">
<p>添加用户</p>
<Icon :size="18" :stroke-width="1.3" name="CirclePlus"></Icon>
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
@import 'base';
.bar-layout {
background: $light-bg-color;
display: flex;
align-items: center;
justify-content: space-between;
padding: $padding*2;
border-radius: $radius;
gap: 5rem;
}
.user-num {
display: flex;
align-items: center;
gap: $gap;
h3 {
font-size: 22px;
font-weight: 500;
}
}
.bar-actions {
display: flex;
gap: $gap*2;
}
.search-box {
display: flex;
flex: 1;
align-items: center;
gap: $gap;
padding: 0 $padding*2;
height: 100%;
background: #fff;
border-radius: 6px;
border: 1px solid #cbd5e1;
box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05);
color: #64748b;
max-width: 1200px;
input {
height: 100%;
width: 100%;
border: unset;
outline: unset;
background: transparent;
}
}
.sort-box {
display: flex;
gap: $gap*1.7;
align-items: center;
box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05);
border: 1px solid #cbd5e1;
border-radius: 6px;
color: #64748b;
padding: 0 $padding;
}
.add-user {
display: flex;
align-items: center;
gap: $gap*1.7;
background: $primary-color;
color: #FFF;
padding: 0 $padding;
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);;
}
</style>

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
</script>
<template>
<div class="Indicator-layout">
<div class="Indicator-item">
<h4>名称</h4>
<p>- 邮箱</p>
</div>
<div class="Indicator-item">
<h4>姓名</h4>
<p>- 职务</p>
</div>
<div class="Indicator-item">
<h4>手机号</h4>
<p>- 地址</p>
</div>
<div class="Indicator-item">
<h4>上次登陆时间</h4>
<p>- 登录IP</p>
</div>
<div class="Indicator-item">
<h4>修改日期</h4>
<p>-创建日期</p>
</div>
<div class="Indicator-item">
<h4>状态</h4>
<p>-锁定</p>
</div>
</div>
</template>
<style lang="scss" scoped>
@import 'base';
.Indicator-layout {
background-color: $light-bg-color;
display: flex;
align-items: center;
padding: $padding*1.5 $padding*10 $padding*1.5 $padding*8;
gap: $gap*4;
border-radius: $radius;
}
.Indicator-item {
display: flex;
flex-direction: column;
gap: $gap*.25;
flex: 1;
p {
color: $light-unfocused-color;
}
}
.Indicator-item:last-of-type {
flex: unset;
}
</style>

View File

@ -0,0 +1,50 @@
<script lang="ts" setup>
const el = ref(null)
const boxEl = ref(null)
const {width, height} = useElementSize(el)
const {width: boxWidth, height: boxHeight} = useElementSize(boxEl)
</script>
<template>
<div ref="boxEl" class="scroll-container">
<div class="v-scroll">
<div ref="el" class="content">
<slot></slot>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.scroll-container {
--w: calc(v-bind('width') * 1px);
--h: calc(v-bind('height') * 1px);
--bw: calc(v-bind('boxWidth') * 1px);
--bh: calc(v-bind('boxHeight') * 1px);
width: 100%;
height: var(--h);
}
.v-scroll {
height: var(--bw);
width: var(--bh);
position: relative;
transform-origin: left top;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
transform: rotate(-90deg) translateX(calc(var(--bh) * -1));
}
.content {
position: absolute;
left: var(--h);
transform-origin: left top;
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
</script>
<template>
<div class="BottomBar-Box">
<div class="Left-Text-Box">
<p>隐私策略</p>
<p>关于我们</p>
</div>
<div class="Right-Text-Box">
<p>Copyright © 2024 by <a href="https://ys-api.mihoyo.com/event/download_porter/link/ys_cn/official/pc_default"
target="_blank">
原神玩的队</a></p>
</div>
<!-- 吉祥物-->
<div class="Fox">
<iframe src="/Fox/index.html"></iframe>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.BottomBar-Box {
grid-area: folder;
display: flex;
padding: 16px 24px;
align-items: center;
background: $light-bg-color;
}
.Left-Text-Box {
display: flex;
gap: $gap;
flex: 1;
p {
@include SC_Font;
color: $light-text-color;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
}
.Right-Text-Box {
@extend .Left-Text-Box;
justify-content: flex-end;
a {
cursor: pointer;
color: $primary-color;
}
}
.dark-mode {
.BottomBar-Box {
background: $dark-bg-color;
}
p {
color: $dark-text-color
}
}
.Fox {
position: fixed;
right: -40px;
bottom: -110px;
z-index: 20;
height: 400px;
width: 300px;
iframe {
height: 100%;
width: 100%;
border: unset;
}
}
</style>

314
web/components/shell/SideBar.vue Executable file
View File

@ -0,0 +1,314 @@
<script lang="ts" setup>
import Logo from "~/components/Logo.vue";
import {ArrowLeft, Search} from 'lucide-vue-next';
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
const {$gsap} = useNuxtApp()
const mainLayoutStore = useMainLayoutStore()
const Menus = [
{
"label": "概览",
"icon": "LayoutGrid",
"route": "/Home"
}, {
"label": "用户",
"icon": "User",
"route": "/User"
}, {
"label": "主机",
"icon": "Cpu",
"route": "/host/cpu"
},
]
onMounted(() => {
const t1 = $gsap.timeline();
if (mainLayoutStore.IsLeftSidebarMini) {
t1.to(".sidebar-layout", {
duration: 0,
width: "104px",
ease: "power2.out",
}, 0)
t1.to(".sidebar-layout p,.sidebar-layout h3,.sidebar-layout .aa>svg", {
display: "none",
duration: 0,
}, 0)
t1.to(".sidebar-layout .user", {
duration: 0,
gap: 0
}, 0)
t1.to(".switch-button", {
duration: 0,
//3
rotate: 360 + 180,
ease: "power2.out",
}, 0)
}
t1.from(".sidebar-layout", {
duration: 0.5,
x: "-100%",
opacity: 0,
ease: "power2.out",
})
})
watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
const t1 = $gsap.timeline();
t1.to(".sidebar-layout .header,.folder", {
duration: 0.5,
x: "-200%",
opacity: 0,
ease: "power2.out",
}, 0)
if (newValue) {
t1.to(".sidebar-layout", {
duration: 0.5,
width: "104px",
ease: "power2.out",
}, 0.5)
t1.to(".switch-button", {
duration: 1,
//3
rotate: 180,
ease: "power2.out",
}, 0.5)
t1.to(".sidebar-layout p,.sidebar-layout h3,.sidebar-layout .aa>svg", {
display: "none",
duration: 0,
}, 0.5)
t1.to(".sidebar-layout .user", {
duration: 0,
gap: 0
}, 0.5)
} else {
t1.to(".sidebar-layout", {
duration: 0.5,
width: 210,
ease: "power2.out",
}, 0.5)
t1.to(".sidebar-layout", {
width: "auto",
}, 0.55)
t1.to(".switch-button", {
duration: 1,
y: 0,
rotate: 360,
ease: "power2.out",
}, 0.5)
t1.to(".sidebar-layout p,.sidebar-layout h3,.sidebar-layout .aa>svg", {
display: "block",
duration: 0,
ease: "power2.out",
}, 0.5)
t1.to(".sidebar-layout .user", {
duration: 0,
gap: "1rem"
}, 0.5)
}
t1.to(".sidebar-layout .header,.sidebar-layout .folder", {
duration: 0.5,
x: 0,
opacity: 1,
ease: "power2.out",
}, 1)
})
</script>
<template>
<section class="sidebar-layout">
<div class="switch-button" @click="mainLayoutStore.toggleLeftSidebarMini()">
<ArrowLeft/>
</div>
<div class="header">
<div class="user">
<!-- <Logo/>-->
<NuxtImg src="/Dragon_Head.gif" style="transform: scaleX(-1)" width="48"/>
<div class="name">
<h3>
龙盾云御
</h3>
</div>
</div>
<div class="search">
<Search/>
<p>搜索</p>
</div>
<div class="menus">
<div v-for="menu in Menus" v-tooltip="menu.label" class="menu-item" @click="navigateTo(menu.route)">
<Icon :name="menu.icon"/>
<p>{{ menu.label }}</p>
</div>
</div>
</div>
<div class="folder">
<div class="menu-item">
<Icon name="LogOut"/>
<p>登出</p>
</div>
<div class="menu-item aa">
<Icon name="Sun"/>
<p>颜色模式</p>
<color-switch/>
</div>
</div>
</section>
</template>
<style lang="scss" scoped>
@import "base";
.sidebar-layout {
display: flex;
grid-area: sidebar;
padding: 24px 24px 32px 24px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
flex-shrink: 0;
background: $light-bg-color;
position: relative;
.dark-mode & {
background: $dark-bg-color;
}
}
.header {
display: flex;
gap: 44px;
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.user {
display: flex;
gap: $gap*2;
align-items: center;
width: 100%;
justify-content: center;
> svg {
height: 44px;
width: 44px;
}
.name {
h3, p {
color: #09090A;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: normal;
.dark-mode & {
color: #FFF;
}
}
}
}
.search {
width: 100%;
padding: $padding;
background: $light-bg-underline-color;
display: flex;
align-items: center;
gap: $gap*2;
border-radius: $radius*2;
.dark-mode & {
background: $dark-bg-underline-color;
svg, p {
color: #FFF;
stroke: #FFF;
}
}
> p {
color: #2A2A2E;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 140%; /* 22.4px */
}
}
.menus {
width: 100%;
display: flex;
flex-direction: column;
gap: $gap*3;
}
.menu-item {
display: flex;
padding: $padding;
gap: $gap*2;
align-items: center;
cursor: pointer;
border-radius: $radius;
.dark-mode & {
> svg, p {
color: #FFF;
stroke: #FFF;
}
&:hover {
background: $dark-bg-underline-color;
}
}
&:hover {
background: $light-bg-underline-color;
}
> svg {
stroke: #09090A;
}
> p {
color: #09090A;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 140%; /* 22.4px */
}
}
.folder {
display: flex;
flex-direction: column;
width: 100%;
gap: $gap;
}
.switch-button {
position: absolute;
top: 2%;
right: -15px;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
width: 30px;
height: 30px;
cursor: pointer;
background: $primary-color;
color: #FFF;
border-radius: $radius*100;
box-shadow: 0 2px 4px 0 rgba(138, 146, 166, 0.30);
> svg {
width: 18px;
height: 18px;
}
}
</style>

View File

@ -0,0 +1,67 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
const {$gsap} = useNuxtApp()
const mainLayoutStore = useMainLayoutStore()
import {Mail, BellRing} from 'lucide-vue-next';
import SidebarRight from "~/components/SidebarRight.vue";
const visibleRight = ref(false)
</script>
<template>
<section class="title-bar-layout">
<Sidebar v-model:visible="visibleRight" header="通知" position="right">
<SidebarRight/>
</Sidebar>
<h1 class="name">
{{ $router.currentRoute.value.name }}
</h1>
<div class="action">
<BellRing @click="visibleRight=true"/>
<Mail/>
</div>
<div class="user">
<UserMini/>
</div>
</section>
</template>
<style lang="scss" scoped>
@import "base";
.title-bar-layout {
grid-area: titlebar;
background: $light-bg-color;
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-rows: 1fr;
align-items: center;
padding: 0 $padding*2;
gap: $gap*2;
.dark-mode & {
background: $dark-bg-color;
}
}
.name {
font-weight: 700;
font-size: 26px;
color: $light-text-color;
.dark-mode & {
color: $dark-text-color;
}
}
.user {
grid-column: 4;
}
.action {
display: flex;
gap: $gap*2;
grid-column: 3;
}
</style>

21
web/config/cards.ts Normal file
View File

@ -0,0 +1,21 @@
export type cardItem = {
id: string,
name: string,
description: string,
component?: any,
}
export const cards: cardItem[] = [
{
id: "systemInfo",
name: "系统信息卡片",
description: "显示系统信息的卡片",
component: defineAsyncComponent(() => import("../components/Cards/SystemInfoCard.vue"))
}, {
id: "onlineUsers",
name: "在线用户卡片",
description: "显示在线用户的卡片",
component: defineAsyncComponent(() => import("../components/Cards/OnlineUsersCard.vue"))
}
]

65
web/config/charts.ts Executable file
View File

@ -0,0 +1,65 @@
export const charts: chartItem[] = [
{
id: 'pie',
name: '饼状图',
description: '饼状图描述',
image: '/chartIcon/Pie.svg',
component: defineAsyncComponent(() => import(`~/components/Charts/PieChart.vue`))
}, {
id: 'area',
name: '面积图',
description: '面积图描述',
image: '/chartIcon/Area.svg',
component: defineAsyncComponent(() => import(`~/components/Charts/AreaChart.vue`))
}, {
id: 'bubble',
name: '气泡图',
description: '气泡图描述',
image: '/chartIcon/Bubble.svg'
}, {
id: 'doughunt',
name: '环形图',
description: '环形图描述',
image: '/chartIcon/Doughnut.svg'
}, {
id: 'radar',
name: '雷达图',
description: '雷达图描述',
image: '/chartIcon/Filled_Radar.svg'
}, {
id: 'histogram',
name: '柱状图',
description: '柱状图描述',
image: '/chartIcon/Histogram.svg',
component: defineAsyncComponent(() => import(`~/components/Charts/BarChart.vue`))
}, {
id: 'circle',
name: '容积图',
description: '容积图描述',
image: '/chartIcon/Increasing_Circle_Process.svg'
}, {
id: 'line',
name: '折线图',
description: '折线图描述',
image: '/chartIcon/Line.svg',
component: defineAsyncComponent(() => import(`~/components/Charts/LineChart.vue`))
}, {
id: 'pareto',
name: '帕累托图',
description: '帕累托图描述',
image: '/chartIcon/Pareto.svg'
}, {
id: 'scatter',
name: '散点图',
description: '散点图描述',
image: '/chartIcon/Scatter.svg'
}
]
export type chartItem = {
id: string,
name: string,
description: string,
image: string,
component?: any,
}

View File

@ -0,0 +1,6 @@
//创建UserId和UserName的对照表
export const userIdToUserName: { [key: string]: string } = {
User: "用户",
Admin: "管理员",
Manager: "经理",
}

352
web/layouts/Login.vue Executable file
View File

@ -0,0 +1,352 @@
<script lang="ts" setup>
import {useState} from "#app";
import {Sun,Moon,SunMoon} from "lucide-vue-next"
const colorMode = useColorMode()
//
const route = useRoute();
const IsLogin = useState("IsLogin", () => route.path.toLowerCase() === "/signin")
//
watch(() => route.path, (newValue, _) => {
IsLogin.value = newValue.toLowerCase() === "/Signin"
})
const {$gsap} = useNuxtApp()
onMounted(() => {
//
//线
const tl = $gsap.timeline({defaults: {duration: 0.5}})
//
tl.fromTo(".login-container", {opacity: 0}, {opacity: 1, duration: 1})
.fromTo(".Box", {opacity: 0, y: 100}, {opacity: 1, y: 0, duration: 1}, "-=1")
.fromTo(".Hero-Ornament", {opacity: 0, scale: 0}, {opacity: 1, scale: 1, duration: 1, stagger: 0.5}, "-=1")
})
</script>
<template>
<div class="login-container">
<div class="Box">
<!-- 深色模式切换-->
<div class="ColorSwitch">
<Sun @click="$colorMode.preference='system'" v-if="colorMode.preference==='light'"/>
<Moon @click="$colorMode.preference='light'" v-if="colorMode.preference==='dark'"/>
<SunMoon @click="$colorMode.preference='dark'" v-if="colorMode.preference==='system'"/>
</div>
<div class="Header-Nav">
<ul>
<li><a>主页</a></li>
<li><a>技术与支持</a></li>
<li><a>关于我们</a></li>
</ul>
</div>
<div class="Header-Action">
<NuxtLink to="/SignIn">
<button :data-is-selected=IsLogin @click="()=>IsLogin=true">登录</button>
</NuxtLink>
<NuxtLink to="/SignUp">
<button :data-is-selected=!IsLogin @click="()=>IsLogin=false">注册</button>
</NuxtLink>
</div>
<div class="Body-Hero">
<div class="Hero-Ornament"/>
<div class="Hero-Ornament"/>
<div class="Hero-Text">
<h1><span>Loong</span>Panel</h1>
<h1>欢迎来到龙芯运维平台</h1>
<p>Welcome to the <span>Loong</span> Panel</p>
<p>如果还没有账号?
<button>去注册</button>
</p>
</div>
</div>
<div class="Body-Login">
<h1>LoongPanel</h1>
<div class="Login-Container-Bg">
<slot/>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.login-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: $light-bg-color;
overflow: hidden;
transition: all 0.5s ease-in-out;
}
.ColorSwitch{
position: absolute;
top: 2%;
right: 2%;
cursor: pointer;
}
.Box {
width: 100%;
height: 100%;
max-height: 1080px;
max-width: 1920px;
padding: 100px 200px;
overflow: visible;
display: grid;
position: relative;
overflow-y: auto;
grid-template-rows: 42px 1fr;
grid-template-columns: minmax(auto, 1fr) auto;
grid-template-areas:
"Nav Action"
"BodyL BodyR";
&::-webkit-scrollbar{
width: 0;
}
}
.Header-Nav {
grid-area: Nav;
display: flex;
align-items: center;
gap: $gap*3;
& > ul {
display: flex;
gap: $gap*3;
justify-items: center;
list-style: none; /*关闭小圆点*/
a {
@include SC_Font;
cursor: pointer;
color: $light-text-color;
text-decoration: none;
font-size: 17px;
font-style: normal;
font-weight: 600;
line-height: normal;
letter-spacing: 1.02px;
&:hover {
color: $primary-color;
}
}
}
}
.Header-Action {
grid-area: Action;
display: flex;
align-items: center;
justify-content: center;
& button {
@include SC_Font;
cursor: pointer;
height: 42px;
padding: 0 $padding*2;
border-radius: $radius*2;
border: unset;
background: unset;
color: $light-text-color;
font-size: 17px;
font-style: normal;
font-weight: 700;
line-height: normal;
letter-spacing: 1.02px;
}
& button[data-is-selected=true] {
color: $primary-color;
position: relative;
&:after {
content: "";
position: absolute;
width: 26px;
height: 3px;
background: $primary-color;
border-radius: 2px; /* 添加圆角 */
left: 50%;
bottom: 0; /* 将矩形置于按钮下方 */
transform: translateX(-50%); /* 水平居中 */
}
}
}
.Body-Hero {
grid-area: BodyL;
position: relative;
& > .Hero-Ornament {
position: absolute;
width: 226px;
height: 226px;
flex-shrink: 0;
filter: blur(180.5px);
animation-delay: 0.4s;
&:first-child {
left: 100px;
background: $primary-color;
opacity: 0.55;
top: 166px;
}
&:nth-child(2) {
left: 320px;
background: $secondary-color;
opacity: 0.45;
top: 432px;
animation-delay: 0.6s;
}
}
& > .Hero-Text {
@include SC_Font;
position: absolute;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
z-index: 2;
padding: $padding*2;
& > h1 {
color: #000;
font-size: 58px;
font-style: normal;
font-weight: 700;
line-height: 77px;
& > span {
color: $primary-color;
text-shadow: 0, 0, 10px $primary-color;
}
}
& > p {
color: #000;
font-size: 23px;
font-style: normal;
font-weight: 500;
line-height: normal;
&:nth-child(3) {
margin-bottom: 77px;
}
& > button {
cursor: pointer;
background: unset;
border: unset;
color: $primary-color;
font-size: 23px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
}
}
}
.Body-Login {
grid-area: BodyR;
padding: $padding;
display: flex;
gap: $gap*5;
flex-direction: column;
justify-content: center;
align-items: center;
width: 600px;
>h1{
display: none;
}
>.Login-Container-Bg{
width: min-content;
height: min-content;
padding: $padding*2;
border-radius: $radius*2;
justify-content: center;
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(40px);
z-index: 20;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.45);
}
.Dividing_Line {
& :deep(.n-divider__line) {
background: #ACADAC;
}
& :deep(.n-divider__title) {
color: #ACADAC;
}
}
}
@media screen and (max-width: 1920px) {
.Box {
padding: 50px 100px;
}
}
@media screen and (max-width: 1600px) {
.Body-Hero {
& > .Hero-Image {
display: none;
}
}
}
.dark-mode {
.login-container {
background: $dark-bg-color;
*>h1,h2,h3,h4,h5,h6,p,span,a{
color: #FFFFFF;
}
}
button[data-is-selected=false]{
color: #ACADAC;
}
.Header-Nav{
a{
color: #FFFFFF;
}
}
.Body-Hero{
&>.Hero-Ornament{
opacity: 1;
}
}
}
@media screen and (max-width: 1500px) {
.Header-Nav{
display: none;
}
.Body-Hero{
>.Hero-Text{
display: none;
}
}
.Header-Action{
grid-column: 1/3;
}
.Body-Login{
width: 100%;
grid-column: 1/3;
>h1{
display: block;
}
}
}
</style>

308
web/layouts/Main.vue Executable file
View File

@ -0,0 +1,308 @@
<script lang="ts" setup>
import SideBar from "~/components/shell/SideBar.vue";
import TitleBar from "~/components/shell/TitleBar.vue";
import FloaterBar from "~/components/shell/FloaterBar.vue";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
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";
const audio = ref<any>(null);
const toast = useToast()
const visible = ref<boolean>(false)
const DataStore = useDataStore()
const {$gsap} = useNuxtApp()
const mainRef = ref<HTMLElement>()
const mainLayoutStore = useMainLayoutStore()
const ServerList = ref<{ name: string, id?: string }[]>([{name: 'Australia'},
{name: 'Brazil'},
{name: 'China'},
{name: 'Egypt'},
{name: 'France'},
{name: 'Germany'},
{name: 'India'},
{name: 'Japan'},
{name: 'Spain'},
{name: 'United States'}])
const {isScrolling} = useScroll(mainRef)
watch(isScrolling, (newValue) => {
mainLayoutStore.IsScrolling = newValue
})
onMounted(() => {
$fetch('/Api/User/Info', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
const data = res as HttpType<UserInfoType>
if (data.code === 200) {
mainLayoutStore.UserInfo = data.data
}
})
})
//
onMounted(() => {
$fetch('/Api/Server/GetServerList', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + useCookie('token').value
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
const data = res as { name: string, id?: string }[]
ServerList.value = data;
if (!mainLayoutStore.SelectServer.id) {
mainLayoutStore.SelectServer = data[0];
}
})
})
const signalR = useSessionSignalRStore();
//
onMounted(() => {
signalR.initConnection();
signalR.connection?.on('userJoined', (data: any) => {
mainLayoutStore.setOnlineUsers(data)
console.log(data);
})
signalR.connection?.on('userLeft', (data: any) => {
const uses = mainLayoutStore.OnlineUsers.filter(item => item.id !== data)
mainLayoutStore.setOnlineUsers(uses)
})
signalR.connection?.on('ReceiveData', (id: string, type: string, message: string) => {
if (id !== mainLayoutStore.SelectServer.id) return
DataStore.setData(type, message)
})
signalR.connection?.on("sendMessage", (id, message) => {
console.log(id)
audio.value.currentTime = 0;
audio.value && audio.value.click()
audio.value && audio.value.play()
const user = mainLayoutStore.OnlineUsers.find(user => user.id === id)
toast.info(`${user?.nickName}向你发送了一条消息`, {
position: POSITION.BOTTOM_RIGHT,
})
console.log(`${user?.nickName}对你说: ${message}`)
setTimeout(() => {
audio.value && audio.value.pause()
const speech = useSpeechSynthesis(`${user?.nickName}对你说:${message}`, {
voice: window.speechSynthesis.getVoices().find(v => v.name === 'Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland) (zh-CN)') ?? window.speechSynthesis.getVoices().find(v => v.lang.includes('zh-CN')) ?? window.speechSynthesis.getVoices()[0],
lang: 'zh-CN',
pitch: 1,
rate: 1,
})
speech.speak()
}, 2000)
})
})
onUnmounted(() => {
//
signalR.connection?.stop()
})
const getHistoryData = async () => {
$fetch('/Api/Server/GetServerHistoryDate', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
},
params: {
ServerId: mainLayoutStore.SelectServer.id,
},
baseURL: useRuntimeConfig().public.baseUrl,
}).then((res) => {
const data = res as dataHistoryType
DataStore.dataHistory = data
const datas = data.data
console.log(datas)
//
for (const key in datas) {
if (Object.prototype.hasOwnProperty.call(datas, key)) {
const element = datas[key];
//
DataStore.data[key] = element[element.length - 1];
}
}
console.log("dasdas")
DataStore.startTimer()
})
}
onMounted(async () => {
await getHistoryData()
})
watch(() => mainLayoutStore.SelectServer.id, async () => {
await getHistoryData()
})
onKeyStroke('Shift', (e) => {
e.preventDefault()
visible.value = !visible.value
})
</script>
<template>
<section class="layout">
<audio ref="audio">
<source src="/audios/idle1.mp3" type="audio/mpeg">
您的浏览器不支持 audio 元素
</audio>
<SideBar/>
<TitleBar/>
<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 }">
<Term/>
</template>
</Dialog>
<div class="main-box">
<div ref="mainRef" class="main">
<div class="hero">
<div class="hero-box">
<h3>你好 {{ mainLayoutStore.UserInfo.nickName }}</h3>
<p>欢迎来到龙盾云御,这是基于Nuxt+Vue3的后台管理系统</p>
</div>
<div class="server-dropdown">
<Dropdown v-model="mainLayoutStore.SelectServer" :options="ServerList"
class="server-dropdown-input"
optionLabel="name"
placeholder="选择服务器">
<template #value="slotProps">
<div v-if="slotProps.value">
<div style="color: #FFFFFF;gap: 8px;display: flex;align-items: center">{{ slotProps.value.name }}
<Tag :value="slotProps.value.id"/>
</div>
</div>
<span v-else>
{{ slotProps.placeholder }}
</span>
</template>
<template #option="slotProps">
<div
style="display: flex;align-items: center;justify-content: space-between;width: 100%;">
{{ slotProps.option.name }}
<Tag :value="slotProps.option.id"/>
</div>
</template>
</Dropdown>
</div>
<HeroBox/>
</div>
<div class="out">
<slot/>
</div>
</div>
</div>
<FloaterBar/>
</section>
</template>
<style lang="scss" scoped>
@import "base";
.layout {
width: 100%;
height: 100vh;
max-width: 100vw;
display: grid;
grid:
"sidebar titlebar" 76px
"sidebar main" 1fr
"sidebar folder" auto
/ auto 1fr;
grid-auto-flow: row dense;
}
.main-box {
grid-area: main;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
}
}
.main {
display: grid;
width: 100%;
grid-template-rows: 145px 55px 1fr;
will-change: scroll-position;
padding-bottom: 400px;
}
.hero {
grid-row: 1/3;
grid-column: 1;
overflow: hidden;
border-radius: 0 0 $radius*2 $radius*2;
position: relative;
.hero-box {
position: absolute;
z-index: 11;
left: $padding*2;
top: $padding;
display: flex;
flex-direction: column;
gap: $gap;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
h3, p {
color: #FFF;
font-size: 40px;
font-style: normal;
font-weight: 700;
line-height: normal;
}
p {
font-size: 23px;
font-style: normal;
font-weight: 500;
line-height: 130%; /* 29.9px */
}
}
}
.server-dropdown {
position: absolute;
z-index: 11;
right: $padding*2.5;
top: $padding*2;
}
.out {
grid-row: 2/4;
grid-column: 1;
z-index: 10;
width: 100%;
display: flex;
padding: 0 40px 40px;
}
.server-dropdown-input {
background: $primary-color;
color: #FFF;
border: unset;
}
</style>

57
web/middleware/auth.ts Executable file
View File

@ -0,0 +1,57 @@
import type {HttpType} from "~/types/baseType";
import {defineNuxtRouteMiddleware} from "#app";
import {useToast} from 'vue-toastification'
interface RouteBackType extends HttpType<any> {
}
export default defineNuxtRouteMiddleware(async (to, from) => {
const toast = useToast();
const runtimeConfig = useRuntimeConfig();
const token = useCookie('token').value;
if (!token) {
toast.error('未登录', {timeout: 3000})
return navigateTo("/SignIn");
}
//如果导航到/
if (to.path === '/' && token) {
return navigateTo("/Home");
}
// 获取当前导航路径
const currentPath = to.path;
try {
const response = await $fetch('/Api/Rote/RoteVerify', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
},
baseURL: runtimeConfig.public.baseUrl,
params: {'path': currentPath}
});
// 直接从响应中获取状态码
const data = response as HttpType<any>;
if (data.code !== 200) {
toast.error('未登录', {timeout: 3000})
if (data.code === 403) {
if (to.path === from.path) {
return navigateTo("/Home");
}
return navigateTo(from.path);
}
return navigateTo("/SignIn");
}
} catch (error) {
// 处理错误情况
console.error('请求验证路由时发生错误:', error);
toast.error('请求错误', {timeout: 3000})
return navigateTo("/SignIn");
}
return true;
});

61
web/nuxt.config.ts Executable file
View File

@ -0,0 +1,61 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import * as fs from "node:fs";
export default defineNuxtConfig({
devtools: {enabled: true},
ssr: false,
modules: ['@nuxtjs/color-mode', '@hypernym/nuxt-gsap', "@nuxt/image", "@nuxtjs/google-fonts", "nuxt-primevue", 'nuxt-lucide-icons', '@pinia/nuxt', '@pinia-plugin-persistedstate/nuxt', "@vite-pwa/nuxt", "@vueuse/nuxt"],
pwa: {
manifest: {
name: "pwa nuxt 3",
short_name: "pwa nuxt",
theme_color: '#FFFFFF',
description: "Arman Abi r.man.abi@gmail.com",
icons: [{
src: '/pwa-192x192.png', sizes: "192x192", type: "image/png"
},]
}, workbox: {
navigateFallback: "/SignIn",
}, devOptions: {
enabled: true, type: "module"
}
},
gsap: {
autoImport: true,
extraPlugins: {
text: true
}
},
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",
// }
},
plugins: [
{src: '~/plugins/vue-toast.ts'},
{src: '~/plugins/apexcharts.ts'},
],
runtimeConfig: {
baseUrl: '', public: {
apiBase: '/Api', baseUrl: process.env.API_SERVER
}
},
googleFonts: {
families: {
Roboto: true, 'Josefin+Sans': true, Lato: [100, 300], Raleway: {
wght: [100, 400], ital: [100]
}, Inter: '200..700', 'Crimson Pro': {
wght: '200..900', ital: '200..700',
}, 'Noto Sans SC': {
wght: '200..900', ital: '200..700'
}
}
},
})

53
web/package.json Executable file
View File

@ -0,0 +1,53 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"postUpdate": "patch-package"
},
"dependencies": {
"@nuxt/image": "^1.7.0",
"@nuxtjs/google-fonts": "^3.2.0",
"@types/lodash": "^4.17.5",
"@vite-pwa/nuxt": "^0.8.0",
"@vueuse/components": "^10.10.1",
"@xterm/xterm": "^5.5.0",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"grid-layout-plus": "^1.0.5",
"lodash": "^4.17.21",
"nuxt": "^3.11.2",
"nuxt-primevue": "^3.0.0",
"patch-package": "^8.0.0",
"primeicons": "^7.0.0",
"uuid": "^9.0.1",
"vue": "^3.4.27",
"vue-echarts": "^6.7.3",
"vue-router": "^4.3.2",
"vue-toastification": "^2.0.0-rc.5",
"vue3-apexcharts": "^1.5.3",
"xterm-addon-fit": "^0.8.0",
"yup": "^1.4.0"
},
"devDependencies": {
"@hypernym/nuxt-gsap": "^2.4.2",
"@microsoft/signalr": "^8.0.0",
"@nuxtjs/color-mode": "^3.4.1",
"@pinia-plugin-persistedstate/nuxt": "^1.2.0",
"@pinia/nuxt": "^0.5.1",
"@types/uuid": "^9.0.8",
"@vite-pwa/assets-generator": "^0.2.4",
"@vueuse/core": "^10.11.0",
"@vueuse/nuxt": "^10.10.0",
"nuxt-gsap-module": "^2.0.0",
"nuxt-lucide-icons": "^1.0.4",
"sass": "^1.77.4",
"vue3-draggable-grid": "^0.0.6",
"vue3-puzzle-vcode": "^1.1.7"
}
}

255
web/pages/SignIn.vue Executable file
View File

@ -0,0 +1,255 @@
<script lang="ts" setup>
import Vcode from 'vue3-puzzle-vcode';
definePageMeta({
layout: 'login',
})
import * as Yup from 'yup';
import type {HttpType} from "~/types/baseType";
import {useToast} from "#imports";
const toast = useToast()
const errors = ref<string[]>([]);
const isShow = ref(false);
//
const form = reactive({
emailOrUserName: '',
password: '',
remember: false
});
const schema = Yup.object().shape({
emailOrUserName: Yup.string().required('邮箱或用户名不能为空'),
password: Yup.string().min(6, '密码至少需要6个字符').required('密码不能为空')
});
//
const handleSubmit = async () => {
try {
//
await schema.validate(form, {abortEarly: false});
errors.value = []; //
//
isShow.value = true;
// API
} catch (error) {
//
if (error instanceof Yup.ValidationError) {
errors.value = error.inner.map(e => e.message);
}
}
};
const onSuccess = () => {
isShow.value = false;
$fetch('/Api/Account/Login', {
method: 'post',
body: form,
baseURL: useRuntimeConfig().public.baseUrl,
}).then((res) => {
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})
setTimeout(() => {
navigateTo('/Home')
}, 1000)
} else {
toast.add({severity: 'error', summary: data.message, detail: "发生了错误", life: 3000})
}
})
}
</script>
<template>
<div class="SignIn-Box">
<Vcode :show="isShow" @success="onSuccess"/>
<div class="SignIn-Box-Header"><h1>登录</h1>
<h3>登录以保持连接.</h3></div>
<div class="SignIn-Box-From">
<div v-if="errors.length>0" class="SignIn-Box-From-Errors">
<InlineMessage v-for="error in errors" severity="error">{{ error }}</InlineMessage>
</div>
<form @submit.prevent="handleSubmit">
<div class="From-Group">
<label for="email">邮箱/用户名</label>
<InputText id="email" v-model="form.emailOrUserName" aria-describedby="username-help"/>
</div>
<div class="From-Group">
<label for="password">密码</label>
<Password id="password" v-model="form.password" :feedback="false"/>
</div>
<div class="From-Group-Check">
<div class="flex align-items-center">
<Checkbox v-model="form.remember" :binary="true" inputId="remember" name="remember"/>
<label class="ml-2" for="remember"> 记住我? </label>
</div>
<a href="#">
忘记密码?
</a>
</div>
<div class="From-Action">
<Button label="登录" type="submit"/>
</div>
</form>
</div>
<div class="SignIn-Box-Bottom">
<p>还是使用其他帐户登录</p>
<div>
<NuxtImg height="40" src="/Gmail.svg" width="40"/>
<NuxtImg height="40" src="/Facebook.svg" width="40"/>
<NuxtImg height="40" src="/Instagram.svg" width="40"/>
<NuxtImg height="40" src="/Linkedin.svg" width="40"/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.SignIn-Box {
display: flex;
flex-direction: column;
gap: $gap*2;
width: 360px;
> * {
transition: all 0.3s ease;
}
}
.SignIn-Box-Header {
display: flex;
flex-direction: column;
gap: $gap*2;
align-items: center;
h3 {
color: $unfocused-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%; /* 28px */
}
}
.SignIn-Box-From {
display: flex;
flex-direction: column;
gap: $gap*2;
> form {
display: flex;
flex-direction: column;
gap: $gap*2;
}
}
.From-Group {
display: flex;
flex-direction: column;
gap: $gap;
label {
color: $unfocused-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%; /* 28px */
}
}
.SignIn-Box-Bottom {
display: flex;
gap: $gap*2;
flex-direction: column;
align-items: center;
justify-content: center;
> p {
color: $light-text-color;
font-size: 16px;
}
> div {
display: flex;
height: 30px;
overflow: hidden;
gap: 24px;
}
}
.SignIn-Box-From-Errors {
display: flex;
flex-direction: column;
align-items: center;
gap: $gap;
.p-inline-message {
padding: $padding*.25 $padding;
min-width: 250px;
gap: $gap;
}
}
//primeVue
:deep(.p-inputtext) {
padding: $padding*.5 $padding;
border-radius: $radius;
width: 100%;
border: 1px solid $primary-color;
&:enabled:hover {
border: 1px solid $primary-color;
box-shadow: 0 0 0 2px $primary-color;
}
&:enabled:focus {
outline: 2px solid $primary-color;
}
}
.From-Group-Check {
display: flex;
justify-content: space-between;
& > * {
display: flex;
align-items: center;
gap: $gap;
}
label {
color: $unfocused-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%; /* 28px */
}
}
.From-Action {
display: flex;
justify-content: center;
margin-top: $padding*.5;
.p-button {
display: flex;
width: 100%;
padding: $padding*.75;
justify-content: center;
align-items: center;
border-radius: $radius;
background: $primary-color;
}
}
.dark-mode {
h1, h2, h3, p {
color: $dark-text-color;
}
:deep(.p-inputtext) {
background: $dark-bg-color;
}
}
</style>

459
web/pages/SignUp.vue Executable file
View File

@ -0,0 +1,459 @@
<script lang="ts" setup>
import {useToast} from "#imports";
import Vcode from "vue3-puzzle-vcode";
definePageMeta({
layout: 'login',
})
import * as Yup from 'yup';
import {Minus} from 'lucide-vue-next'
import type {HttpType} from "~/types/baseType";
const toast = useToast()
const form = reactive({
username: '',
nickname: '',
email: '',
phoneNumber: '',
password: '',
confirmPassword: '',
remember: false
});
type FormErrorMessages = {
[K in keyof typeof form]?: string;
};
const EmailDialog = ref(false);
const isShow = ref(false);
const errors = ref<FormErrorMessages>({});
const code = ref<string>();
const schema = Yup.object().shape({
username: Yup.string()
.required('用户名是必填项')
.min(3, '用户名至少需要3个字符')
.max(15, '用户名不能超过15个字符'),
nickname: Yup.string()
.max(15, '昵称不能超过15个字符')
.required('昵称是必填项'),
email: Yup.string()
.email('邮箱地址格式不正确')
.required('邮箱地址是必填项'),
phoneNumber: Yup.string()
.matches(/^1[3-9]\d{9}$/, '手机号码格式不正确'), // 使
password: Yup.string()
.required('密码是必填项')
.min(6, '密码至少需要6个字符')
.matches(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^&*()_+])[0-9a-zA-Z!@#$%^&*()_+]{6,}/, '密码必须包含字母、数字和至少一个特殊字符'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], '两次输入的密码必须一致')
.required('请再次输入密码'),
remember: Yup.boolean().oneOf([true], '请同意用户协议')
});
const handleSubmit = async () => {
try {
//
await schema.validate(form, {abortEarly: false});
errors.value = {}; //
isShow.value = true;
} catch (error) {
//
if (error instanceof Yup.ValidationError) {
const errorMessages: FormErrorMessages = {};
error.inner.forEach((e) => {
// e.pathform
if (e.path) {
errorMessages[e.path as keyof typeof form] = e.message;
}
});
errors.value = errorMessages;
}
}
};
const onSuccess = () => {
//
code.value = "";
//
isShow.value = false;
$fetch('/Api/Account/VerifyEmailName', {
method: 'POST',
body: {
email: form.email,
username: form.username
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
const data = res as HttpType
if (data.code === 200) {
$fetch('/Api/Account/SendVerificationCode', {
method: 'POST',
body: {
email: form.email
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
toast.add({
severity: 'success',
summary: '验证码发送成功',
detail: '请查收邮箱',
life: 3000
})
EmailDialog.value = true;
})
} else {
toast.add({
severity: 'error',
summary: data.message,
detail: '错误',
life: 3000
})
}
})
};
const submitCode = () => {
$fetch('/Api/Account/Register', {
method: 'POST',
body: {
username: form.username,
nickname: form.nickname,
email: form.email,
phone: form.phoneNumber,
password: form.password,
code: code.value
},
baseURL: useRuntimeConfig().public.baseUrl,
}).then(res => {
const data = res as HttpType<any>
if (data.code === 200) {
toast.add({
severity: 'success',
summary: '注册成功',
detail: '即将跳转登录页面',
life: 3000
})
setTimeout(() => {
navigateTo('/SignIn')
}, 1000)
}
})
}
</script>
<template>
<div class="SignUp-Box">
<Vcode :show="isShow" @success="onSuccess"/>
<Dialog v-model:visible="EmailDialog" :style="{ width: '25rem' }" header="验证你的账户" modal>
<div class="Email-Box">
<h2>验证您的帐户</h2>
<p>请输入发送到您邮箱的代码</p>
<div>
<InputOtp v-model="code" :length="6" style="gap: 0">
<template #default="{ attrs, events, index }">
<input class="custom-otp-input" type="text" v-bind="attrs" v-on="events"/>
<div v-if="index === 3" style="padding-inline: 5px">
<Minus/>
</div>
</template>
</InputOtp>
</div>
<div class="Email-Box-Action">
<Button label="重新发送" link></Button>
<Button label="提交验证码" @click.prevent="submitCode"></Button>
</div>
</div>
</Dialog>
<div class="SignUp-Box-Header">
<h1>注册</h1>
<h3>创建你的帐户</h3>
</div>
<div class="SignUp-Box-Form">
<form @submit.prevent="handleSubmit">
<div class="From-Item">
<div class="From-Group">
<label for="username">用户名</label>
<InputText id="username" v-model="form.username" aria-describedby="username-help"/>
<small id="username-help" class="p-error">{{ errors.username }}</small>
</div>
<div class="From-Group">
<label for="nickname">昵称</label>
<InputText id="nickname" v-model="form.nickname" aria-describedby="username-help"/>
<small id="username-help" class="p-error">{{ errors.nickname }}</small>
</div>
</div>
<div class="From-Item">
<div class="From-Group">
<label for="email">邮箱</label>
<InputText id="email" v-model="form.email" aria-describedby="username-help"/>
<small id="username-help" class="p-error">{{ errors.email }}</small>
</div>
<div class="From-Group">
<label for="phoneNumber">手机号</label>
<InputText id="phoneNumber" v-model="form.phoneNumber" aria-describedby="username-help"/>
<small id="username-help" class="p-error">{{ errors.phoneNumber }}</small>
</div>
</div>
<div class="From-Item">
<div class="From-Group">
<label for="password">密码</label>
<Password id="password" v-model="form.password" toggleMask>
<template #header>
<h6>检查你的密码</h6>
</template>
<template #footer>
<Divider/>
<p>需求</p>
<ul>
<li>至少一个特殊字符</li>
<li>至少一个字母</li>
<li>至少一个数字</li>
<li>至少6个字符</li>
</ul>
</template>
</Password>
<small id="username-help" class="p-error">{{ errors.password }}</small>
</div>
<div class="From-Group">
<label for="confirmPassword">确认密码</label>
<Password id="confirmPassword" v-model="form.confirmPassword" :feedback="false" aria-describedby="username-help"
toggleMask/>
<small id="username-help" class="p-error">{{ errors.confirmPassword }}</small>
</div>
</div>
<div class="From-Check">
<div>
<Checkbox v-model="form.remember" :binary="true" inputId="remember" name="remember"/>
<label class="ml-2" for="remember"> 我同意使用条款</label>
<small id="username-help" class="p-error">{{ errors.remember }}</small>
</div>
</div>
<div class="From-Action">
<Button label="注册" type="submit"/>
</div>
</form>
</div>
<div class="SignIn-Box-Bottom">
<p>还是使用其他帐户登录</p>
<div>
<NuxtImg height="40" src="/Gmail.svg" width="40"/>
<NuxtImg height="40" src="/Facebook.svg" width="40"/>
<NuxtImg height="40" src="/Instagram.svg" width="40"/>
<NuxtImg height="40" src="/Linkedin.svg" width="40"/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.SignUp-Box {
display: flex;
flex-direction: column;
justify-content: center;
gap: $gap*2;
> * {
transition: all 0.3s ease;
}
}
.SignUp-Box-Header {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
gap: $gap*2;
h3 {
color: $unfocused-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%; /* 28px */
}
}
.SignUp-Box-Form {
display: flex;
align-items: center;
form {
width: 100%;
display: flex;
gap: $gap*2;
flex-direction: column;
}
}
.Email-Box {
display: flex;
flex-direction: column;
gap: $gap*2;
justify-content: center;
align-items: center;
> p {
color: $light-unfocused-color;
font-size: 16px;
margin-top: -10px;
}
.Email-Box-Action {
display: flex;
gap: $gap;
width: 100%;
> button {
width: 100%;
}
}
}
.From-Item {
display: flex;
align-items: center;
gap: $gap;
justify-content: space-between;
}
.From-Group {
display: flex;
flex-direction: column;
gap: $gap;
small {
max-height: 5px;
}
}
.From-Group {
display: flex;
flex-direction: column;
gap: $gap;
width: 240px;
label {
color: $unfocused-color;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 175%; /* 28px */
}
}
.From-Check {
display: flex;
justify-content: center;
color: $unfocused-color;
}
.From-Action {
display: flex;
justify-content: center;
margin-top: $padding*.5;
.p-button {
display: flex;
width: 100%;
padding: $padding*.75;
justify-content: center;
align-items: center;
border-radius: $radius;
background: $primary-color;
}
}
.SignIn-Box-Bottom {
display: flex;
gap: $gap*2;
flex-direction: column;
align-items: center;
justify-content: center;
> p {
color: $light-text-color;
font-size: 16px;
}
> div {
display: flex;
height: 30px;
overflow: hidden;
gap: 24px;
}
}
.custom-otp-input {
width: 48px;
height: 48px;
font-size: 24px;
appearance: none;
text-align: center;
border-radius: 0;
border: 1px solid var(--surface-400);
background: transparent;
outline-offset: -2px;
outline-color: transparent;
border-right: 0 none;
transition: outline-color 0.3s;
color: var(--text-color);
}
.custom-otp-input:focus {
outline: 2px solid var(--primary-color);
}
.custom-otp-input:first-child,
.custom-otp-input:nth-child(5) {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.custom-otp-input:nth-child(3),
.custom-otp-input:last-child {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-right-width: 1px;
border-right-style: solid;
border-color: var(--surface-400);
}
:deep(.p-input-icon) {
top: 30%
}
//primeVue
:deep(.p-inputtext) {
padding: $padding*.5 $padding;
border-radius: $radius;
width: 100%;
border: 1px solid $primary-color;
&:enabled:hover {
border: 1px solid $primary-color;
box-shadow: 0 0 0 2px $primary-color;
}
&:enabled:focus {
outline: 2px solid $primary-color;
}
}
:deep(.p-password-panel) {
padding: $padding;
background: red;
}
.dark-mode {
h1, h2, h3, p {
color: $dark-text-color;
}
}
</style>

35
web/pages/home.vue Executable file
View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import TopGrid from "~/components/Grid/TopGrid.vue";
import MainGrid from "~/components/Grid/MainGrid.vue";
import {useSessionSignalRStore} from "~/strores/HubStore";
import {useDataStore} from "~/strores/DataStore";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
definePageMeta({
layout: 'main',
middleware: ['auth']
})
</script>
<template>
<section class="home-layout">
<TopGrid/>
<MainGrid/>
</section>
</template>
<style lang="scss" scoped>
@import "base";
.home-layout {
width: 100%;
height: 100%;
min-height: 100vh;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
gap: $gap*2;
}
</style>

195
web/pages/host.vue Normal file
View File

@ -0,0 +1,195 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
definePageMeta({
layout: 'main',
middleware: ['auth']
})
type RouteType = {
label: string,
icon: string,
route: string
}
const mainLayoutStore = useMainLayoutStore()
const diskRoute = ref<RouteType[]>([])
const netRoute = ref<RouteType[]>([])
const vgaRoute = ref<RouteType[]>([])
const headers = computed(() => {
{
return [
{
label: 'CPU',
icon: 'Cpu',
route: '/host/cpu'
}, {
label: '内存',
icon: 'Cpu',
route: '/host/memory'
},
...diskRoute.value.map(x => {
return {
label: x.label,
icon: x.icon,
route: x.route
}
}), ...netRoute.value.map(x => {
return {
label: x.label,
icon: x.icon,
route: x.route
}
}),
...vgaRoute.value.map(x => {
return {
label: x.label,
icon: x.icon,
route: x.route
}
})
]
}
})
onMounted(() => {
getRote()
})
watch(() => mainLayoutStore.SelectServer, () => {
getRote()
})
const getRote = () => {
$fetch('/Api/Server/GetServerDiskList', {
method: 'GET',
params: {
ServerId: mainLayoutStore.SelectServer.id
},
baseURL: useRuntimeConfig().public.baseUrl,
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
}
}).then(res => {
const data = res as any[]
console.log(data)
diskRoute.value = data.map((x, index) => {
return {
label: `驱动器 ${index} (${x.name.replace('/dev/', '')}) - ${x.size}GB`,
icon: 'HardDrive',
route: '/host/disk/' + x.name.replace('/dev/', '').replace('/', '_')
}
})
})
$fetch('/Api/Server/GetServerNetworkEquipmentList', {
method: 'GET',
params: {
ServerId: mainLayoutStore.SelectServer.id
},
baseURL: useRuntimeConfig().public.baseUrl,
headers: {
'Authorization': 'Bearer ' + useCookie('token').value
}
}).then(res => {
const data = res as string[]
netRoute.value = data.map((x, index) => {
return {
label: `网络设备 ${index} (${x})`,
icon: 'Network',
route: '/host/network/' + x
}
})
})
$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
}
})
})
}
</script>
<template>
<div class="Host-Layout">
<XScroll>
<div class="header">
<div v-for="header in headers" :class="{
'header-item':true,'header-item-activated': $route.path === header.route,
}" @click="navigateTo(header.route)">
<Icon :name="header.icon" :stroke-width="1.6"/>
<p>{{ header.label }}</p>
</div>
</div>
</XScroll>
<NuxtPage :foobar="123"/>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.Host-Layout {
background: $light-bg-color;
width: 100%;
border-radius: $radius*2;
box-shadow: 0 0 10px rgba(0, 0, 0, .1);
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns:1fr;
.dark-mode & {
background: $dark-bg-color;
}
}
.header {
display: flex;
min-height: 50px;
}
.header-item {
display: flex;
align-items: center;
padding: $padding;
gap: $gap;
//
white-space: nowrap;
&:not(.header-item-activated):hover, &:active {
background: rgba($primary-color, 0.1);
box-shadow: 0 0 200px 10px rgba($primary-color, .3);
}
p, svg {
font-weight: 500;
color: $light-text-color;
}
.dark-mode & {
p, svg {
color: $dark-text-color;
}
}
}
.header-item-activated {
background: $primary-color;
p, svg {
color: #FFF;
stroke: #FFF;
}
}
</style>

445
web/pages/host/cpu.vue Normal file
View File

@ -0,0 +1,445 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import VChart from "vue-echarts";
import dayjs from "dayjs";
import {useDataStore} from "~/strores/DataStore";
const mainLayoutStore = useMainLayoutStore()
type CpuInfo = {
field: string,
data: string
}
const lsCpu = ref<CpuInfo[]>([])
const dataStore = useDataStore()
const colorMode = useColorMode()
const getSystemInfo = () => {
$fetch(`/Api/Server/GetServerCpuInfo`, {
method: 'GET',
responseType: "json",
params: {
serverId: mainLayoutStore.SelectServer.id
},
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${useCookie('token').value}`
},
baseURL: useRuntimeConfig().public.baseUrl
}).then((res) => {
const data = res as any
//json
lsCpu.value = data['lscpu']
console.log(data)
})
}
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>
<template>
<div class="cpu-layout">
<div class="left-chart">
<div class="cpu-title">
<h1>CPU</h1>
<h2>{{ lsCpu.find(x => x.field === 'Model name:')?.data }}</h2>
</div>
<div class="cpu-chart-top">
<p>使用率</p>
<p>100%</p>
</div>
<div class="cpu-chart">
<v-chart
:option="option"
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="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>0</p>
</div>
</div>
<div class="right-info">
<div class="info-title-box">
<div class="info-title">
<p>使用率</p>
<h2>{{ Number(dataStore.data["CpuTotalUsage"]).toFixed(2) }}%</h2>
</div>
<div class="info-title">
<p>速度</p>
<h2>{{ (Number(dataStore.data["CpuTotalSpeed"]) / 1000).toFixed(2) }}GHZ</h2>
</div>
<div class="info-title">
<p>进程</p>
<h2>{{ Number(dataStore.data["ProcessTotalCount"]) }}</h2>
</div>
<div class="info-title">
<p>线程</p>
<h2>{{ Number(dataStore.data["ThreadsTotalCount"]) }}</h2>
</div>
<div class="info-title">
<p>句柄</p>
<h2>{{ Number(dataStore.data["PhrasePatternCount"]) }}</h2>
</div>
</div>
<div class="other-info-box">
<div v-for="info in lsCpu" class="other-info">
<p>{{ info.field }}</p>
<p>{{ info.data }}</p>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.cpu-layout {
padding: $padding*1.5;
display: grid;
grid-template-columns: 1fr 300px;
grid-template-rows: 1fr;
gap: $gap*2;
}
.left-chart {
display: flex;
flex-direction: column;
gap: $gap;
.cpu-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;
}
}
}
.cpu-chart-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.cpu-chart {
flex: 1;
border: 4px solid #0073f4;
border-radius: $radius*2;
height: min-content;
max-height: 32rem;
min-height: 24rem;
overflow: hidden;
}
.cpu-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;
}
}
}
.right-info {
display: flex;
flex-direction: column;
gap: $gap*2;
}
</style>

View File

@ -0,0 +1,360 @@
<script lang="ts" setup>
import VChart from "vue-echarts";
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import {useDataStore} from "~/strores/DataStore";
import dayjs from "dayjs";
type diskInfo = {
name: string,
value: strings
}
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',
params: {
ServerId: mainLayoutStore.SelectServer.id,
DiskId: id.value
},
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer " + useCookie('token').value
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
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>
<div class="disk-layout">
<div class="left-chart">
<div class="disk-title">
<h1>驱动器 {{ $route.params.id }}</h1>
<h2>{{ lsDisk.find(x => x.name === 'Device_Model' || x.name === 'Model_Number')?.value }}</h2>
</div>
<div class="disk-chart-top">
<p>活动时间</p>
<p>100%</p>
</div>
<div class="disk-chart">
<v-chart
:option="option"
autoresize
class="chart"/>
</div>
<div class="disk-chat-bottom">
<p>1分钟</p>
<p>0</p>
</div>
<div class="disk-chart-top">
<p>吞吐量</p>
<p></p>
</div>
<div class="disk-chart">
<v-chart
:option="RWOption"
autoresize
class="chart"/>
</div>
<div class="disk-chat-bottom">
<p>1分钟</p>
<p>0</p>
</div>
</div>
<div class="right-info">
<div class="info-title-box">
<div class="info-title">
<p>使用率</p>
<h2>{{ dataStore.data[`diskUtil-${id}`] }}%</h2>
</div>
<div class="info-title">
<p>平均响应时间</p>
<h2>{{ dataStore.data[`diskAwait-${id}`] }} ms</h2>
</div>
<div class="infos">
<div class="info2">
<div></div>
<p>读取速度</p>
<h2>{{ dataStore.data[`diskReadKB-${id}`] }} KiB/s</h2>
</div>
<div class="info2">
<div></div>
<p>写入速度</p>
<h2>{{ dataStore.data[`diskWriteKB-${id}`] }} KiB/s</h2>
</div>
</div>
</div>
<div class="other-info-box">
<div v-for="value in lsDisk" class="other-info">
<p>{{ value.name }}</p>
<p>{{ value.value }}</p>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.disk-layout {
padding: $padding*1.5;
display: grid;
grid-template-columns: 1fr 300px;
grid-template-rows: 1fr;
gap: $gap*2;
}
.left-chart {
display: flex;
flex-direction: column;
gap: $gap;
.disk-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;
}
}
}
.disk-chart-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.disk-chart {
flex: 1;
border: 4px solid #269964;
border-radius: $radius*2;
height: min-content;
max-height: 32rem;
min-height: 24rem;
overflow: hidden;
}
.disk-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 #269964;
grid-row: 1/3;
}
}
.info2:last-of-type {
& > div {
height: 100%;
border: 3px dashed #269964;
grid-row: 1/3;
}
}
}
</style>

View File

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

491
web/pages/host/memory.vue Normal file
View File

@ -0,0 +1,491 @@
<script lang="ts" setup>
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
import VChart from "vue-echarts";
import dayjs from "dayjs";
import {useDataStore} from "~/strores/DataStore";
const mainLayoutStore = useMainLayoutStore()
type MemoryInfo = {
[key: string]: any
}
const lsMemory = ref<MemoryInfo[]>([])
const dataStore = useDataStore()
const colorMode = useColorMode()
const getSystemInfo = () => {
$fetch(`/Api/Server/GetServerMemoryInfo`, {
method: 'GET',
responseType: "json",
params: {
serverId: mainLayoutStore.SelectServer.id
},
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${useCookie('token').value}`
},
baseURL: useRuntimeConfig().public.baseUrl
}).then((res) => {
const data = res as any
//json
lsMemory.value = data
console.log(data)
})
}
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>
<template>
<div class="memory-layout">
<div class="left-chart">
<div class="memory-title">
<h1>CPU</h1>
<h2>{{ (Number(dataStore.data["MemoryTotal"]) / 1024 / 1024).toFixed(2) }} GB</h2>
</div>
<div class="memory-chart-top">
<p>内存使用率</p>
<p>100%</p>
</div>
<div class="memory-chart">
<v-chart
:option="option"
autoresize
class="chart"/>
</div>
<div class="memory-chat-bottom">
<p>1分钟</p>
<p>0</p>
</div>
<div class="memory-chart-top">
<p>内存组成</p>
</div>
<div class="memory-composition-chart">
<div :style="{
width:`${dataStore.data['MemoryTotalUsage']}%`
}" class="composition"></div>
<div :style="{
width:`${dataStore.data['SwapTotalUsage']}%`
}" 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">
<div class="info-title">
<p>使用中</p>
<h2>{{ (Number(dataStore.data["MemoryUsed"]) / 1024 / 1024).toFixed(2) }} GB</h2>
</div>
<div class="info-title">
<p>可用</p>
<h2>{{ (Number(dataStore.data["MemoryFree"]) / 1024 / 1024).toFixed(2) }} GB</h2>
</div>
<div class="info-title">
<p>已提交</p>
<h2>{{
((Number(dataStore.data["MemoryTotal"]) - Number(dataStore.data["MemoryCache"])) / 1024 / 1024).toFixed(2)
}} GB</h2>
</div>
<div class="info-title">
<p>已缓存</p>
<h2>{{ (Number(dataStore.data["MemoryCache"]) / 1024 / 1024).toFixed(2) }} GB</h2>
</div>
<div class="info-title">
<p>已使用交换空间</p>
<h2>{{ (Number(dataStore.data["SwapUsed"]) / 1024).toFixed(2) }} KiB</h2>
</div>
<div class="info-title">
<p>可用交换空间</p>
<h2>{{ (Number(dataStore.data["SwapFree"]) / 1024 / 1024).toFixed(2) }} GB</h2>
</div>
</div>
<div class="other-info-box">
<div v-for="info in lsMemory" class="other-info">
<p>{{ info.description !== '[empty]' ? info.description : info.id }}</p>
<p>{{ info.size ?? info.handle }}</p>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "base";
.memory-layout {
padding: $padding*1.5;
display: grid;
grid-template-columns: 1fr 300px;
grid-template-rows: 1fr;
gap: $gap*2;
}
.left-chart {
display: flex;
flex-direction: column;
gap: $gap;
.memory-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;
}
}
}
.memory-chart-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.memory-chart {
flex: 1;
border: 4px solid #5c9be8;
border-radius: $radius*2;
height: min-content;
max-height: 32rem;
min-height: 24rem;
overflow: hidden;
}
.memory-composition-chart {
height: 80px;
border: 4px solid #5c9be8;
border-radius: $radius*2;
display: flex;
overflow: hidden;
.composition {
border-right: 2px solid #5c9be8;
transition: width 0.3s ease-in-out;
}
.composition:first-of-type {
background: rgba(92, 155, 232, 0.75);
width: 20px;
}
.composition:nth-of-type(2) {
background: rgba(92, 155, 232, 0.3);
width: 20px;
}
.composition:last-of-type {
flex: 1;
border-right: unset;
}
}
.memory-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;
}
}
}
.right-info {
display: flex;
flex-direction: column;
gap: $gap*2;
}
</style>

View File

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

13
web/pages/index.vue Executable file
View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
definePageMeta({
middleware: ['auth']
})
</script>
<template>
</template>
<style lang="scss" scoped>
</style>

59
web/pages/user.vue Normal file
View File

@ -0,0 +1,59 @@
<script lang="ts" setup>
import UserItem from "~/components/UserPage/UserItem.vue";
import type {UserInfoListType} from "~/types/UserType";
definePageMeta({
layout: 'main',
middleware: ['auth']
})
const userList = ref<UserInfoListType | null>(null)
onMounted(() => {
$fetch("/Api/User/GetUserInfoList", {
method: "GET",
headers: {
"Authorization": "Bearer " + useCookie("token").value
},
baseURL: useRuntimeConfig().public.baseUrl
}).then(res => {
userList.value = res as UserInfoListType
})
})
</script>
<template>
<div class="user-layout">
<UserPageBar/>
<UserPageIndicator/>
<div class="user-content">
<UserItem v-for="user in userList" :avatar="user.avatar" :create-time="user.createDate"
:email="user.email" :full-name="user.nickName" :isLock="user.isLock"
:last-login-time="user.lastLoginTime"
:login-ip="user.address??':: 0'"
:modify-time="user.modifiedDate"
:name="user.userName"
:phone="user.phoneNumber"
:physical-address="user.physicalAddress??'未设置'"
:position="user.posts"
:user-id="user.id"/>
</div>
</div>
</template>c
<style lang="scss" scoped>
@import "base";
.user-layout {
width: 100%;
height: 100%;
min-height: 1200px;
display: flex;
flex-direction: column;
gap: $gap*2;
}
.user-content {
display: flex;
flex-direction: column;
gap: $gap*2;
}
</style>

View File

@ -0,0 +1,12 @@
diff --git a/node_modules/grid-layout-plus/src/components/grid-layout.vue b/node_modules/grid-layout-plus/src/components/grid-layout.vue
index e5174ea..50d371c 100644
--- a/node_modules/grid-layout-plus/src/components/grid-layout.vue
+++ b/node_modules/grid-layout-plus/src/components/grid-layout.vue
@@ -365,7 +365,6 @@ function onWindowResize() {
if (wrapper.value) {
state.width = wrapper.value.offsetWidth
}
-
emitter.emit('resizeEvent')
}

5
web/plugins/apexcharts.ts Executable file
View File

@ -0,0 +1,5 @@
import VueApexCharts from "vue3-apexcharts";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueApexCharts);
});

Some files were not shown because too many files have changed in this diff Show More