尝试修改数据图表更新方式
This commit is contained in:
parent
16de25c2ca
commit
30ffdd1782
|
@ -1,10 +1,12 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Dynamic;
|
||||||
using System.Dynamic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using LoongPanel_Asp.Helpers;
|
using LoongPanel_Asp.Helpers;
|
||||||
|
using LoongPanel_Asp.Hubs;
|
||||||
using LoongPanel_Asp.Servers;
|
using LoongPanel_Asp.Servers;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
@ -12,7 +14,10 @@ namespace LoongPanel_Asp.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("Api/[controller]")]
|
[Route("Api/[controller]")]
|
||||||
public class ServerController(IServiceProvider serviceProvider, ApplicationDbContext dbContext) : ControllerBase
|
public class ServerController(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
|
IHubContext<SessionHub> hubContext) : ControllerBase
|
||||||
{
|
{
|
||||||
//获取远程服务器地址列表
|
//获取远程服务器地址列表
|
||||||
[HttpGet("GetServerList")]
|
[HttpGet("GetServerList")]
|
||||||
|
@ -46,64 +51,52 @@ public class ServerController(IServiceProvider serviceProvider, ApplicationDbCon
|
||||||
public async Task<IActionResult> GetServerHistoryDate([FromQuery] string serverId,
|
public async Task<IActionResult> GetServerHistoryDate([FromQuery] string serverId,
|
||||||
[FromQuery] List<string?>? dataTypes = null, [FromQuery] int? startIndex = 0)
|
[FromQuery] List<string?>? dataTypes = null, [FromQuery] int? startIndex = 0)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(serverId)) return BadRequest("ServerId is required.");
|
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
|
||||||
|
//创建一个缓存
|
||||||
|
Dictionary<string, ServerMonitoringData?> temp = new();
|
||||||
|
//创建一个时间缓存
|
||||||
|
//创建查询 流式
|
||||||
var query = dbContext.ServerMonitoringData
|
var query = dbContext.ServerMonitoringData
|
||||||
.Where(s => s.ServerId == serverId && s.DataType != null);
|
.AsNoTracking() // 不追踪实体状态
|
||||||
|
.Where(x => x.ServerId == serverId) // 筛选服务器ID
|
||||||
if (dataTypes != null && dataTypes.Any()) query = query.Where(s => dataTypes.Contains(s.DataType));
|
.Where(x => dataTypes == null || dataTypes.Contains(x.DataType)) // 如果dataTypes不为空,则进一步筛选
|
||||||
|
.Skip(Math.Max(startIndex ?? 0, 0)) // 跳过指定数量的记录
|
||||||
var allData = await query
|
|
||||||
.OrderByDescending(s => s.Time)
|
|
||||||
.Skip(startIndex ?? 0)
|
|
||||||
.Take(1000)
|
.Take(1000)
|
||||||
.ToListAsync();
|
.AsAsyncEnumerable(); // 启用流式查询
|
||||||
|
await foreach (var data in query)
|
||||||
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>();
|
if (!temp.TryGetValue(data.DataType, out var tempData)) tempData = data;
|
||||||
var temp = double.Parse(dataList[0].Data ?? string.Empty);
|
temp[data.DataType] = data;
|
||||||
foreach (var time in timeList)
|
var time = tempData?.Time;
|
||||||
|
if (time == null || data.Time == null) continue;
|
||||||
|
var timeDifferenceInSeconds = (data.Time - time)?.TotalSeconds;
|
||||||
|
if (timeDifferenceInSeconds <= 0) continue;
|
||||||
|
var steps = (int)timeDifferenceInSeconds!;
|
||||||
|
var value = double.Parse(data.Data ?? "0");
|
||||||
|
var lastValue = double.Parse(tempData?.Data ?? "0");
|
||||||
|
//计算中间值 贝塞尔曲线
|
||||||
|
var controlPoint = (value + lastValue) / 2;
|
||||||
|
List<List<object>> values = [];
|
||||||
|
for (var i = 0; i < steps; i++)
|
||||||
{
|
{
|
||||||
var currentData = dataList.Where(d => d.Time == time).ToList();
|
var t = (double)i / (steps - 1);
|
||||||
if (currentData.Count > 0)
|
var bezierValue = (1 - t) * (1 - t) * value + 2 * (1 - t) * t * controlPoint + t * t * lastValue;
|
||||||
{
|
var bezierTime = time.Value.AddSeconds(i);
|
||||||
//计算data平均值
|
var utcTime = bezierTime.ToUniversalTime();
|
||||||
var dataSum = currentData.Sum(d => double.Parse(d.Data ?? string.Empty));
|
var unixTimestamp = ((DateTimeOffset)utcTime).ToUnixTimeMilliseconds();
|
||||||
temp = dataSum;
|
List<object> innerList =
|
||||||
|
[
|
||||||
|
unixTimestamp,
|
||||||
|
((int)bezierValue).ToString(CultureInfo.InvariantCulture)
|
||||||
|
];
|
||||||
|
values.Add(innerList);
|
||||||
}
|
}
|
||||||
|
|
||||||
backDataList.Add(temp);
|
await hubContext.Clients.Groups(userId).SendAsync("ReceiveDataHistory", data.DataType, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataType != null) groupedData[dataType] = backDataList;
|
return Ok();
|
||||||
}
|
|
||||||
|
|
||||||
//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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -116,54 +109,6 @@ public class ServerController(IServiceProvider serviceProvider, ApplicationDbCon
|
||||||
return Ok(output);
|
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")]
|
[HttpGet("GetServerMemoryInfo")]
|
||||||
public async Task<IActionResult> GetServerMemoryInfo([FromQuery] string serverId)
|
public async Task<IActionResult> GetServerMemoryInfo([FromQuery] string serverId)
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
|
||||||
<PackageReference Include="MimeKit" Version="4.6.0"/>
|
<PackageReference Include="MimeKit" Version="4.6.0"/>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||||
|
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.11" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
|
||||||
<PackageReference Include="Quartz" Version="3.9.0"/>
|
<PackageReference Include="Quartz" Version="3.9.0"/>
|
||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.9.0"/>
|
<PackageReference Include="Quartz.AspNetCore" Version="3.9.0"/>
|
||||||
|
|
|
@ -6,6 +6,7 @@ using LoongPanel_Asp.Middlewares;
|
||||||
using LoongPanel_Asp.Servers;
|
using LoongPanel_Asp.Servers;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NLog.Web;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
using Quartz.AspNetCore;
|
using Quartz.AspNetCore;
|
||||||
|
|
||||||
|
@ -107,6 +108,8 @@ builder.Services.AddSingleton<DataService>();
|
||||||
|
|
||||||
builder.Services.AddHostedService<Init>();
|
builder.Services.AddHostedService<Init>();
|
||||||
|
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Host.UseNLog();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
using System.Globalization;
|
||||||
using LoongPanel_Asp.Hubs;
|
using LoongPanel_Asp.Hubs;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace LoongPanel_Asp.Servers;
|
namespace LoongPanel_Asp.Servers;
|
||||||
|
|
||||||
public class DataService(IServiceProvider serviceProvider, IHubContext<SessionHub> context) : IDisposable
|
public class DataService(IServiceProvider serviceProvider, IHubContext<SessionHub> context,ILogger<DataService> logger) : IDisposable
|
||||||
{
|
{
|
||||||
//创建一个存储
|
//创建一个存储
|
||||||
private static readonly Dictionary<string, ServerMonitoringData> ServerMonitoringData = new();
|
private static readonly Dictionary<string, ServerMonitoringData> ServerMonitoringData = new();
|
||||||
|
@ -16,16 +17,24 @@ public class DataService(IServiceProvider serviceProvider, IHubContext<SessionHu
|
||||||
public async Task Save(ServerMonitoringData data)
|
public async Task Save(ServerMonitoringData data)
|
||||||
{
|
{
|
||||||
data.Time = DateTime.UtcNow;
|
data.Time = DateTime.UtcNow;
|
||||||
|
//取两位小数
|
||||||
|
data.Data = Math.Round(double.Parse(data.Data!), 2).ToString(CultureInfo.InvariantCulture);
|
||||||
ServerMonitoringData[$"{data.DataType}-{data.ServerId}"] = data;
|
ServerMonitoringData[$"{data.DataType}-{data.ServerId}"] = data;
|
||||||
|
logger.LogInformation($"发送数据{data.DataType}:{data.Data}成功");
|
||||||
await context.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
await context.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Save(List<ServerMonitoringData> datas)
|
public async Task Save(List<ServerMonitoringData> datas)
|
||||||
{
|
{
|
||||||
|
datas.ForEach(x =>
|
||||||
|
{
|
||||||
|
x.Time = DateTime.UtcNow;
|
||||||
|
x.Data = Math.Round(double.Parse(x.Data!), 2).ToString(CultureInfo.InvariantCulture);
|
||||||
|
});
|
||||||
foreach (var data in datas)
|
foreach (var data in datas)
|
||||||
{
|
{
|
||||||
data.Time = DateTime.UtcNow;
|
|
||||||
ServerMonitoringData[$"{data.DataType}-{data.ServerId}"] = data;
|
ServerMonitoringData[$"{data.DataType}-{data.ServerId}"] = data;
|
||||||
|
logger.LogInformation($"发送数据{data.DataType}:{data.Data}成功");
|
||||||
await context.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
await context.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,12 +42,28 @@ public class DataService(IServiceProvider serviceProvider, IHubContext<SessionHu
|
||||||
//提交
|
//提交
|
||||||
public async Task Submit()
|
public async Task Submit()
|
||||||
{
|
{
|
||||||
var dbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
|
try
|
||||||
|
{
|
||||||
|
using (var scope = serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
var dataDb = dbContext.ServerMonitoringData;
|
var dataDb = dbContext.ServerMonitoringData;
|
||||||
await dataDb.AddRangeAsync(ServerMonitoringData.Values);
|
await dataDb.AddRangeAsync(ServerMonitoringData.Values);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
logger.LogInformation($"监控数据已保存,共保存 {ServerMonitoringData.Count} 条记录。");
|
||||||
|
Console.WriteLine($"监控数据已保存,共保存 {ServerMonitoringData.Count} 条记录。");
|
||||||
|
}
|
||||||
ServerMonitoringData.Clear();
|
ServerMonitoringData.Clear();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 记录异常信息
|
||||||
|
logger.LogError(ex, "保存监控数据时发生错误。");
|
||||||
|
// 根据需要重新抛出异常或进行其他错误处理
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CheckData()
|
private async Task CheckData()
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,6 +9,8 @@ public class SshService : IDisposable
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly ApplicationDbContext _db;
|
private readonly ApplicationDbContext _db;
|
||||||
private readonly ILogger<SshService> _logger;
|
private readonly ILogger<SshService> _logger;
|
||||||
|
//注册nlog
|
||||||
|
|
||||||
private readonly Dictionary<string, ConnectionInfo> _serverConnectionInfos = new();
|
private readonly Dictionary<string, ConnectionInfo> _serverConnectionInfos = new();
|
||||||
private readonly Dictionary<string, SshClient> _serverSshClients = new();
|
private readonly Dictionary<string, SshClient> _serverSshClients = new();
|
||||||
|
|
||||||
|
@ -44,30 +46,39 @@ public class SshService : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ExecuteCommandAsync(string serverId, string command, params string[] arguments)
|
public async Task<string?> ExecuteCommandAsync(string serverId, string command, params string[] arguments)
|
||||||
{
|
{
|
||||||
var sshClient = _serverSshClients[serverId];
|
if (!_serverSshClients.TryGetValue(serverId, out var sshClient))
|
||||||
if (sshClient == null) throw new InvalidOperationException($"SSH client for server ID '{serverId}' not found.");
|
{
|
||||||
|
var errorMessage = $"服务器ID '{serverId}' 的SSH客户端未找到。";
|
||||||
|
_logger.LogError(errorMessage);
|
||||||
|
throw new InvalidOperationException(errorMessage);
|
||||||
|
}
|
||||||
var output = "";
|
var output = "";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 确保在执行命令前连接到服务器
|
// 确保在执行命令前连接到服务器
|
||||||
if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token);
|
if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token);
|
||||||
var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments));
|
var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments));
|
||||||
Console.WriteLine(commandString);
|
_logger.LogInformation($"服务器ID:{serverId},正在执行命令:{commandString}");
|
||||||
using var commandResult = sshClient.RunCommand(commandString);
|
using var commandResult = sshClient.RunCommand(commandString);
|
||||||
output = commandResult.Result;
|
output = commandResult.Result;
|
||||||
if (commandResult.ExitStatus != 0) output = commandResult.Error;
|
if (commandResult.ExitStatus != 0)
|
||||||
|
{
|
||||||
|
_logger.LogError($"命令 {commandString} 执行失败,退出状态码 {commandResult.ExitStatus}:\n {output}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_logger.LogInformation($"命令 {commandString} 执行成功:{output}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine(ex);
|
_logger.LogError(ex, "执行命令时发生错误。");
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<string> ExecuteCommandAsync(bool langC, string serverId, string command,
|
public async Task<string> ExecuteCommandAsync(bool langC, string serverId, string command,
|
||||||
params string[] arguments)
|
params string[] arguments)
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
autoReload="true"
|
||||||
|
internalLogLevel="Info"
|
||||||
|
internalLogFile="c:\temp\internal-nlog-AspNetCore.txt">
|
||||||
|
|
||||||
|
<!-- 启用 ASP.NET Core 布局渲染器 -->
|
||||||
|
<extensions>
|
||||||
|
<add assembly="NLog.Web.AspNetCore"/>
|
||||||
|
</extensions>
|
||||||
|
|
||||||
|
<!-- 日志写入的目标 -->
|
||||||
|
<targets>
|
||||||
|
<!-- 所有日志消息的基础详细信息文件目标 -->
|
||||||
|
<target xsi:type="File" name="allfile" fileName="logs/all-${shortdate}.log"
|
||||||
|
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message}${exception:format=tostring}" />
|
||||||
|
|
||||||
|
<!-- 自定义日志消息的额外Web详细信息文件目标 -->
|
||||||
|
<target xsi:type="File" name="ownFile-web" fileName="logs/own-${shortdate}.log"
|
||||||
|
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message}${exception:format=tostring}|url: ${aspnet-request-url}|action:${aspnet-mvc-action}" />
|
||||||
|
|
||||||
|
<!-- 控制台目标,用于提高 Docker / Visual Studio 启动检测的速度 -->
|
||||||
|
<target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
|
||||||
|
|
||||||
|
<!-- LoongPanel_Asp.Servers.SshService 日志的单独文件目标 -->
|
||||||
|
<target xsi:type="File" name="sshServiceFile" fileName="logs/ssh-${shortdate}.log"
|
||||||
|
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message}${exception:format=tostring}" />
|
||||||
|
</targets>
|
||||||
|
|
||||||
|
<!-- 将日志记录器名称映射到目标的规则 -->
|
||||||
|
<rules>
|
||||||
|
<!-- 所有日志,包括来自 Microsoft 的 -->
|
||||||
|
<logger name="*" minlevel="Trace" writeTo="allfile" />
|
||||||
|
|
||||||
|
<!-- 将宿主生命周期消息输出到控制台目标,以便更快地启动检测 -->
|
||||||
|
<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />
|
||||||
|
|
||||||
|
<!-- 忽略非关键的 Microsoft 日志,因此只记录自己的日志 -->
|
||||||
|
<logger name="Microsoft.*" maxlevel="Info" final="true" />
|
||||||
|
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
|
||||||
|
|
||||||
|
<!-- 将 LoongPanel_Asp.Servers.SshService 的日志消息写入单独的文件 -->
|
||||||
|
<logger name="LoongPanel_Asp.Servers.SshService" minlevel="Trace" writeTo="sshServiceFile" final="true" />
|
||||||
|
|
||||||
|
<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
|
||||||
|
</rules>
|
||||||
|
</nlog>
|
|
@ -3,7 +3,6 @@ import {charts} from "~/config/charts";
|
||||||
import type {PropType} from "vue";
|
import type {PropType} from "vue";
|
||||||
import type {serverValueItem} from "~/components/SettingCard.vue";
|
import type {serverValueItem} from "~/components/SettingCard.vue";
|
||||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||||
import {THEME_KEY} from "vue-echarts";
|
|
||||||
|
|
||||||
|
|
||||||
const mainLayoutStore = useMainLayoutStore()
|
const mainLayoutStore = useMainLayoutStore()
|
||||||
|
@ -95,17 +94,6 @@ const items = [
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :id="`card_${id}`" ref="cardRef" class="card-layout ">
|
<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 :id="'item' + id" class="card-title vue-draggable-handle">
|
||||||
<div></div>
|
<div></div>
|
||||||
<h3>{{ title ?? "默认标题" }}</h3>
|
<h3>{{ title ?? "默认标题" }}</h3>
|
||||||
|
@ -130,7 +118,7 @@ const items = [
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $gap*1.5;
|
gap: $gap;
|
||||||
will-change: scroll-position, contents;
|
will-change: scroll-position, contents;
|
||||||
border: $border;
|
border: $border;
|
||||||
.dark-mode & {
|
.dark-mode & {
|
||||||
|
@ -138,40 +126,15 @@ const items = [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.card-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $gap;
|
gap: $gap;
|
||||||
color: $light-text-color;
|
color: $light-text-color;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
&,
|
&,
|
||||||
h3:hover {
|
h3:hover {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
|
|
|
@ -7,10 +7,6 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: "CPU使用率",
|
default: "CPU使用率",
|
||||||
},
|
},
|
||||||
info:{
|
|
||||||
type:Array,
|
|
||||||
default:()=>[],
|
|
||||||
},
|
|
||||||
unit: {
|
unit: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "%"
|
default: "%"
|
||||||
|
@ -50,7 +46,7 @@ dataStore.$subscribe((_, state) => {
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="mini-card-box">
|
<div class="mini-card-box">
|
||||||
<n-progress type="circle" processing :status="status" :percentage="
|
<n-progress type="circle" processing :status="status" :percentage="
|
||||||
values[0]
|
Number(values[0])
|
||||||
" style="width: 68px;">
|
" style="width: 68px;">
|
||||||
<MoveDown class="arrow" :style="{transform:`rotate(${(100-value)/100*360*-1}deg)`}" v-if="status==='success'"/>
|
<MoveDown class="arrow" :style="{transform:`rotate(${(100-value)/100*360*-1}deg)`}" v-if="status==='success'"/>
|
||||||
<div class="warning" v-if="status==='warning'">
|
<div class="warning" v-if="status==='warning'">
|
||||||
|
|
|
@ -7,14 +7,15 @@ import {ref} from 'vue';
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import VChart from 'vue-echarts';
|
import VChart from 'vue-echarts';
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import {useSessionSignalRStore} from "~/strores/HubStore";
|
||||||
|
|
||||||
const dataStore = useDataStore()
|
const dataStore = useDataStore()
|
||||||
|
const hubStore=useSessionSignalRStore()
|
||||||
const mainLayoutStore = useMainLayoutStore()
|
const mainLayoutStore = useMainLayoutStore()
|
||||||
type ArbitraryKeyValuePairs = {
|
type ArbitraryKeyValuePairs = {
|
||||||
[key: string]: (number | string)[];
|
[key: string]: (number | string)[];
|
||||||
};
|
};
|
||||||
const values = ref<ArbitraryKeyValuePairs>({});
|
const chartRef = ref<any>(null)
|
||||||
const chartRet = ref<any>(null)
|
|
||||||
const isLoading = ref<boolean>(true)
|
const isLoading = ref<boolean>(true)
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
valueIds: {
|
valueIds: {
|
||||||
|
@ -33,6 +34,8 @@ const props = defineProps({
|
||||||
const option = computed(() => {
|
const option = computed(() => {
|
||||||
return {
|
return {
|
||||||
backgroundColor:'',
|
backgroundColor:'',
|
||||||
|
//关闭动画
|
||||||
|
// animation: false,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
|
@ -50,10 +53,13 @@ const option = computed(() => {
|
||||||
},
|
},
|
||||||
xAxis: [
|
xAxis: [
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'time',
|
||||||
boundaryGap: false,
|
// boundaryGap: false
|
||||||
data: []
|
axisLabel:{
|
||||||
|
showMinLabel:true,
|
||||||
|
showMaxLabel:true,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
],
|
],
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
|
@ -72,18 +78,15 @@ const option = computed(() => {
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
type: 'inside',
|
type: 'inside',
|
||||||
start: 90,
|
start: 99.5,
|
||||||
end: 100,
|
end: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'slider',
|
type: 'slider',
|
||||||
throttle: 500,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'slider',
|
type: 'slider',
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
startValue: 0,
|
|
||||||
endValue: 100,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
legend: {
|
legend: {
|
||||||
|
@ -96,9 +99,20 @@ const option = computed(() => {
|
||||||
areaStyle: {},
|
areaStyle: {},
|
||||||
large: true,
|
large: true,
|
||||||
smooth: true,
|
smooth: true,
|
||||||
|
largeThreshold: 10000,
|
||||||
|
sampling:'lttb',
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: 'series'
|
focus: 'series'
|
||||||
},
|
},
|
||||||
|
markPoint: {
|
||||||
|
data: [
|
||||||
|
{ type: 'max', name: 'Max' },
|
||||||
|
{ type: 'min', name: 'Min' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
markLine: {
|
||||||
|
data: [{ type: 'average', name: 'Avg' }]
|
||||||
|
},
|
||||||
data: [],
|
data: [],
|
||||||
}
|
}
|
||||||
}) ?? []
|
}) ?? []
|
||||||
|
@ -109,58 +123,18 @@ onUnmounted(() => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
})
|
})
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let history = dataStore.dataHistory
|
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
|
hubStore.connection?.on("ReceiveDataHistory",(type:string,values:string[][])=>{
|
||||||
|
isLoading.value = true;
|
||||||
|
chartRef.value.appendData({
|
||||||
|
seriesIndex: props.valueIds?.indexOf(type),
|
||||||
|
data:values
|
||||||
|
})
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}, 5000)
|
|
||||||
nextTick(() => {
|
|
||||||
props.valueIds?.forEach((id, index) => {
|
|
||||||
chartRet.value.appendData({
|
|
||||||
seriesIndex: index,
|
|
||||||
data: history.data[id]
|
|
||||||
})
|
})
|
||||||
})
|
},1000)
|
||||||
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(()=>{
|
setTimeout(()=>{
|
||||||
isLoading.value = false
|
|
||||||
}, 5000)
|
|
||||||
$fetch('/Api/Server/GetServerHistoryDate', {
|
$fetch('/Api/Server/GetServerHistoryDate', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -169,41 +143,120 @@ const zoom = _.throttle((e: any) => {
|
||||||
params: {
|
params: {
|
||||||
ServerId: mainLayoutStore.SelectServer.value,
|
ServerId: mainLayoutStore.SelectServer.value,
|
||||||
DataTypes: props.valueIds,
|
DataTypes: props.valueIds,
|
||||||
StartIndex: endIndex,
|
StartIndex: 0,
|
||||||
},
|
},
|
||||||
baseURL: useRuntimeConfig().public.baseUrl,
|
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(()=>{
|
setTimeout(()=>{
|
||||||
isLoading.value = false
|
interval = setInterval(() => {
|
||||||
}, 1000)
|
const data = dataStore.data
|
||||||
|
const time=Date.now()
|
||||||
|
props.valueIds?.forEach((id, index) => {
|
||||||
|
const newData = data[id] ?? 0
|
||||||
|
chartRef.value.appendData({
|
||||||
|
seriesIndex: index,
|
||||||
|
data: [[time,newData]]
|
||||||
|
})
|
||||||
|
const currentOption = chartRef.value.getOption();
|
||||||
|
chartRef.value.setOption(currentOption)
|
||||||
|
})},1000)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
},1000)
|
},1000)
|
||||||
|
// 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.value,
|
||||||
|
// 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-chart ref="chartRet" :loading="isLoading" :manual-update="true" :option="option"
|
<v-chart ref="chartRef" :loading="isLoading" :manual-update="true" :option="option"
|
||||||
autoresize
|
autoresize
|
||||||
:theme="$colorMode.value"
|
:theme="$colorMode.value"
|
||||||
class="chart"
|
class="chart"
|
||||||
@datazoom="zoom"/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -60,15 +60,6 @@ const option = computed(() => {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
toolbox: {
|
|
||||||
feature: {
|
|
||||||
dataZoom: {
|
|
||||||
yAxisIndex: 'none'
|
|
||||||
},
|
|
||||||
|
|
||||||
saveAsImage: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataZoom: [
|
dataZoom: [
|
||||||
{
|
{
|
||||||
type: 'inside',
|
type: 'inside',
|
||||||
|
@ -88,6 +79,8 @@ const option = computed(() => {
|
||||||
],
|
],
|
||||||
legend: {
|
legend: {
|
||||||
data: props.valueNames,
|
data: props.valueNames,
|
||||||
|
right:"5",
|
||||||
|
top:"1",
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
|
@ -101,6 +94,15 @@ const option = computed(() => {
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: 'series'
|
focus: 'series'
|
||||||
},
|
},
|
||||||
|
markPoint: {
|
||||||
|
data: [
|
||||||
|
{ type: 'max', name: 'Max' },
|
||||||
|
{ type: 'min', name: 'Min' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
markLine: {
|
||||||
|
data: [{ type: 'average', name: 'Avg' }]
|
||||||
|
},
|
||||||
data: [],
|
data: [],
|
||||||
}
|
}
|
||||||
}) ?? []
|
}) ?? []
|
||||||
|
|
|
@ -10,6 +10,14 @@ import _ from "lodash";
|
||||||
|
|
||||||
var term: Terminal;
|
var term: Terminal;
|
||||||
const terminal = ref<HTMLDivElement|null>(null);
|
const terminal = ref<HTMLDivElement|null>(null);
|
||||||
|
const props=defineProps(
|
||||||
|
{
|
||||||
|
isPage:{
|
||||||
|
type:Boolean,
|
||||||
|
default:false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
const resizeObserver =_.debounce(()=>{
|
const resizeObserver =_.debounce(()=>{
|
||||||
if(terminal.value===null) return;
|
if(terminal.value===null) return;
|
||||||
const cols = Math.floor(terminal.value.clientWidth / 9);
|
const cols = Math.floor(terminal.value.clientWidth / 9);
|
||||||
|
@ -24,8 +32,11 @@ onMounted(()=>{
|
||||||
}});
|
}});
|
||||||
term.open(document.getElementById("terminal") as HTMLElement);
|
term.open(document.getElementById("terminal") as HTMLElement);
|
||||||
if(terminal.value===null) return;
|
if(terminal.value===null) return;
|
||||||
const cols = Math.floor(terminal.value.clientWidth / 9);
|
let cols = Math.floor(terminal.value.clientWidth / 9);
|
||||||
const rows = Math.floor(terminal.value.clientHeight / 17);
|
let rows = Math.floor(terminal.value.clientHeight / 17);
|
||||||
|
if (props.isPage) {
|
||||||
|
cols-=1
|
||||||
|
}
|
||||||
term.resize(cols-2, rows-1)
|
term.resize(cols-2, rows-1)
|
||||||
connection.invoke("ResizeTerminal", rows-1, cols-2)
|
connection.invoke("ResizeTerminal", rows-1, cols-2)
|
||||||
term.focus();
|
term.focus();
|
||||||
|
@ -80,7 +91,9 @@ const resize=()=>{
|
||||||
let width = x - terminal.value.getBoundingClientRect().left;
|
let width = x - terminal.value.getBoundingClientRect().left;
|
||||||
let height = y - terminal.value.getBoundingClientRect().top;
|
let height = y - terminal.value.getBoundingClientRect().top;
|
||||||
// 设置terminal的宽高
|
// 设置terminal的宽高
|
||||||
|
if(!props.isPage){
|
||||||
terminal.value.style.width = `${width}px`;
|
terminal.value.style.width = `${width}px`;
|
||||||
|
}
|
||||||
terminal.value.style.height = `${height}px`;
|
terminal.value.style.height = `${height}px`;
|
||||||
}
|
}
|
||||||
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
||||||
|
@ -91,7 +104,9 @@ const resize=()=>{
|
||||||
width = Math.round(width / 9) * 9+20;
|
width = Math.round(width / 9) * 9+20;
|
||||||
let height = terminal.value.getBoundingClientRect().height;
|
let height = terminal.value.getBoundingClientRect().height;
|
||||||
height = Math.round(height / 17) * 17+20;
|
height = Math.round(height / 17) * 17+20;
|
||||||
|
if(!props.isPage){
|
||||||
terminal.value.style.width = `${width}px`;
|
terminal.value.style.width = `${width}px`;
|
||||||
|
}
|
||||||
terminal.value.style.height = `${height}px`;
|
terminal.value.style.height = `${height}px`;
|
||||||
resizeObserver()
|
resizeObserver()
|
||||||
// 停止监听鼠标移动事件
|
// 停止监听鼠标移动事件
|
||||||
|
@ -107,10 +122,29 @@ const resize=()=>{
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="terminal-box" ref="terminal">
|
<div :class="{'terminal-box':true,'terminal-box-page':isPage}" ref="terminal">
|
||||||
<div id="terminal" @resize="resizeObserver" >
|
<div id="terminal" @resize="resizeObserver" >
|
||||||
</div>
|
</div>
|
||||||
<ArrowDownRight class="arrow" @mousedown="resize" />
|
<ArrowDownRight class="arrow" @mousedown="resize" />
|
||||||
|
<svg
|
||||||
|
class="bg2"
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
style="background: black;filter:grayscale(1);"
|
||||||
|
>
|
||||||
|
|
||||||
|
<filter id='noiseFilter'>
|
||||||
|
<feTurbulence
|
||||||
|
type='fractalNoise'
|
||||||
|
baseFrequency='0.65'
|
||||||
|
numOctaves='3'
|
||||||
|
stitchTiles='stitch' />
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<rect
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
filter='url(#noiseFilter)' />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,11 +159,29 @@ const resize=()=>{
|
||||||
height: 500px;
|
height: 500px;
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
|
min-width: 400px;
|
||||||
|
min-height: 300px;
|
||||||
border-radius: $radius*2;
|
border-radius: $radius*2;
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
|
will-change: contents;
|
||||||
|
}
|
||||||
|
.terminal-box-page{
|
||||||
|
border: unset;
|
||||||
|
padding: unset;
|
||||||
|
width: 100%;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
.bg2{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#terminal {
|
#terminal {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&::-webkit-scrollbar{
|
&::-webkit-scrollbar{
|
||||||
|
|
|
@ -7,8 +7,7 @@ import type {UserInfoType} from "~/types/UserType";
|
||||||
import type {HttpType} from "~/types/baseType";
|
import type {HttpType} from "~/types/baseType";
|
||||||
import {useSessionSignalRStore} from "~/strores/HubStore";
|
import {useSessionSignalRStore} from "~/strores/HubStore";
|
||||||
import {POSITION, useToast} from "vue-toastification";
|
import {POSITION, useToast} from "vue-toastification";
|
||||||
import {type dataHistoryType, useDataStore} from "~/strores/DataStore";
|
import { useDataStore} from "~/strores/DataStore";
|
||||||
import VueDragResize from 'vue-drag-resize'
|
|
||||||
|
|
||||||
const audio = ref<any>(null);
|
const audio = ref<any>(null);
|
||||||
const audio1 = ref<any>(null);
|
const audio1 = ref<any>(null);
|
||||||
|
@ -16,7 +15,6 @@ const audio2 = ref<any>(null);
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const visible = ref<boolean>(false)
|
const visible = ref<boolean>(false)
|
||||||
const DataStore = useDataStore()
|
const DataStore = useDataStore()
|
||||||
const {$gsap} = useNuxtApp()
|
|
||||||
const mainRef = ref<HTMLElement>()
|
const mainRef = ref<HTMLElement>()
|
||||||
const mainLayoutStore = useMainLayoutStore()
|
const mainLayoutStore = useMainLayoutStore()
|
||||||
const ServerList = ref<{ label: string, value: string }[]>([])
|
const ServerList = ref<{ label: string, value: string }[]>([])
|
||||||
|
@ -132,40 +130,9 @@ onMounted(() => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
//断开链接
|
|
||||||
signalR.connection?.stop()
|
signalR.connection?.stop()
|
||||||
})
|
})
|
||||||
const getHistoryData = async () => {
|
|
||||||
$fetch('/Api/Server/GetServerHistoryDate', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + useCookie('token').value
|
|
||||||
},
|
|
||||||
params: {
|
|
||||||
ServerId: mainLayoutStore.SelectServer.value,
|
|
||||||
},
|
|
||||||
baseURL: useRuntimeConfig().public.baseUrl,
|
|
||||||
}).then((res) => {
|
|
||||||
const data = res as dataHistoryType
|
|
||||||
DataStore.dataHistory = data
|
|
||||||
const datas = data.data
|
|
||||||
//遍历字典
|
|
||||||
for (const key in datas) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(datas, key)) {
|
|
||||||
const element = datas[key];
|
|
||||||
//获取最后一次的数据
|
|
||||||
DataStore.data[key] = element[element.length - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DataStore.startTimer()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onMounted(async () => {
|
|
||||||
await getHistoryData()
|
|
||||||
})
|
|
||||||
watch(() => mainLayoutStore.SelectServer.value, async () => {
|
|
||||||
await getHistoryData()
|
|
||||||
})
|
|
||||||
let isShift = false
|
let isShift = false
|
||||||
onKeyStroke('Shift', (e) => {
|
onKeyStroke('Shift', (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
@ -8,7 +8,7 @@ definePageMeta({
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="terminal-layout">
|
<div class="terminal-layout">
|
||||||
<Term/>
|
<Term is-page/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@ definePageMeta({
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
border: $border;
|
border: $border;
|
||||||
background: #000;
|
background: #000;
|
||||||
padding: $padding*2;
|
padding: $padding;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,14 @@ import {
|
||||||
CalendarComponent,
|
CalendarComponent,
|
||||||
AriaComponent,
|
AriaComponent,
|
||||||
BrushComponent,
|
BrushComponent,
|
||||||
|
MarkLineComponent,
|
||||||
|
MarkPointComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
DataZoomComponent
|
DataZoomComponent
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
use([CanvasRenderer, BarChart, GaugeChart, LinesChart, LineChart, PieChart, GridComponent, TooltipComponent, ToolboxComponent, TimelineComponent, TitleComponent, LegendComponent, DataZoomComponent]);
|
use([CanvasRenderer, BarChart, GaugeChart, LinesChart, LineChart, PieChart, GridComponent, TooltipComponent, ToolboxComponent, TimelineComponent, TitleComponent, LegendComponent, DataZoomComponent,MarkLineComponent,
|
||||||
|
MarkPointComponent]);
|
||||||
});
|
});
|
|
@ -5,42 +5,11 @@ import dayjs from "dayjs";
|
||||||
export const useDataStore = defineStore('DataStore', {
|
export const useDataStore = defineStore('DataStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
data: <dataType>{},
|
data: <dataType>{},
|
||||||
dataHistory: <dataHistoryType>{...defaultDataHistory},
|
|
||||||
timer: null as any,
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setData(type: string, data: any) {
|
setData(type: string, data: any) {
|
||||||
//不存在则添加,存在则更新
|
|
||||||
this.data[type] = data
|
this.data[type] = data
|
||||||
},
|
},
|
||||||
startTimer() {
|
|
||||||
if (this.timer) return; // 防止重复启动定时器
|
|
||||||
|
|
||||||
this.timer = setInterval(() => {
|
|
||||||
// 每秒将 data 中的数据添加到 dataHistory 中
|
|
||||||
this.addDataToHistory();
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
stopTimer() {
|
|
||||||
if (this.timer) {
|
|
||||||
clearInterval(this.timer);
|
|
||||||
this.timer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addDataToHistory() {
|
|
||||||
const currentTime = dayjs().format('MM-DD HH:mm:ss');
|
|
||||||
for (const key in this.data) {
|
|
||||||
const value = this.data[key];
|
|
||||||
if (!this.dataHistory.data[key]) {
|
|
||||||
this.dataHistory.data[key] = [];
|
|
||||||
}
|
|
||||||
this.dataHistory.data[key].push(value)
|
|
||||||
}
|
|
||||||
this.dataHistory.times.push(currentTime);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
persist: {
|
|
||||||
storage: persistedState.sessionStorage,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -48,15 +17,3 @@ export const useDataStore = defineStore('DataStore', {
|
||||||
export type dataType = {
|
export type dataType = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type dataHistoryType = {
|
|
||||||
times: string[],
|
|
||||||
data: dataHistoryDataType,
|
|
||||||
}
|
|
||||||
type dataHistoryDataType = {
|
|
||||||
[key: string]: string[]
|
|
||||||
}
|
|
||||||
const defaultDataHistory: dataHistoryType = {
|
|
||||||
times: [],
|
|
||||||
data: {}
|
|
||||||
}
|
|
|
@ -44,7 +44,6 @@ export function useBeep() {
|
||||||
|
|
||||||
export function toggleDark(event: MouseEvent) {
|
export function toggleDark(event: MouseEvent) {
|
||||||
const isAppearanceTransition = document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
const isAppearanceTransition = document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
|
||||||
if (!isAppearanceTransition) {
|
if (!isAppearanceTransition) {
|
||||||
useColorMode().preference = useColorMode().value == 'dark' ? 'light' : 'dark'
|
useColorMode().preference = useColorMode().value == 'dark' ? 'light' : 'dark'
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in New Issue