init
This commit is contained in:
commit
dfbc1cfdb0
|
@ -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/
|
|
@ -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
|
|
@ -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"><AssemblyExplorer>
|
||||
<Assembly Path="C:\Users\niyyz\.nuget\packages\czgl.systeminfo\2.2.0\lib\net7.0\CZGL.SystemInfo.dll" />
|
||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
|
@ -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());
|
|
@ -0,0 +1,2 @@
|
|||
[CpuTotalUsage]
|
||||
Value=80
|
|
@ -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 * * * * ? *
|
|
@ -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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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];
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
@LoongPanel_Asp_HostAddress = http://localhost:5253
|
||||
|
||||
GET {{LoongPanel_Asp_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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();
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;"
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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.
|
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -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;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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#cpuUserUsage的key的value
|
||||
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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
13123
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"))
|
||||
}
|
||||
]
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
//创建UserId和UserName的对照表
|
||||
export const userIdToUserName: { [key: string]: string } = {
|
||||
User: "用户",
|
||||
Admin: "管理员",
|
||||
Manager: "经理",
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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.path是form对象的有效键
|
||||
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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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(() => {
|
||||
//data为minutesTemp的均值
|
||||
let data: number = minutesTemp.reduce((a, b) => a + b, 0) / minutesTemp.length
|
||||
data = Number((data).toFixed(2))
|
||||
hoursValue.value.push(data)
|
||||
values.value.shift()
|
||||
}, 60000)
|
||||
IntervalRefresh = setInterval(() => {
|
||||
getHistoryData()
|
||||
}, 600000)
|
||||
})
|
||||
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer.id, () => {
|
||||
values.value = [...Array.from({length: 61}, (_, index) => null)]
|
||||
getHistoryData()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Interval)
|
||||
clearInterval(IntervalHours)
|
||||
})
|
||||
const getHistoryData = () => {
|
||||
values.value = dataStore.dataHistory.data["CpuTotalUsage"].slice(-60).map(x => Number(x))
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'CpuTotalUsage',
|
||||
timeRange: '1h'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
hoursValue.value = res.data
|
||||
})
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'CpuTotalUsage',
|
||||
timeRange: '1d'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
dayValue.value = res.data
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
getHistoryData()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<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>
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>{{ $route.params.id }}</p>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -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(() => {
|
||||
//data为minutesTemp的均值
|
||||
let data: number = minutesTemp.reduce((a, b) => a + b, 0) / minutesTemp.length
|
||||
data = Number((data).toFixed(2))
|
||||
hoursValue.value.push(data)
|
||||
values.value.shift()
|
||||
}, 60000)
|
||||
IntervalRefresh = setInterval(() => {
|
||||
getHistoryData()
|
||||
}, 600000)
|
||||
})
|
||||
|
||||
})
|
||||
watch(() => mainLayoutStore.SelectServer.id, () => {
|
||||
values.value = [...Array.from({length: 61}, (_, index) => null)]
|
||||
getHistoryData()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(Interval)
|
||||
clearInterval(IntervalHours)
|
||||
clearInterval(IntervalRefresh)
|
||||
})
|
||||
const getHistoryData = () => {
|
||||
values.value = dataStore.dataHistory.data["MemoryTotalUsage"].slice(-60).map(x => Number(x))
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'MemoryTotalUsage',
|
||||
timeRange: '1h'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
hoursValue.value = res.data
|
||||
})
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
$fetch('/Api/Server/GetServerHistoryStep', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
ServerId: mainLayoutStore.SelectServer.id,
|
||||
DataType: 'MemoryTotalUsage',
|
||||
timeRange: '1d'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${useCookie('token').value}`
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then((res: any) => {
|
||||
dayValue.value = res.data
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
getHistoryData()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<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>
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>{{ $route.params.id }}</p>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -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>
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue