初赛截稿
|
@ -115,8 +115,8 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
|||
Name = roleName,
|
||||
NormalizedName = roleName.ToUpperInvariant(),
|
||||
ApiPermissions =
|
||||
["1","2","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20","21","22","23","24","25","26","27","28","29","30"],
|
||||
RouterPermissions = ["1", "2", "3", "4","5","6","7","8","9","10","11","12","13","14"]
|
||||
["1","2","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35"],
|
||||
RouterPermissions = ["1", "2", "3", "4","5","6","7","8","9","10","11","12","13","14","15","16"]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -145,6 +145,8 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
|||
new RotePermission { Id = 10, Name = "进程列表", Router = "/host/process" },
|
||||
new RotePermission { Id = 11, Name = "网络连接列表", Router = "/host/networklist" },
|
||||
new RotePermission { Id = 12, Name = "巡检记录", Router = "/inspectionrecords" },
|
||||
new RotePermission { Id = 13, Name = "巡检记录", Router = "^/settings/.+$" },
|
||||
new RotePermission { Id = 14, Name = "登录页面修改密码", Router = "^/changepassword/.+$" },
|
||||
];
|
||||
|
||||
foreach (var permission in rotePermissions) modelBuilder.Entity<RotePermission>().HasData(permission);
|
||||
|
@ -172,6 +174,9 @@ public class ApplicationUser : IdentityUser
|
|||
|
||||
[MaxLength(255)] public string? PhysicalAddress { get; set; }
|
||||
|
||||
[DataType(DataType.DateTime)]
|
||||
public DateTimeOffset? PasswordExpiredDate { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class ApplicationRole : IdentityRole
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[CpuTotalUsage_d3YT]
|
||||
Notify=20
|
||||
Warning=30
|
|
@ -0,0 +1,5 @@
|
|||
[CpuTotalUsage_d3YT]
|
||||
ValueName=Cpu总使用率
|
||||
Description=Cpu总使用率超过20%通知,超过30%警告
|
||||
Notify=20
|
||||
Warning=30
|
|
@ -36,7 +36,7 @@ ValueName = 句柄总使用数
|
|||
Description = A simple job that uses the Process
|
||||
JobType = LoongPanel_Asp.Jobs.PhrasePatternJob, LoongPanel-Asp
|
||||
Executor = d3YT,xseg
|
||||
CronExpression = 6/30 * * * * ? *
|
||||
CronExpression = 6/15 * * * * ? *
|
||||
|
||||
[MemoryTotalJob]
|
||||
Group = Memory
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using LiteDB;
|
||||
using LoongPanel_Asp.Helpers;
|
||||
using LoongPanel_Asp.Models;
|
||||
using LoongPanel_Asp.Types;
|
||||
using LoongPanel_Asp.Servers;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -13,7 +14,7 @@ namespace LoongPanel_Asp.Controllers;
|
|||
public class AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
EmailHelper emailHelper,
|
||||
EmailService emailService,
|
||||
TokenHelper tokenHelper,
|
||||
ILiteDatabase db)
|
||||
: ControllerBase
|
||||
|
@ -59,47 +60,157 @@ public class AccountController(
|
|||
[HttpPost("Login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginModel model)
|
||||
{
|
||||
if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request"));
|
||||
|
||||
try
|
||||
if (!ModelState.IsValid) return BadRequest("错误的内容");
|
||||
ApplicationUser? user;
|
||||
if (model.EmailOrUserName.Contains('@'))
|
||||
user = await userManager.FindByEmailAsync(model.EmailOrUserName);
|
||||
else
|
||||
user = await userManager.FindByNameAsync(model.EmailOrUserName);
|
||||
if (user == null) return BadRequest("用户不存在");
|
||||
var result = await signInManager.CheckPasswordSignInAsync(user, model.Password,true);
|
||||
if (!result.Succeeded) return BadRequest("错误账号或密码");
|
||||
if (await userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
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[]
|
||||
return BadRequest("账号已锁定,请联系管理员");
|
||||
}
|
||||
if (!user.EmailConfirmed&&user.UserName!="admin")
|
||||
{
|
||||
return Unauthorized(new
|
||||
{
|
||||
// userId
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||
// email
|
||||
new Claim(ClaimTypes.Email, user.Email!),
|
||||
// role
|
||||
new Claim(ClaimTypes.Role, roleId.ToLower()) // 将角色ID列表转换为逗号分隔的字符串
|
||||
user.Email,
|
||||
user.Id
|
||||
});
|
||||
var token = tokenHelper.GenerateToken(claimsIdentity);
|
||||
|
||||
return Ok(new ApiResponse(ApiResponseState.Success, "Login successful", new
|
||||
{
|
||||
user.UserName,
|
||||
Token = token
|
||||
}));
|
||||
;
|
||||
}
|
||||
catch (Exception ex)
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
var roleId = roles.ToList()[0]; // 直接获取角色ID列表
|
||||
var claimsIdentity = new ClaimsIdentity(new[]
|
||||
{
|
||||
// Log the exception
|
||||
// logger.LogError(ex, "An error occurred while processing the login request.");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
new ApiResponse(ApiResponseState.Error, ex.Message));
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||
new Claim(ClaimTypes.Email, user.Email!),
|
||||
new Claim(ClaimTypes.Role, roleId.ToLower()) // 将角色ID列表转换为逗号分隔的字符串
|
||||
});
|
||||
var token = tokenHelper.GenerateToken(claimsIdentity);
|
||||
if (user.PasswordExpiredDate == null || user.PasswordExpiredDate < DateTimeOffset.Now)
|
||||
{
|
||||
//返回402
|
||||
return StatusCode(402,new
|
||||
{
|
||||
token,
|
||||
user.Id,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
user.NickName,
|
||||
Token=token
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("VerifyEmail")]
|
||||
public async Task<IActionResult> VerifyEmail( [FromQuery] string userId,[FromQuery] string email ,[FromQuery] string? code = null)
|
||||
{
|
||||
//如果code 不为空
|
||||
if (code != null)
|
||||
{
|
||||
var r= emailService.VerifyEmailVerifyCode(userId,code);
|
||||
if (!r) return BadRequest("验证码错误");
|
||||
//验证成功
|
||||
//更新用户信息
|
||||
var user = await userManager.FindByIdAsync(userId);
|
||||
if (user == null) return BadRequest("用户不存在");
|
||||
user.EmailConfirmed = true;
|
||||
user.Email = email;
|
||||
await userManager.UpdateAsync(user);
|
||||
return Ok("邮箱验证成功");
|
||||
}
|
||||
await emailService.SendEmailVerifyCodeAsync( userId,email, "尊敬的用户");
|
||||
return Ok("邮件已发送");
|
||||
}
|
||||
[HttpGet("ChangePassword")]
|
||||
public async Task<IActionResult> ChangePassword([FromQuery] string currentPassword,[FromQuery] string newPassword)
|
||||
{
|
||||
// 获取当前经过身份验证的用户
|
||||
var authenticatedUser = await userManager.GetUserAsync(HttpContext.User);
|
||||
|
||||
if (authenticatedUser == null)
|
||||
{
|
||||
return BadRequest("用户未登录");
|
||||
}
|
||||
|
||||
// 检查当前密码是否正确
|
||||
var isCurrentPasswordValid = await userManager.CheckPasswordAsync(authenticatedUser, currentPassword);
|
||||
if (!isCurrentPasswordValid)
|
||||
{
|
||||
return BadRequest("当前密码不正确");
|
||||
}
|
||||
|
||||
// 检查新密码是否与旧密码相同
|
||||
if (currentPassword == newPassword)
|
||||
{
|
||||
return BadRequest("新密码不能与旧密码相同");
|
||||
}
|
||||
|
||||
// 生成密码重置令牌
|
||||
var token = await userManager.GeneratePasswordResetTokenAsync(authenticatedUser);
|
||||
|
||||
// 重置密码
|
||||
var result = await userManager.ResetPasswordAsync(authenticatedUser, token, newPassword);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return BadRequest("修改密码失败");
|
||||
}
|
||||
//修改用户的过期时间为1天后
|
||||
authenticatedUser.PasswordExpiredDate = DateTimeOffset.Now.AddDays(1);
|
||||
await userManager.UpdateAsync(authenticatedUser);
|
||||
return Ok("修改密码成功");
|
||||
}
|
||||
|
||||
[HttpGet("ForgotPassword")]
|
||||
public async Task<IActionResult> ForgotPassword([FromQuery] string email)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user == null) return BadRequest("用户不存在");
|
||||
var token = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
await emailService.SendEmailAsync(email, "尊敬的用户", "重置密码",
|
||||
$"请点击<a href='http://192.168.0.13:3001/ResetPassword?email={email}&token={token}'>这里</a>重置密码");
|
||||
return Ok("邮件已发送");
|
||||
}
|
||||
|
||||
[HttpGet("ResetPassword")]
|
||||
public async Task<IActionResult> ResetPassword([FromQuery] string email, [FromQuery] string token)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user == null) return BadRequest("用户不存在");
|
||||
//随机生成密码12位 特殊字符大小写数字 英文
|
||||
token= token.Replace(" ", "+");
|
||||
var newPassword = GenerateRandomPassword(32);
|
||||
var result = await userManager.ResetPasswordAsync(user, token, newPassword);
|
||||
if (!result.Succeeded) return BadRequest(result.Errors.FirstOrDefault()?.Description);
|
||||
user.PasswordExpiredDate = DateTimeOffset.Now.AddDays(1);
|
||||
await userManager.UpdateAsync(user);
|
||||
await emailService.SendEmailAsync(user.Email ?? throw new InvalidOperationException(),
|
||||
user.UserName ?? throw new InvalidOperationException(), "新密码",
|
||||
$"<h1>您的新密码是:<strong>{newPassword}</strong></h1>");
|
||||
return Ok("密码重置成功,新密码已生成并发送给用户邮箱。");
|
||||
}
|
||||
|
||||
//登出
|
||||
[HttpGet("Logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await signInManager.SignOutAsync();
|
||||
return Ok("Logout successful");
|
||||
}
|
||||
private string GenerateRandomPassword(int length)
|
||||
{
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*";
|
||||
var byteBuffer = new byte[length];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(byteBuffer);
|
||||
}
|
||||
return new string(byteBuffer.Select(b => chars[b % chars.Length]).ToArray());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
using System.Security.Claims;
|
||||
using IniParser;
|
||||
using IniParser.Model;
|
||||
using LoongPanel_Asp.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LoongPanel_Asp.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("Api/[controller]")]
|
||||
public class ConfigController:ControllerBase
|
||||
{
|
||||
[HttpGet("GetAlertConfig")]
|
||||
public IActionResult GetAlertConfig()
|
||||
{
|
||||
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
|
||||
//从Configs/Alerts/userId.ini
|
||||
var parser=new FileIniDataParser();
|
||||
var fullPath=Path.Combine("Configs","Alerts",$"{userId}.ini");
|
||||
//判断文件是否存在
|
||||
if (!System.IO.File.Exists(fullPath))
|
||||
{
|
||||
//复制一份默认配置文件
|
||||
System.IO.File.Copy(Path.Combine("Configs","alert.ini"),fullPath);
|
||||
JobConfigHelper.ReloadAlerts();
|
||||
}
|
||||
var data = parser.ReadFile(fullPath);
|
||||
//获得所有section
|
||||
var sections = data.Sections;
|
||||
//遍历
|
||||
var alertsConfigs = sections.Select(x =>
|
||||
{
|
||||
var alertConfiguration = new
|
||||
{
|
||||
AlertType = x.SectionName.Split("_")[0],
|
||||
ServerId = x.SectionName.Split("_")[1],
|
||||
Notify= x.Keys["Notify"],
|
||||
Warning=x.Keys["Warning"],
|
||||
TypeName=x.Keys["ValueName"],
|
||||
Description=x.Keys["Description"],
|
||||
};
|
||||
return alertConfiguration;
|
||||
});
|
||||
return Ok(alertsConfigs);
|
||||
}
|
||||
|
||||
[HttpPost("AddAlertConfig")]
|
||||
public IActionResult AddAlertConfig([FromBody] AlertModel model)
|
||||
{
|
||||
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
|
||||
var fullPath=Path.Combine("Configs","Alerts",$"{userId}.ini");
|
||||
var parser=new FileIniDataParser();
|
||||
var data = parser.ReadFile(fullPath);
|
||||
//判断是否存在
|
||||
if (data.Sections.Contains($"{model.DataType}_{model.ServerId}"))
|
||||
{
|
||||
return BadRequest("配置已存在");
|
||||
}
|
||||
var sectionName=$"{model.DataType}_{model.ServerId}";
|
||||
data[sectionName]["Notify"]=model.Notify;
|
||||
data[sectionName]["Warning"]=model.Warning;
|
||||
data[sectionName]["ValueName"]=model.DataName;
|
||||
data[sectionName]["Description"]=model.Description;
|
||||
parser.WriteFile(fullPath,data);
|
||||
JobConfigHelper.ReloadAlerts();
|
||||
return Ok("配置已添加");
|
||||
}
|
||||
[HttpDelete("DeleteAlertConfig")]
|
||||
public IActionResult DeleteAlertConfig([FromQuery] string dataType,[FromQuery] string serverId)
|
||||
{
|
||||
var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value;
|
||||
var fullPath=Path.Combine("Configs","Alerts",$"{userId}.ini");
|
||||
var parser=new FileIniDataParser();
|
||||
var data = parser.ReadFile(fullPath);
|
||||
//判断是否存在
|
||||
if (!data.Sections.Contains($"{dataType}_{serverId}"))
|
||||
{
|
||||
return BadRequest("配置不存在");
|
||||
}
|
||||
data.Sections.RemoveSection($"{dataType}_{serverId}");
|
||||
parser.WriteFile(fullPath,data);
|
||||
JobConfigHelper.ReloadAlerts();
|
||||
return Ok("配置已删除");
|
||||
}
|
||||
}
|
||||
|
||||
public class AlertModel
|
||||
{
|
||||
public required string ServerId {get;init;}
|
||||
public required string DataType {get;init;}
|
||||
public required string Description {get;init;}
|
||||
public required string Notify {get;init;}
|
||||
public required string Warning {get;init;}
|
||||
|
||||
public required string DataName {get;init;}
|
||||
}
|
|
@ -7,18 +7,18 @@ namespace LoongPanel_Asp.Controllers;
|
|||
public class JobController(ApplicationDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet("GetJobList")]
|
||||
public IActionResult GetJobList(string serverId)
|
||||
public IActionResult GetJobList([FromQuery]string? serverId=null)
|
||||
{
|
||||
var serverMonitoringData = dbContext.ServerMonitoringData;
|
||||
//获得所有DataName类型
|
||||
var dataNameTypesWithDataType = serverMonitoringData
|
||||
.GroupBy(x => x.DataName) // 假设DataName是ServerMonitoringData实体中的一个属性
|
||||
.Where(x=>!string.IsNullOrEmpty(serverId) || x.ServerId==serverId)
|
||||
.GroupBy(x => x.DataName)
|
||||
.Select(group => new
|
||||
{
|
||||
DataName = group.Key, group.First().DataType // 假设DataType是ServerMonitoringData实体中的一个属性
|
||||
DataName = group.Key, group.First().DataType ,group.First().ServerId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(dataNameTypesWithDataType); // 返回所有唯一的DataName类型及其对应的DataType
|
||||
return Ok(dataNameTypesWithDataType);
|
||||
}
|
||||
}
|
|
@ -17,11 +17,10 @@ public class RoteController(
|
|||
{
|
||||
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);
|
||||
var apiPermissions = dbContext.RotePermissions.ToList().Where(x => rotes.Any(y => y == x.Id.ToString())).Select(x=>x.Router).ToList();
|
||||
//将path全部小写
|
||||
path = path.ToLower();
|
||||
//使用正则匹配
|
||||
|
|
|
@ -45,6 +45,7 @@ public class DataHelper(ApplicationDbContext dbContext,IHubContext<SessionHub> c
|
|||
foreach (var item in matchingValues)
|
||||
{
|
||||
await context.Clients.Group(item).SendAsync("ReceiveWaring", value, valueName);
|
||||
var key=$"{serverId}_{valueType}+{item}";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
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];
|
||||
}
|
||||
}
|
|
@ -164,6 +164,18 @@ public static class JobConfigHelper
|
|||
_alertsConfigs =alertsConfigs;
|
||||
return alertsConfigs;
|
||||
}
|
||||
|
||||
public static void ReloadAlerts()
|
||||
{
|
||||
_alertsConfigs = null;
|
||||
GetAlerts();
|
||||
}
|
||||
|
||||
public static void ReloadServers()
|
||||
{
|
||||
_serverConfigs = null;
|
||||
GetServers();
|
||||
}
|
||||
}
|
||||
|
||||
public class JobConfiguration
|
||||
|
|
|
@ -113,12 +113,15 @@ public class CpuSpeedJob(
|
|||
}
|
||||
|
||||
cpuDataListAll.AddRange(cpuDataList);
|
||||
cpuDataList.ForEach(async data =>
|
||||
|
||||
cpuDataList.ForEach(Action);
|
||||
continue;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
@ -162,11 +165,14 @@ public class CpuSingleUsageJob(
|
|||
cpuDataList.Add(singleUsage);
|
||||
}
|
||||
|
||||
cpuDataList.ForEach(async data =>
|
||||
cpuDataList.ForEach(Action);
|
||||
continue;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
|
|
@ -37,6 +37,7 @@ public class DiskTotalJob(
|
|||
};
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", diskTotalUsage.ServerId, diskTotalUsage.DataType,
|
||||
diskTotalUsage.Data);
|
||||
await dataHelper.CheckData(server.Id, diskTotalUsage.DataType ?? "", diskTotalUsage.Data ?? "", "磁盘总使用率");
|
||||
diskDataList.Add(diskTotalUsage);
|
||||
diskDataListAll.AddRange(diskDataList);
|
||||
}
|
||||
|
@ -116,10 +117,15 @@ public class DiskUseJob(
|
|||
DataType = $"DiskUtil-{dev}"
|
||||
};
|
||||
diskDataList.Add(diskUtil);
|
||||
diskDataList.ForEach(data =>
|
||||
|
||||
diskDataList.ForEach(Action);
|
||||
continue;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
});
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
}
|
||||
}
|
||||
diskDataListAll.AddRange(diskDataList);
|
||||
}
|
||||
|
|
|
@ -79,11 +79,16 @@ public class MemoryTotalJob(
|
|||
DataType = "SwapTotalUsage"
|
||||
};
|
||||
serverDataList.Add(swapData);
|
||||
serverDataList.ForEach(data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
});
|
||||
|
||||
serverDataList.ForEach(Action);
|
||||
serverDataListAll.AddRange(serverDataList);
|
||||
continue;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
}
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
|
|
@ -65,10 +65,15 @@ public class NetworkTotalJob(
|
|||
};
|
||||
netWorkDataList.Add(d);
|
||||
netWorkDataListAll.AddRange(netWorkDataList);
|
||||
netWorkDataList.ForEach(data =>
|
||||
|
||||
netWorkDataList.ForEach( Action);
|
||||
continue;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
});
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
}
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
|
|
@ -46,11 +46,16 @@ public class ProcessTotalJob(
|
|||
DataType = "ThreadsTotalCount"
|
||||
};
|
||||
processDataList.Add(threadsTotalCount);
|
||||
processDataList.ForEach(data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
});
|
||||
|
||||
processDataList.ForEach(Action);
|
||||
processDataListAll.AddRange(processDataList);
|
||||
continue;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(server.Id, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
}
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
@ -89,6 +94,8 @@ public class PhrasePatternJob(
|
|||
processDataListAll.Add(phrasePatternCount);
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", server.Id, phrasePatternCount.DataType,
|
||||
phrasePatternCount.Data);
|
||||
await dataHelper.CheckData(server.Id, phrasePatternCount.DataType ?? "", phrasePatternCount.Data ?? "",
|
||||
"句柄数");
|
||||
}
|
||||
|
||||
_count++;
|
||||
|
|
|
@ -59,13 +59,18 @@ public class UserTotalJob(IHubContext<SessionHub> hubContext,
|
|||
userDataListAll.Add(data);
|
||||
}
|
||||
}
|
||||
userDataListAll.ForEach(data =>
|
||||
{
|
||||
hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
});
|
||||
|
||||
userDataListAll.ForEach(Action);
|
||||
_count++;
|
||||
if (_count <= 10) return;
|
||||
_count = 0;
|
||||
await dataHelper.SaveData(userDataListAll);
|
||||
return;
|
||||
|
||||
async void Action(ServerMonitoringData data)
|
||||
{
|
||||
await hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data);
|
||||
await dataHelper.CheckData(data.ServerId, data.DataType ?? "", data.Data ?? "", data.DataName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,6 +46,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\public\" />
|
||||
<Folder Include="Configs\Alerts\" />
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -16,9 +16,8 @@ public class ApiPermissionMiddleware(
|
|||
// 获取配置中定义的公开API列表
|
||||
var publicApis = configuration["PublicApi"]?.Split(";", StringSplitOptions.RemoveEmptyEntries) ??
|
||||
[];
|
||||
|
||||
// 如果请求路径在公开API列表中,则直接调用下一个中间件
|
||||
if (publicApis.Any(api => api == context.Request.Path.Value))
|
||||
|
||||
if (publicApis.Any(api => context.Request.Path.Value == api))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
|
|
|
@ -13,7 +13,7 @@ public class PermissionMiddleware(
|
|||
var publicApis = configuration["PublicApi"]?.Split(";") ?? [];
|
||||
|
||||
// 如果请求路径在公开API列表中,则直接调用下一个中间件
|
||||
if (publicApis.Any(api => api == context.Request.Path.Value))
|
||||
if (publicApis.Any(api => context.Request.Path.Value == api))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
|
|
|
@ -18,3 +18,12 @@ public class ServerModel
|
|||
public required string Username { get; set; }
|
||||
public required bool Http { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class EmailSettings
|
||||
{
|
||||
public required string Host {get;init;}
|
||||
public required string Port {get;init;}
|
||||
public required string Username {get;init;}
|
||||
public required string Password {get;init;}
|
||||
}
|
|
@ -3,8 +3,10 @@ using LoongPanel_Asp;
|
|||
using LoongPanel_Asp.Helpers;
|
||||
using LoongPanel_Asp.Hubs;
|
||||
using LoongPanel_Asp.Middlewares;
|
||||
using LoongPanel_Asp.Models;
|
||||
using LoongPanel_Asp.Servers;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Quartz;
|
||||
using Quartz.AspNetCore;
|
||||
|
@ -39,7 +41,7 @@ builder.Services.Configure<IdentityOptions>(options =>
|
|||
// User settings.
|
||||
options.User.AllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
|
||||
options.User.RequireUniqueEmail = false;
|
||||
options.User.RequireUniqueEmail = true;
|
||||
});
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
|
@ -61,14 +63,8 @@ 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<EmailService>(provider => new EmailService(emailSettings));
|
||||
|
||||
|
||||
builder.Services.AddSingleton<ILiteDatabase, LiteDatabase>(
|
||||
sp => new LiteDatabase("Filename=temp.db;Connection=shared;"));
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://127.0.0.1:58826",
|
||||
"applicationUrl": "http://192.168.0.13:58826",
|
||||
"sslPort": 44304
|
||||
}
|
||||
},
|
||||
|
@ -14,7 +14,7 @@
|
|||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://127.0.0.1:5253",
|
||||
"applicationUrl": "http://192.168.0.13:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
|||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://127.0.0.1:7233;http://127.0.0.1:5253",
|
||||
"applicationUrl": "https://192.168.0.13:7233;http://192.168.0.13:5253",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using LoongPanel_Asp.Models;
|
||||
using MimeKit;
|
||||
using SmtpClient = MailKit.Net.Smtp.SmtpClient;
|
||||
|
||||
namespace LoongPanel_Asp.Servers;
|
||||
|
||||
public class EmailService:IDisposable
|
||||
{
|
||||
|
||||
private readonly SmtpClient _smtpClient;
|
||||
private readonly IConfigurationSection _emailSettings;
|
||||
//创建一个验证码缓存字典
|
||||
private readonly ConcurrentDictionary<string, string> _verifyCodeCache = new();
|
||||
public EmailService(IConfigurationSection emailSettings)
|
||||
{
|
||||
_emailSettings = emailSettings;
|
||||
// 初始化SmtpClient
|
||||
_smtpClient = new SmtpClient();
|
||||
_smtpClient.Connect(_emailSettings["Host"], int.Parse(_emailSettings["Port"] ?? throw new InvalidOperationException()), true);
|
||||
_smtpClient.Authenticate(_emailSettings["Username"], _emailSettings["Password"]);
|
||||
}
|
||||
|
||||
|
||||
//发送邮件
|
||||
public async Task SendEmailAsync(string toAddress,string toName, string subject, string body)
|
||||
{
|
||||
using var mailMessage = new MimeMessage();
|
||||
mailMessage.From.Add(new MailboxAddress("龙盾云御",_emailSettings["Username"]));
|
||||
mailMessage.To.Add(new MailboxAddress(toName, toAddress));
|
||||
mailMessage.Subject = subject;
|
||||
var bodyBuilder = new BodyBuilder
|
||||
{
|
||||
HtmlBody = body,
|
||||
};
|
||||
mailMessage.Body = bodyBuilder.ToMessageBody();
|
||||
await _smtpClient.SendAsync(mailMessage);
|
||||
}
|
||||
|
||||
|
||||
//发送邮箱验证码
|
||||
public async Task SendEmailVerifyCodeAsync( string userId,string toAddress, string toName)
|
||||
{
|
||||
const string subject = "龙盾云御邮箱验证码";
|
||||
var code=GenerateVerifyCode();
|
||||
var body = $"<h1>您的验证码是:{code}</h1>";
|
||||
await SendEmailAsync(toAddress, toName, subject, body);
|
||||
//存入缓存
|
||||
_verifyCodeCache.AddOrUpdate(userId, code, (key, oldValue) => code);
|
||||
}
|
||||
//验证邮箱验证码
|
||||
public bool VerifyEmailVerifyCode(string userId, string code)
|
||||
{
|
||||
if (!_verifyCodeCache.TryGetValue(userId, out var verifyCode)) return false;
|
||||
if (verifyCode != code) return false;
|
||||
_verifyCodeCache.TryRemove(userId, out _);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//生成6位随机验证码
|
||||
private static string GenerateVerifyCode()
|
||||
{
|
||||
var random = new Random();
|
||||
var verifyCode = random.Next(100000, 999999).ToString();
|
||||
return verifyCode;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -20,5 +20,5 @@
|
|||
"Secret": "p4Qzf/+GPP/XNLalZGCzwlelOl6skiFZscj6iZ6rZZE=",
|
||||
"Issuer": "LoongPanel",
|
||||
"Audience": "LoongPanel",
|
||||
"PubLicApi": "/Api/Account/Login"
|
||||
"PubLicApi": "/Api/Account/Login;/Api/Account/VerifyEmail;/Api/Account/ForgotPassword;/Api/Account/ResetPassword;"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
| Öµ°à¼Ç¼ | col | col | col | col | col |
|
||||
| -------- | ------- | ------- | ------- | ------- | ------- |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
||||
| content | content | content | content | content | content |
|
|
@ -1 +1 @@
|
|||
NUXT_API_URL="http://127.0.0.1:5253"
|
||||
NUXT_API_URL="http://192.168.0.13:5253"
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.2 KiB |
|
@ -4,7 +4,6 @@
|
|||
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 {
|
||||
|
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.2 KiB |
BIN
web/bun.lockb
|
@ -3,79 +3,108 @@
|
|||
</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>
|
||||
<input class="toggle-checkbox" type="checkbox" :checked="$colorMode.value == 'dark'" @click="$colorMode.preference = $colorMode.value == 'dark' ? 'light' : 'dark'">
|
||||
<div class="toggle-slot">
|
||||
<div class="sun-icon-wrapper">
|
||||
<div class="iconify sun-icon" data-icon="feather-sun" data-inline="false"></div>
|
||||
</div>
|
||||
<div class="toggle-button"></div>
|
||||
<div class="moon-icon-wrapper">
|
||||
<div class="iconify moon-icon" data-icon="feather-moon" data-inline="false"></div>
|
||||
</div>
|
||||
</div>
|
||||
</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 {
|
||||
@import "base";
|
||||
.toggle-checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
height: 0;
|
||||
width: 0;
|
||||
|
||||
}
|
||||
|
||||
.off {
|
||||
.toggle-slot {
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
height: 2em;
|
||||
width: 4em;
|
||||
border: 0 solid transparent;
|
||||
border-radius: 10em;
|
||||
background-color: $light-bg-underline-color;
|
||||
transition: background-color 250ms;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked ~ .toggle-slot {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
transform: translate(0.3em, 0.25em);
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
image-rendering: pixelated;
|
||||
opacity: 1;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
border-radius: 50%;
|
||||
background-color: #ffeccf;
|
||||
box-shadow: inset 0px 0px 0px 0.75em #ffbb52;
|
||||
transition: background-color 250ms, border-color 250ms, transform 500ms cubic-bezier(.26,2,.46,.71);
|
||||
}
|
||||
|
||||
.on {
|
||||
.toggle-checkbox:checked ~ .toggle-slot .toggle-button {
|
||||
background-color: #485367;
|
||||
box-shadow: inset 0px 0px 0px 0.75em white;
|
||||
transform: translate(2em, 0.25em);
|
||||
}
|
||||
|
||||
.sun-icon {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
background-color: white;
|
||||
transition: 4s;
|
||||
background-color: #ccc;
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
color: #ffbb52;
|
||||
}
|
||||
|
||||
.switch input:focus+.slider {
|
||||
box-shadow: 0 0 1px #ccc;
|
||||
}
|
||||
|
||||
.switch input:checked~.off {
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
|
||||
.switch input:checked~.on {
|
||||
transform: translateX(1rem);
|
||||
.sun-icon-wrapper {
|
||||
position: absolute;
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
opacity: 1;
|
||||
transform: translate(1em, 1em) rotate(15deg);
|
||||
transform-origin: 50% 50%;
|
||||
transition: opacity 150ms, transform 500ms cubic-bezier(.26,2,.46,.71);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked ~ .toggle-slot .sun-icon-wrapper {
|
||||
opacity: 0;
|
||||
transform: translate(1.5em, 1em) rotate(0deg);
|
||||
}
|
||||
|
||||
.moon-icon {
|
||||
position: absolute;
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.moon-icon-wrapper {
|
||||
position: absolute;
|
||||
height: 6em;
|
||||
width: 6em;
|
||||
opacity: 0;
|
||||
transform: translate(1.5em, 1em) rotate(0deg);
|
||||
transform-origin: 50% 50%;
|
||||
transition: opacity 150ms, transform 500ms cubic-bezier(.26,2.5,.46,.71);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked ~ .toggle-slot .moon-icon-wrapper {
|
||||
opacity: 1;
|
||||
transform: translate(1em, 1em) rotate(-15deg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -43,27 +43,6 @@ const items = ref([
|
|||
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[]) => {
|
||||
|
@ -124,15 +103,7 @@ watchDebounced(
|
|||
</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>
|
||||
<SpeedDial :model="items" class="speed-button" :tooltipOptions="{ position: 'left' }" />
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
:pt="{
|
||||
|
@ -205,15 +176,21 @@ watchDebounced(
|
|||
<style lang="scss" scoped>
|
||||
.main-grid-layout {
|
||||
//min-width: 950px;
|
||||
|
||||
}
|
||||
|
||||
.draggable-action {
|
||||
background: red;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.vgl-layout {
|
||||
margin: -10px;
|
||||
}
|
||||
:deep(.speed-button){
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
bottom: 40px;
|
||||
z-index: 100;
|
||||
gap: 5px;
|
||||
.p-speeddial-list{
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -18,6 +18,10 @@ const props=defineProps({
|
|||
template:{
|
||||
type:String,
|
||||
default: `# ${dayjs().format()}`
|
||||
},
|
||||
closeCallBack:{
|
||||
type:Function,
|
||||
default:()=>{}
|
||||
}
|
||||
})
|
||||
const text = ref<string>(props.template);
|
||||
|
@ -134,6 +138,9 @@ onBeforeMount(()=>{
|
|||
<Button label="保存" @click="saveFileName" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<div class="actions">
|
||||
<Icon name="X" @click="closeCallBack" ></Icon>
|
||||
</div>
|
||||
<MdEditor v-model="text" v-if="!preview" :theme="$colorMode.value as 'light'| 'dark'" class="editor" codeTheme="kimbie"
|
||||
@onSave="onSave" @onUploadImg="onUploadImg" />
|
||||
<MdPreview :modelValue="text" v-else class="editor-preview"/>
|
||||
|
@ -144,7 +151,6 @@ onBeforeMount(()=>{
|
|||
@import "base";
|
||||
|
||||
.editor-layout {
|
||||
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -172,4 +178,18 @@ onBeforeMount(()=>{
|
|||
padding: $padding;
|
||||
}
|
||||
}
|
||||
.actions{
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
svg{
|
||||
cursor: pointer;
|
||||
stroke: $light-text-color;
|
||||
color: $light-text-color;
|
||||
&:hover{
|
||||
stroke: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -49,7 +49,7 @@ const refresh = () => {
|
|||
<div class="user-list-body">
|
||||
<div class="user-list-item" @click="navigateTo(`/serverUser/all`)">
|
||||
<div class="avatar">
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" :image="`https://api.multiavatar.com/all.svg`"/>
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" label="全"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="top">
|
||||
|
@ -58,9 +58,9 @@ const refresh = () => {
|
|||
<p>NULL</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-list-item" v-for="item in userList" @click="navigateTo(`/serverUser/${item.name}`)">
|
||||
<div :class="{'user-list-item':true,'user-list-item-active':$route.params.id===item.name}" v-for="item in userList" @click="navigateTo(`/serverUser/${item.name}`)">
|
||||
<div class="avatar">
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" :image="`https://api.multiavatar.com/${item.name}.svg`"/>
|
||||
<Avatar size="xlarge" shape="circle" class="avatar" :label="item.name[0].toUpperCase()"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="top">
|
||||
|
@ -87,6 +87,12 @@ const refresh = () => {
|
|||
border: $border;
|
||||
padding: $padding 0;
|
||||
grid-row: 1/3;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.user-list-header {
|
||||
|
@ -94,15 +100,29 @@ const refresh = () => {
|
|||
align-items: center;
|
||||
padding: 0 $padding;
|
||||
justify-content: space-between;
|
||||
|
||||
h3{
|
||||
color: $light-text-color;
|
||||
}
|
||||
svg {
|
||||
color: rgba(51, 51, 51, 0.34);
|
||||
stroke: rgba(51, 51, 51, 0.44);
|
||||
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
stroke: rgba(51, 51, 51, 0.64);
|
||||
}
|
||||
}
|
||||
.dark-mode &{
|
||||
h3{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
svg{
|
||||
color: rgba(255, 255, 255, 0.34);
|
||||
stroke: rgba(255, 255, 255, 0.44);
|
||||
&:hover {
|
||||
stroke: rgba(255, 255, 255, 0.64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-list-body{
|
||||
display: flex;
|
||||
|
@ -116,9 +136,23 @@ const refresh = () => {
|
|||
gap: $gap;
|
||||
padding: 8px;
|
||||
border-radius: $radius;
|
||||
cursor: pointer;
|
||||
h3,p{
|
||||
color: $light-text-color;
|
||||
}
|
||||
.dark-mode &{
|
||||
h3,p{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background: rgba(51, 51, 51, 0.06);
|
||||
}
|
||||
.dark-mode &{
|
||||
&:hover{
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
.info{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -134,4 +168,11 @@ const refresh = () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
.user-list-item-active{
|
||||
background: rgba(51, 51, 51, 0.2);
|
||||
|
||||
.dark-mode &{
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,6 +9,7 @@ const select = ref(0)
|
|||
export type serverValueItem = {
|
||||
dataName: string,
|
||||
dataType: string,
|
||||
serverId:string
|
||||
}
|
||||
const props = defineProps({
|
||||
closeCallback: {
|
||||
|
|
|
@ -112,6 +112,20 @@ const isOnline = computed(() => {
|
|||
align-items: center;
|
||||
padding: $padding $padding*2;
|
||||
gap: $gap*4;
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
h4,p,svg{
|
||||
color: $light-text-color;
|
||||
stroke: $light-text-color;
|
||||
}
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
h4,p,svg{
|
||||
color: $dark-text-color;
|
||||
stroke: $dark-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-start, .user-end {
|
||||
|
|
|
@ -21,6 +21,12 @@ const fitters = [{
|
|||
}]
|
||||
const selectedFitters = ref([])
|
||||
const visible = ref(false)
|
||||
defineProps({
|
||||
userNumber:{
|
||||
type:Number,
|
||||
default:0
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -44,7 +50,7 @@ const visible = ref(false)
|
|||
</Dialog>
|
||||
<div class="user-num">
|
||||
<Icon :size="30" :stroke-width="1.8" name="User"></Icon>
|
||||
<h3>30</h3>
|
||||
<h3>{{userNumber}}</h3>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<Icon name="UserRoundSearch"></Icon>
|
||||
|
@ -77,6 +83,12 @@ const visible = ref(false)
|
|||
padding: $padding*2;
|
||||
border-radius: $radius;
|
||||
gap: 5rem;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.user-num {
|
||||
|
@ -108,7 +120,10 @@ const visible = ref(false)
|
|||
box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 2px 0 rgba(18, 18, 23, 0.05);
|
||||
color: #64748b;
|
||||
max-width: 1200px;
|
||||
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
border: 1px solid #5a5e60;
|
||||
}
|
||||
input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -148,4 +163,7 @@ const visible = ref(false)
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.p-multiselect{
|
||||
background: unset;
|
||||
}
|
||||
</style>
|
|
@ -41,6 +41,12 @@
|
|||
padding: $padding*1.5 $padding*10 $padding*1.5 $padding*8;
|
||||
gap: $gap*4;
|
||||
border-radius: $radius;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
.dark-mode &{
|
||||
background-color: $dark-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.Indicator-item {
|
||||
|
@ -48,7 +54,14 @@
|
|||
flex-direction: column;
|
||||
gap: $gap*.25;
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
color: $light-text-color;
|
||||
}
|
||||
.dark-mode &{
|
||||
h4 {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
p {
|
||||
color: $light-unfocused-color;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
|||
|
||||
const {$gsap} = useNuxtApp()
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
const Menus = [
|
||||
const Menus =ref([
|
||||
{
|
||||
"label": "系统概览",
|
||||
"icon": "LayoutGrid",
|
||||
|
@ -27,7 +27,18 @@ const Menus = [
|
|||
"icon": "PackageSearch",
|
||||
"route": "/InspectionRecords"
|
||||
},
|
||||
]
|
||||
{
|
||||
"label": "系统设置",
|
||||
"icon": "Settings",
|
||||
"route": "/Settings/PanelSettings"
|
||||
},
|
||||
])
|
||||
const filteredMenus=computed(()=>{
|
||||
return Menus.value.filter(menu=>{
|
||||
const role=mainLayoutStore.UserInfo.Role
|
||||
return menu.label!=="账号列表"&&role!="admin"
|
||||
})
|
||||
})
|
||||
onMounted(() => {
|
||||
const t1 = $gsap.timeline();
|
||||
if (mainLayoutStore.IsLeftSidebarMini) {
|
||||
|
@ -78,6 +89,7 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
rotate: 180,
|
||||
ease: "power2.out",
|
||||
}, 0.5)
|
||||
|
||||
t1.to(".sidebar-layout p,.sidebar-layout h3,.sidebar-layout .aa>svg", {
|
||||
display: "none",
|
||||
duration: 0,
|
||||
|
@ -86,7 +98,6 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
duration: 0,
|
||||
gap: 0
|
||||
}, 0.5)
|
||||
|
||||
} else {
|
||||
t1.to(".sidebar-layout", {
|
||||
duration: 0.5,
|
||||
|
@ -102,6 +113,7 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
rotate: 360,
|
||||
ease: "power2.out",
|
||||
}, 0.5)
|
||||
|
||||
t1.to(".sidebar-layout p,.sidebar-layout h3,.sidebar-layout .aa>svg", {
|
||||
display: "block",
|
||||
duration: 0,
|
||||
|
@ -119,6 +131,18 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
ease: "power2.out",
|
||||
}, 1)
|
||||
})
|
||||
const logout=()=>{
|
||||
$fetch('/Api/Account/Logout', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
navigateTo("/SignIn")
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -128,8 +152,7 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
</div>
|
||||
<div class="header">
|
||||
<div class="user">
|
||||
<!-- <Logo/>-->
|
||||
<NuxtImg src="/Dragon_Head.gif" style="transform: scaleX(-1)" width="48"/>
|
||||
<Logo/>
|
||||
<div class="name">
|
||||
<h3>
|
||||
龙盾云御
|
||||
|
@ -141,14 +164,14 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
<p>搜索</p>
|
||||
</div>
|
||||
<div class="menus">
|
||||
<div v-for="menu in Menus" v-tooltip="menu.label" class="menu-item" @click="navigateTo(menu.route)">
|
||||
<div v-for="menu in filteredMenus" 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">
|
||||
<div class="menu-item" @click="logout">
|
||||
<Icon name="LogOut"/>
|
||||
<p>登出</p>
|
||||
</div>
|
||||
|
@ -174,7 +197,9 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
flex-shrink: 0;
|
||||
background: $light-bg-color;
|
||||
position: relative;
|
||||
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
.dark-mode & {
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
|
@ -187,6 +212,11 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar{
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
|
@ -249,11 +279,12 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*3;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
|
||||
padding: $padding;
|
||||
gap: $gap*2;
|
||||
align-items: center;
|
||||
|
@ -299,7 +330,7 @@ watch(() => mainLayoutStore.IsLeftSidebarMini, (newValue) => {
|
|||
|
||||
.switch-button {
|
||||
position: absolute;
|
||||
top: 2%;
|
||||
top: 25px;
|
||||
right: -15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
<script lang="ts" setup>
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {BellRing, Mail} from 'lucide-vue-next';
|
||||
import SidebarRight from "~/components/SidebarRight.vue";
|
||||
|
||||
const {$gsap} = useNuxtApp()
|
||||
const mainLayoutStore = useMainLayoutStore()
|
||||
import {Mail, BellRing} from 'lucide-vue-next';
|
||||
import SidebarRight from "~/components/SidebarRight.vue";
|
||||
|
||||
const home = ref({
|
||||
icon: 'pi pi-home'
|
||||
});
|
||||
const router = useRouter()
|
||||
const routes=computed(()=>{
|
||||
const route=router.currentRoute.value.name as string
|
||||
return route.split('-').map(x => {
|
||||
return {
|
||||
label: x.charAt(0).toUpperCase() + x.slice(1),
|
||||
icon: 'pi pi-angle'
|
||||
}
|
||||
})
|
||||
})
|
||||
const visibleRight = ref(false)
|
||||
</script>
|
||||
|
||||
|
@ -15,12 +27,11 @@ const visibleRight = ref(false)
|
|||
<SidebarRight/>
|
||||
</Sidebar>
|
||||
<h1 class="name">
|
||||
{{ $router.currentRoute.value.name }}
|
||||
<button @click="req">申请</button>
|
||||
<Breadcrumb :home="home" :model="routes" />
|
||||
</h1>
|
||||
<div class="action">
|
||||
<BellRing @click="visibleRight=true"/>
|
||||
<Mail/>
|
||||
<Icon name="BellRing" @click="visibleRight=true" :stroke-width="1.5" :size="20"/>
|
||||
<Icon name="Mail" :stroke-width="1.5" :size="20"/>
|
||||
</div>
|
||||
<div class="user">
|
||||
<UserMini/>
|
||||
|
@ -40,7 +51,7 @@ const visibleRight = ref(false)
|
|||
align-items: center;
|
||||
padding: 0 $padding*2;
|
||||
gap: $gap*2;
|
||||
|
||||
@include SC_Font();
|
||||
.dark-mode & {
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
|
@ -64,5 +75,24 @@ const visibleRight = ref(false)
|
|||
display: flex;
|
||||
gap: $gap*2;
|
||||
grid-column: 3;
|
||||
svg{
|
||||
cursor: pointer;
|
||||
stroke: rgba(51, 51, 51, 0.5);
|
||||
&:hover{
|
||||
stroke: $light-text-color;
|
||||
}
|
||||
.dark-mode &{
|
||||
stroke: rgba(255, 255, 255, 0.5);
|
||||
&:hover{
|
||||
stroke: $dark-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.p-breadcrumb){
|
||||
background: unset;
|
||||
.p-menuitem-text{
|
||||
color: #D3D3D3;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,352 +1,69 @@
|
|||
<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 class="login-layout">
|
||||
<div class="art"></div>
|
||||
<div class="left-content">
|
||||
<NuxtPage/>
|
||||
</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%;
|
||||
.login-layout{
|
||||
background: white;
|
||||
height: 100%;
|
||||
max-height: 1080px;
|
||||
max-width: 1920px;
|
||||
padding: 100px 200px;
|
||||
overflow: visible;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 32px;
|
||||
gap: 32px;
|
||||
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 {
|
||||
grid-template-columns: 1fr minmax(800px,1fr);
|
||||
grid-template-rows: 1fr;
|
||||
*{
|
||||
@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%); /* 水平居中 */
|
||||
}
|
||||
>div{
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.Body-Hero {
|
||||
grid-area: BodyL;
|
||||
.art{
|
||||
background: url("/bg1.jpg") no-repeat center center / cover;
|
||||
//border: $border;
|
||||
box-shadow: 2px 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.left-content{
|
||||
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;
|
||||
}
|
||||
justify-content: center;
|
||||
.affirm{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 33%;
|
||||
color: #959CB6;
|
||||
text-align: center;
|
||||
font-feature-settings: 'clig' off, 'liga' off;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 16px */
|
||||
letter-spacing: 0.16px;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
@media screen and (max-width: 1200px){
|
||||
.login-layout{
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 200px 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -11,6 +11,7 @@ import {type dataHistoryType, useDataStore} from "~/strores/DataStore";;
|
|||
|
||||
const audio = ref<any>(null);
|
||||
const audio1 = ref<any>(null);
|
||||
const audio2 = ref<any>(null);
|
||||
const toast = useToast()
|
||||
const visible = ref<boolean>(false)
|
||||
const DataStore = useDataStore()
|
||||
|
@ -83,13 +84,13 @@ onMounted(() => {
|
|||
})
|
||||
signalR.connection?.on('ReceiveWaring', (value: string,valueName) => {
|
||||
// //两位小数
|
||||
// audio1.value.currentTime = 0;
|
||||
// audio1.value && audio1.value.click()
|
||||
// audio1.value && audio1.value.play()
|
||||
// toast.info(`你设定的${valueName}已经达到通知阈值,当前值为 ${value}`,{
|
||||
// position: POSITION.BOTTOM_RIGHT,
|
||||
// timeout:5000,
|
||||
// })
|
||||
audio2.value.currentTime = 0;
|
||||
audio2.value && audio2.value.click()
|
||||
audio2.value && audio2.value.play()
|
||||
toast.error(`你设定的${valueName}已经达到警告阈值,当前值为 ${value}`,{
|
||||
position: POSITION.BOTTOM_RIGHT,
|
||||
timeout:5000,
|
||||
})
|
||||
})
|
||||
signalR.connection?.on('ReceiveNotify', (value: string,valueName) => {
|
||||
//两位小数
|
||||
|
@ -104,19 +105,17 @@ onMounted(() => {
|
|||
onError,
|
||||
onClose,
|
||||
} = useWebNotification({
|
||||
title: 'Hello, VueUse world!',
|
||||
title: `你设定的${valueName}已经达到通知阈值,当前值为 ${value}`,
|
||||
dir: 'auto',
|
||||
lang: 'en',
|
||||
renotify: true,
|
||||
tag: 'test',
|
||||
tag: '通知',
|
||||
})
|
||||
if(isSupported.value){
|
||||
Notification.requestPermission().then(res => {
|
||||
//如果允许
|
||||
console.log(res)
|
||||
})
|
||||
show()
|
||||
return
|
||||
}
|
||||
audio1.value && audio1.value.click()
|
||||
audio1.value && audio1.value.play()
|
||||
|
@ -210,6 +209,10 @@ onKeyStroke('Shift', (e) => {
|
|||
<source src="/audios/audio_0a383a4c11.mp3" type="audio/mpeg">
|
||||
您的浏览器不支持 audio 元素。
|
||||
</audio>
|
||||
<audio ref="audio2">
|
||||
<source src="/audios/bibibi.mp3" type="audio/mpeg">
|
||||
您的浏览器不支持 audio 元素。
|
||||
</audio>
|
||||
<SideBar/>
|
||||
<TitleBar/>
|
||||
<Dialog
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"vue-router": "^4.3.2",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vue3-apexcharts": "^1.5.3",
|
||||
"vue3-auth-code-input": "^1.0.10",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
|
|
|
@ -1,255 +0,0 @@
|
|||
<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 "vue-toastification";
|
||||
|
||||
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.success(`登录成功,欢迎回来!${data.data['userName']}`)
|
||||
setTimeout(() => {
|
||||
navigateTo('/Home')
|
||||
}, 1000)
|
||||
} else {
|
||||
toast.error("发生了错误")
|
||||
}
|
||||
})
|
||||
}
|
||||
</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>
|
|
@ -1,459 +0,0 @@
|
|||
<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,175 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
middleware: ['auth']
|
||||
})
|
||||
//导入yup
|
||||
import * as yup from 'yup'
|
||||
import {useToast} from "vue-toastification";
|
||||
import Vcode from 'vue3-puzzle-vcode';
|
||||
//创建表单
|
||||
const form = reactive({
|
||||
currentPassword:'',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
const isShow=ref(false);
|
||||
const toast=useToast()
|
||||
const schema = yup.object().shape({
|
||||
currentPassword:yup.string()
|
||||
.required('密码为必填项')
|
||||
.min(8, '密码至少需要8个字符'),
|
||||
password: yup.string()
|
||||
.required('密码为必填项')
|
||||
.min(8, '密码至少需要8个字符')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, '密码必须包含至少一个小写字母、一个大写字母、一个数字和一个特殊字符(如 @$!%*?&)')
|
||||
.test('no-chinese', '密码不能包含中文字符', (value) => {
|
||||
if (!value) return true; // 如果密码为空,不进行此测试
|
||||
return !/[\u4e00-\u9fa5]/.test(value);
|
||||
}),
|
||||
confirmPassword: yup.string()
|
||||
.required('确认密码为必填项')
|
||||
.oneOf([yup.ref('password')], '两次输入的密码不一致')
|
||||
.min(8, '确认密码至少需要8个字符')
|
||||
.test('no-chinese', '确认密码不能包含中文字符', (value) => {
|
||||
if (!value) return true; // 如果确认密码为空,不进行此测试
|
||||
return !/[\u4e00-\u9fa5]/.test(value);
|
||||
})
|
||||
});
|
||||
//表单提交事件
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await schema.validate(form,{abortEarly: false})
|
||||
// 表单验证通过,处理登录逻辑
|
||||
isShow.value = true;
|
||||
} catch (error:any) {
|
||||
//收集每个错误
|
||||
error.inner.forEach((e: any) => {
|
||||
toast.error(e.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
const onSuccess=()=>{
|
||||
$fetch('/Api/Account/ChangePassword', {
|
||||
method: 'GET',
|
||||
params:{
|
||||
CurrentPassword:form.currentPassword,
|
||||
NewPassword:form.password,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl,
|
||||
}).then((res) => {
|
||||
//清除临时token
|
||||
useCookie('token').value = ""
|
||||
toast.success(res)
|
||||
setTimeout(()=>{
|
||||
navigateTo("/SignIn")
|
||||
},2000)
|
||||
}).finally(()=>{
|
||||
isShow.value=false
|
||||
}).catch(err=>{
|
||||
toast.error(err.response._data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="change-password-layout">
|
||||
<Vcode :show="isShow" @success="onSuccess"/>
|
||||
<NuxtLink href="/signIn">< 返回</NuxtLink>
|
||||
<div class="info">
|
||||
<h1>修改你的密码 👍</h1>
|
||||
<h2>当前账号密码已过期,请重新设置以继续登录</h2>
|
||||
</div>
|
||||
<form class="form-box" @submit.prevent="handleSubmit">
|
||||
<div class="form-item">
|
||||
<label>当前密码</label>
|
||||
<input placeholder="最少8位" required type="password" minlength="8" v-model="form.currentPassword">
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>新的密码</label>
|
||||
<input placeholder="最少8位" required type="password" minlength="8" v-model="form.password">
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>确认密码</label>
|
||||
<input placeholder="最少8位" required type="password" minlength="8" v-model="form.confirmPassword">
|
||||
</div>
|
||||
<button type="submit">提交</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.change-password-layout{
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3,auto);
|
||||
gap: 48px;
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
}
|
||||
.info{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
h1{
|
||||
color: #0C1421;
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
line-height: 100%; /* 36px */
|
||||
letter-spacing: 0.36px;
|
||||
}
|
||||
h2{
|
||||
color: #313957;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 32px */
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
.form-box{
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
.form-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
label{
|
||||
color: #0C1421;
|
||||
font-feature-settings: 'clig' off, 'liga' off;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 16px */
|
||||
letter-spacing: 0.16px;
|
||||
}
|
||||
input{
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #D4D7E3;
|
||||
background: #F7FBFF;
|
||||
}
|
||||
}
|
||||
button{
|
||||
display: flex;
|
||||
padding: 16px 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: #162D3A;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 20px */
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,91 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
})
|
||||
import {useToast} from "vue-toastification";
|
||||
const email=ref(useRoute().params.email)
|
||||
const toast=useToast()
|
||||
const sendEmail=()=>{
|
||||
$fetch('/Api/Account/ForgotPassword', {
|
||||
method:'GET',
|
||||
params:{
|
||||
Email:email.value
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
toast.success(res)
|
||||
}).catch(err=>{
|
||||
toast.error(err.response._data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="forgotPassword-layout">
|
||||
<a href="/signIn">< 返回</a>
|
||||
<div class="info">
|
||||
<h1>忘记密码</h1>
|
||||
<h2>请输入您的邮箱地址,我们将向您发送重置密码的链接。</h2>
|
||||
<h3>请检查你的垃圾箱</h3>
|
||||
</div>
|
||||
<div class="form">
|
||||
<div>
|
||||
<input v-model="email" type="email" placeholder="请输入您的邮箱地址" required minlength="8" />
|
||||
<button @click="sendEmail">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../base";
|
||||
.forgotPassword-layout{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap:48px;
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
.info{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 800;
|
||||
gap: $gap;
|
||||
}
|
||||
input{
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #D4D7E3;
|
||||
background: #F7FBFF;
|
||||
}
|
||||
button{
|
||||
display: flex;
|
||||
padding: 16px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: #162D3A;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 20px */
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.form{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*2;
|
||||
>div{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
input{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -177,6 +177,9 @@ const sortedNetworkList = computed(() => {
|
|||
.network-table {
|
||||
width: 100%;
|
||||
padding: $padding*2;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
@ -199,8 +202,17 @@ const sortedNetworkList = computed(() => {
|
|||
}
|
||||
}
|
||||
}
|
||||
p,td{
|
||||
color: $light-text-color;
|
||||
.dark-mode &{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
tr:hover{
|
||||
background: $light-bg-underline-color;
|
||||
.dark-mode &{
|
||||
background: $dark-bg-underline-color;
|
||||
}
|
||||
}
|
||||
tr>td:first-of-type{
|
||||
display: flex;
|
||||
|
|
|
@ -162,6 +162,9 @@ const killProcess = (pid: string,force:boolean=false) => {
|
|||
.process-table {
|
||||
width: 100%;
|
||||
padding: $padding*2;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
@ -172,6 +175,12 @@ const killProcess = (pid: string,force:boolean=false) => {
|
|||
border-bottom: unset;
|
||||
padding: 8px;
|
||||
}
|
||||
p,td{
|
||||
color: $light-text-color;
|
||||
.dark-mode &{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
th {
|
||||
>div{
|
||||
display: flex;
|
||||
|
@ -186,6 +195,15 @@ const killProcess = (pid: string,force:boolean=false) => {
|
|||
}
|
||||
tr:hover{
|
||||
background: $light-bg-underline-color;
|
||||
.dark-mode &{
|
||||
background: $dark-bg-underline-color;
|
||||
}
|
||||
}
|
||||
td:hover{
|
||||
background:rgba($primary-color,0.2);
|
||||
.dark-mode &{
|
||||
background:#2F3F53;
|
||||
}
|
||||
}
|
||||
tr>td:first-of-type{
|
||||
display: flex;
|
||||
|
@ -205,13 +223,18 @@ const killProcess = (pid: string,force:boolean=false) => {
|
|||
background: unset;
|
||||
border: unset;
|
||||
padding: 0;
|
||||
color:$light-text-color;
|
||||
&:hover{
|
||||
color: $primary-color;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
}
|
||||
&:last-of-type:hover{
|
||||
color: red;
|
||||
}
|
||||
.dark-mode &{
|
||||
color:$dark-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
modal
|
||||
>
|
||||
<template #container="{ closeCallback }">
|
||||
<MarkdownEdit :word-id="selectWord" :preview="isPreview" :template="templateContent"/>
|
||||
<MarkdownEdit :word-id="selectWord" :preview="isPreview" :template="templateContent" :close-call-back="closeCallback"/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
|
@ -143,7 +143,7 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
<p>{{doc.userName}}</p>
|
||||
</div>
|
||||
<div class="action">
|
||||
<button>删除</button>
|
||||
<button v-if="mainLayoutStore.UserInfo.Role==='admin'">删除</button>
|
||||
<button @click="selectWordChange(doc.wordId,true)">查看</button>
|
||||
<button @click="selectWordChange(doc.wordId)">编辑</button>
|
||||
</div>
|
||||
|
@ -169,6 +169,10 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
}
|
||||
.inspection-toolbar{
|
||||
border: $border;
|
||||
background: $light-bg-color;
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
}
|
||||
.toolbar-start{
|
||||
display: flex;
|
||||
|
@ -186,6 +190,12 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
stroke: rgba(51, 51, 51, 0.6);
|
||||
color: rgba(51, 51, 51, 0.6);
|
||||
}
|
||||
.dark-mode &{
|
||||
svg,span{
|
||||
stroke: rgba(200, 200, 200, 0.6);
|
||||
color: rgba(200, 200, 200, 0.6);
|
||||
}
|
||||
}
|
||||
&:hover,*:hover{
|
||||
cursor: pointer;
|
||||
svg,span{
|
||||
|
@ -219,6 +229,28 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
grid-template-columns: 80px 1fr;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: $gap*2;
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
.icon{
|
||||
background: $dark-bg-underline-color;
|
||||
h5{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
.info{
|
||||
p{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
.action{
|
||||
button{
|
||||
color: $dark-unfocused-color;
|
||||
&:hover{
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon{
|
||||
width: 100%;
|
||||
height: min-content;
|
||||
|
@ -230,6 +262,7 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
background: $light-bg-underline-color;
|
||||
border-radius: $radius;
|
||||
h5{
|
||||
color: $light-text-color;
|
||||
text-align: center;
|
||||
padding: 0 $padding*.5;
|
||||
overflow: hidden;
|
||||
|
@ -243,6 +276,9 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-row-gap: $gap*.5;
|
||||
p{
|
||||
color: $light-text-color;
|
||||
}
|
||||
}
|
||||
.action{
|
||||
display: flex;
|
||||
|
@ -254,9 +290,11 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
flex: 1;
|
||||
background: unset;
|
||||
border: unset;
|
||||
color: $light-unfocused-color;
|
||||
border-radius: $radius;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
color: $light-text-color;
|
||||
}
|
||||
button:focus {
|
||||
outline: unset;
|
||||
|
@ -300,5 +338,10 @@ watch(()=>mainLayoutStore.SelectServer.id,()=>{
|
|||
padding: $padding*.5;
|
||||
border-radius: $radius;
|
||||
background: rgba(51, 51, 51, 0.1);
|
||||
.dark-mode &{
|
||||
background: rgba(51, 51, 51, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,91 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
})
|
||||
//接受导航参数
|
||||
import {useToast} from "vue-toastification";
|
||||
const route = useRoute();
|
||||
const email = route.query.email;
|
||||
const token = route.query.token;
|
||||
const toast = useToast();
|
||||
const resetPassword = async () => {
|
||||
$fetch('/Api/Account/ResetPassword', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
Email: email,
|
||||
Token: token
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res => {
|
||||
toast.success(res)
|
||||
setTimeout(() => {
|
||||
navigateTo("/SignIn")
|
||||
}, 2000)
|
||||
}).catch(err => {
|
||||
toast.error(err.response._data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="resetPassword-layout">
|
||||
<div class="intro">
|
||||
<h1>重置密码</h1>
|
||||
<h2>点击下方的确认按钮将为你发送一个新的密码到你的邮箱</h2>
|
||||
</div>
|
||||
<button @click="resetPassword">重置</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.resetPassword-layout{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48px;
|
||||
*{
|
||||
@include SC_Font;
|
||||
|
||||
}
|
||||
}
|
||||
.intro{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
h1{
|
||||
color: #0C1421;
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
line-height: 100%; /* 36px */
|
||||
letter-spacing: 0.36px;
|
||||
}
|
||||
h2{
|
||||
color: #313957;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 32px */
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
button{
|
||||
display: flex;
|
||||
padding: 16px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: #162D3A;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 20px */
|
||||
letter-spacing: 2px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
|
@ -89,6 +89,10 @@ watch(() => mainLayoutStore.SelectServer.id, () => {
|
|||
border-radius: $radius;
|
||||
border: $border;
|
||||
padding: $padding;
|
||||
background: $light-bg-color;
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
}
|
||||
.bottom-box{
|
||||
border-radius: $radius;
|
||||
|
@ -97,8 +101,7 @@ watch(() => mainLayoutStore.SelectServer.id, () => {
|
|||
grid-column: 1/3;
|
||||
min-height: 1000px;
|
||||
}
|
||||
//窗体小于800
|
||||
@media screen and (max-width: 1600px) {
|
||||
@media screen and (max-width: 1800px) {
|
||||
.bottom-box{
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -108,6 +111,9 @@ watch(() => mainLayoutStore.SelectServer.id, () => {
|
|||
border: $border;
|
||||
border-radius: $radius;
|
||||
background: $light-bg-color;
|
||||
.dark-mode &{
|
||||
background: $dark-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -40,6 +40,9 @@ const userId=toRef(route.params.id)
|
|||
grid-template-columns: 1fr 1fr;
|
||||
height: 100%;
|
||||
gap: $gap*2;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
}
|
||||
.cpu-chart {
|
||||
border: 4px solid #0073f4;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'main',
|
||||
middleware: ['auth']
|
||||
})
|
||||
const mainLayoutStore=useMainLayoutStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting-layout">
|
||||
<div class="setting-list">
|
||||
<div @click="navigateTo('/settings/panelSettings')" v-if="mainLayoutStore.UserInfo.Role==='admin'">
|
||||
<Icon name="Settings2"/>
|
||||
<p>平台设置</p>
|
||||
</div>
|
||||
<div >
|
||||
<Icon name="UserRoundCog"/>
|
||||
<p>账号设置</p>
|
||||
</div>
|
||||
<div @click="navigateTo('/settings/alertSettings')">
|
||||
<Icon name="BellElectric"/>
|
||||
<p>告警设置</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-content">
|
||||
<NuxtPage/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "base";
|
||||
.setting-layout{
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
min-height: 800px;
|
||||
width: 100%;
|
||||
gap: $gap*2;
|
||||
}
|
||||
.setting-list,.setting-content{
|
||||
background: $light-bg-color;
|
||||
border: $border;
|
||||
border-radius: $radius;
|
||||
}
|
||||
.setting-list{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
padding: $padding;
|
||||
div{
|
||||
display: flex;
|
||||
padding: $padding;
|
||||
gap: $gap;
|
||||
cursor: pointer;
|
||||
&:hover{
|
||||
background: $light-bg-underline-color;
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,290 @@
|
|||
<script lang="ts" setup>
|
||||
import type {serverValueItem} from "~/components/SettingCard.vue";
|
||||
import {useMainLayoutStore} from "~/strores/UseMainLayoutStore";
|
||||
import {useToast} from "vue-toastification";
|
||||
|
||||
type alertConfigs={
|
||||
"alertType": string,
|
||||
"serverId": string,
|
||||
"notify": string,
|
||||
"warning": string,
|
||||
"typeName": string,
|
||||
"description": string
|
||||
}
|
||||
const mainLayoutStore=useMainLayoutStore()
|
||||
const toast=useToast()
|
||||
const alertConfig = ref<alertConfigs[]>([])
|
||||
const ServerValues = ref<serverValueItem[]>([])
|
||||
const selectServer=ref<string>("")
|
||||
const serverIds = ref<string[]>([])
|
||||
const selectedDataName = ref<string>("")
|
||||
const Description = ref<string>()
|
||||
const Notify = ref<string>("50")
|
||||
const Warning = ref<string>("80")
|
||||
onMounted(()=>{
|
||||
config()
|
||||
})
|
||||
const config=()=>{
|
||||
$fetch('/Api/Config/GetAlertConfig', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res => {
|
||||
alertConfig.value = res as alertConfigs[]
|
||||
})
|
||||
}
|
||||
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 => {
|
||||
ServerValues.value = res as serverValueItem[];
|
||||
ServerValues.value.sort((a, b) => {
|
||||
return a.dataName.localeCompare(b.dataName);
|
||||
})
|
||||
//提取serverValues的唯一serverId
|
||||
serverIds.value = ServerValues.value.map(item => item.serverId)
|
||||
serverIds.value=[...new Set(serverIds.value)]
|
||||
selectServer.value =serverIds.value[0]
|
||||
selectedDataName.value=ServerValues.value.filter(x=>x.serverId===selectServer.value)[0].dataType
|
||||
})
|
||||
})
|
||||
const addAlertVisible= ref(false)
|
||||
watch(()=>selectServer.value,(newValue)=>{
|
||||
selectedDataName.value=ServerValues.value.filter(x=>x.serverId===newValue)[0].dataType
|
||||
})
|
||||
const saveAlertVisible=()=>{
|
||||
|
||||
const data={
|
||||
ServerId:selectServer.value,
|
||||
DataType:selectedDataName.value,
|
||||
Description:Description.value,
|
||||
Notify:Notify.value,
|
||||
Warning:Warning.value,
|
||||
DataName:ServerValues.value.filter(x=>x.dataType===selectedDataName.value)[0].dataName
|
||||
}
|
||||
const isEmpty = (value:any) => value === null || value === undefined || value === '';
|
||||
if (!Object.values(data).every(value => !isEmpty(value))) {
|
||||
toast.error("请填写完整")
|
||||
return
|
||||
}
|
||||
$fetch('/Api/Config/AddAlertConfig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
body: data,
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res => {
|
||||
console.log(res);
|
||||
toast.success("创建成功 "+res)
|
||||
setTimeout(()=>{
|
||||
addAlertVisible.value=false
|
||||
config()
|
||||
},2000)
|
||||
}).catch(err=>{
|
||||
toast.error("创建失败"+err)
|
||||
})
|
||||
}
|
||||
const deleteAlert=(alert:alertConfigs)=>{
|
||||
$fetch('/Api/Config/DeleteAlertConfig', {
|
||||
method:'DELETE',
|
||||
params:{
|
||||
DataType:alert.alertType,
|
||||
ServerId:alert.serverId
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': "Bearer " + useCookie('token').value
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
toast.success(res)
|
||||
setTimeout(()=>{ config()},1000)
|
||||
}).catch(err=>{
|
||||
toast.error(err)
|
||||
})
|
||||
}
|
||||
const confirm = useConfirm();
|
||||
const confirm2 = (alert:alertConfigs) => {
|
||||
confirm.require({
|
||||
message: '你真的打算删除这个警告么?',
|
||||
header: '警告',
|
||||
rejectLabel: 'Cancel',
|
||||
accept: () => {
|
||||
deleteAlert(alert)
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="alert-setting-layout">
|
||||
<ConfirmDialog></ConfirmDialog>
|
||||
<Dialog
|
||||
v-model:visible="addAlertVisible"
|
||||
:pt="{
|
||||
root: {
|
||||
style:'border:unset;background-color:unset;'
|
||||
},
|
||||
mask: {
|
||||
style: 'backdrop-filter: blur(20px)'
|
||||
}
|
||||
}"
|
||||
modal
|
||||
>
|
||||
<div class="add-alert-box">
|
||||
<h3>新的告警</h3>
|
||||
<div class="form-item">
|
||||
<label>监控对象</label>
|
||||
<select v-model="selectedDataName">
|
||||
<option v-for="item in ServerValues.filter(x=>x.serverId===selectServer)" :value="item.dataType">{{item.dataName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>监控服务器</label>
|
||||
<select v-model="selectServer">
|
||||
<option v-for="item in serverIds">{{item}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>告警描述</label>
|
||||
<input type="text" placeholder="告警描述" v-model="Description" required/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>通知阈值</label>
|
||||
<input type="number" placeholder="通知阈值" v-model="Notify" required/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>报警阈值</label>
|
||||
<input type="number" placeholder="报警阈值" v-model="Warning" required/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<button @click="saveAlertVisible">保存</button>
|
||||
<button @click="addAlertVisible=false">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<div class="tool-bar">
|
||||
<h3>告警设置</h3>
|
||||
<div class="actions">
|
||||
<button @click="config">
|
||||
<Icon name="RefreshCw" />
|
||||
<p>刷新</p>
|
||||
</button>
|
||||
<button>
|
||||
<Icon name="RotateCcw"/>
|
||||
<p>重载配置</p>
|
||||
</button>
|
||||
<button @click="addAlertVisible=true">
|
||||
<Icon name="Plus"/>
|
||||
<p>新的告警</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-configs-box">
|
||||
<div v-for="alert in alertConfig" class="alert-item">
|
||||
<Icon name="TriangleAlert" :stroke-width="2" :size="45"/>
|
||||
<div>
|
||||
<p>{{alert.typeName}}</p>
|
||||
<p>介绍:{{alert.description}}</p>
|
||||
<p>监控服务器:{{alert.serverId}}</p>
|
||||
<p>通知阈值:{{alert.notify}}</p>
|
||||
<p>警告阈值:{{alert.warning}}</p>
|
||||
</div>
|
||||
<Icon name="X" :stroke-width="2" @click="confirm2(alert)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "base";
|
||||
|
||||
.alert-setting-layout {
|
||||
display: flex;
|
||||
padding: $padding;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
* {
|
||||
@include SC_Font
|
||||
}
|
||||
}
|
||||
|
||||
.tool-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.actions{
|
||||
display: flex;
|
||||
gap: $gap*1.5;
|
||||
align-items: center;
|
||||
}
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: $border;
|
||||
gap: $gap;
|
||||
padding: $padding*.3 $padding*.5;
|
||||
border-radius: $radius*.5;
|
||||
background: $primary-color;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.alert-configs-box{
|
||||
display: flex;
|
||||
padding: $padding $padding*.5;
|
||||
gap: $gap*2;
|
||||
.alert-item{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
padding: $padding;
|
||||
background: $light-bg-underline-color;
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
|
||||
}
|
||||
}
|
||||
.add-alert-box{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
.form-item{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
align-items: center;
|
||||
label{
|
||||
width: 120px;
|
||||
}
|
||||
input,select{
|
||||
flex: 1;
|
||||
border: $border;
|
||||
background: $light-bg-underline-color;
|
||||
padding: $padding*.5;
|
||||
border-radius: $radius;
|
||||
}
|
||||
button{
|
||||
padding: $padding*.5 $padding;
|
||||
border-radius: $radius;
|
||||
border: $border;
|
||||
margin-top: 20px;
|
||||
&:first-of-type{
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtPage/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
|
@ -0,0 +1,243 @@
|
|||
<script lang="ts" setup>
|
||||
import Vcode from 'vue3-puzzle-vcode';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
})
|
||||
import * as Yup from 'yup';
|
||||
import {useToast} from "vue-toastification";
|
||||
|
||||
const toast = useToast()
|
||||
const errors = ref<string[]>([]);
|
||||
const isShow = ref(false);
|
||||
|
||||
//表单存储对象
|
||||
const form = reactive({
|
||||
emailOrUserName: '',
|
||||
password: '',
|
||||
});
|
||||
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 any;
|
||||
useCookie('token').value =data.token;
|
||||
toast.success(`欢迎回来 ${data.nickName}!`)
|
||||
setTimeout(()=>{
|
||||
navigateTo("/home")
|
||||
},2000)
|
||||
}).catch(err=>{
|
||||
if(err.response.status===401){
|
||||
toast.error('登录失败,邮箱未验证')
|
||||
const data=err.response._data
|
||||
console.log(data)
|
||||
setTimeout(()=>{
|
||||
navigateTo(`/verifyEmail/${data.id}_${data.email}`)
|
||||
},2000)
|
||||
return
|
||||
}
|
||||
if(err.response.status===402){
|
||||
toast.error('登录失败,密码过期,请修改密码')
|
||||
const data=err.response._data
|
||||
useCookie('token').value =data.token;
|
||||
setTimeout(()=>{
|
||||
navigateTo(`/changePassword/${data.id}`)
|
||||
},2000)
|
||||
return
|
||||
}
|
||||
toast.error('登录失败,'+err.response._data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="SignIn-Box">
|
||||
<Vcode :show="isShow" @success="onSuccess"/>
|
||||
<div class="intro">
|
||||
<h1>欢迎回来 👋</h1>
|
||||
<h2>今天是新的一天。这是你的一天。你塑造它。
|
||||
登录以开始管理您的项目。</h2>
|
||||
</div>
|
||||
<form class="login-form" @submit.prevent="handleSubmit">
|
||||
<div class="form-item">
|
||||
<label>电子邮件/账号</label>
|
||||
<input required type="text" v-model="form.emailOrUserName" placeholder="Example@email.com">
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>密码</label>
|
||||
<input required minlength="8" v-model="form.password" type="password" placeholder="至少 8 个字符">
|
||||
</div>
|
||||
<a :href="'/forgotPassword/'+form.emailOrUserName">忘记密码</a>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
<div class="social-sign-in">
|
||||
<div class="or">
|
||||
<hr/>
|
||||
<span>or</span>
|
||||
<hr/>
|
||||
</div>
|
||||
<div class="social-button">
|
||||
<button>
|
||||
<NuxtImg src="/Google.svg"></NuxtImg>
|
||||
<p>使用 Google 帐号登录</p>
|
||||
</button>
|
||||
<button>
|
||||
<NuxtImg src="/Facebook.svg"></NuxtImg>
|
||||
<p>使用 Facebook 登录</p></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "base";
|
||||
|
||||
.SignIn-Box {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3,auto);
|
||||
width: 400px;
|
||||
gap: 48px;
|
||||
}
|
||||
@media screen and (max-width: 400px){
|
||||
.SignIn-Box{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.intro{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
h1{
|
||||
color: #0C1421;
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
line-height: 100%; /* 36px */
|
||||
letter-spacing: 0.36px;
|
||||
}
|
||||
h2{
|
||||
color: #313957;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 160%; /* 32px */
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
.login-form{
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
.form-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
label{
|
||||
color: #0C1421;
|
||||
font-feature-settings: 'clig' off, 'liga' off;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 16px */
|
||||
letter-spacing: 0.16px;
|
||||
}
|
||||
input{
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #D4D7E3;
|
||||
background: #F7FBFF;
|
||||
}
|
||||
}
|
||||
a:first-of-type{
|
||||
align-self: end;
|
||||
color: #1E4AE9;
|
||||
font-feature-settings: 'clig' off, 'liga' off;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 16px */
|
||||
letter-spacing: 0.16px;
|
||||
}
|
||||
button{
|
||||
display: flex;
|
||||
padding: 16px 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: #162D3A;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 20px */
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
.social-sign-in{
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
.or{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
hr{
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
color: #CFDFE2;
|
||||
}
|
||||
}
|
||||
.social-button{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
button{
|
||||
height: 52px;
|
||||
display: flex;
|
||||
padding: 12px 9px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border: unset;
|
||||
align-self: stretch;
|
||||
border-radius: 12px;
|
||||
background: #F3F9FA;
|
||||
color: #313957;
|
||||
font-feature-settings: 'clig' off, 'liga' off;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
letter-spacing: 0.16px;
|
||||
svg{
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -22,7 +22,7 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<div class="user-layout">
|
||||
<UserPageBar/>
|
||||
<UserPageBar :userNumber="userList?.length??0"/>
|
||||
<UserPageIndicator/>
|
||||
<div class="user-content">
|
||||
<UserItem v-for="user in userList" :avatar="user.avatar" :create-time="user.createDate"
|
||||
|
@ -49,6 +49,9 @@ onMounted(() => {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap*2;
|
||||
*{
|
||||
@include SC_Font;
|
||||
}
|
||||
}
|
||||
|
||||
.user-content {
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<script setup lang="ts">
|
||||
import _ from "lodash";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
})
|
||||
import { SmsCode, type ISmsCodeComponentInstance } from 'vue3-auth-code-input';
|
||||
import {useToast} from "vue-toastification";
|
||||
|
||||
const visible= ref(false)
|
||||
const email = ref(useRoute().params.email)
|
||||
const userId = ref(useRoute().params.id)
|
||||
const smsCodeRef = ref<ISmsCodeComponentInstance | null>(null);
|
||||
const toast = useToast()
|
||||
const handelSubmit=()=>{
|
||||
visible.value=true
|
||||
}
|
||||
const sendCode = (email:string) => {
|
||||
$fetch('/Api/Account/VerifyEmail', {
|
||||
method:'GET',
|
||||
params:{
|
||||
UserId:userId.value,
|
||||
Email:email
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
toast.success(res)
|
||||
}).catch(err=>{
|
||||
toast.error(err.response._data)
|
||||
})
|
||||
};
|
||||
//节流
|
||||
const verifyCode=_.debounce((code:string)=>{
|
||||
if(code.length!==6){
|
||||
return
|
||||
}
|
||||
$fetch('/Api/Account/VerifyEmail', {
|
||||
method:'GET',
|
||||
params:{
|
||||
UserId:userId.value,
|
||||
Email:email.value,
|
||||
Code:code
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL: useRuntimeConfig().public.baseUrl
|
||||
}).then(res=>{
|
||||
toast.success(res)
|
||||
toast.info("你的账号信息已被更新")
|
||||
toast.info("重新登录...")
|
||||
setTimeout(()=>{
|
||||
navigateTo("/SignIn")
|
||||
},2000)
|
||||
}).catch(err=>{
|
||||
toast.error(err.response._data)
|
||||
})
|
||||
},1000)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="verify-email-layout">
|
||||
<Dialog v-model:visible="visible"
|
||||
:pt="{
|
||||
root: {
|
||||
style:'border:unset;background-color:unset;'
|
||||
},
|
||||
mask: {
|
||||
style: 'backdrop-filter: blur(20px)'
|
||||
}
|
||||
}"
|
||||
modal>
|
||||
<template #container="{ closeCallback }">
|
||||
<sms-code ref="smsCodeRef" title="验证你的邮箱" card width="400px" codeHeight="50px" font-size="20" content-text="请获取验证码后填写邮箱验证码。" type="text" :mobile="email as string" @send="sendCode" @update:code="verifyCode" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<div class="info">
|
||||
<h1>验证邮箱</h1>
|
||||
<h2>验证你的邮箱以继续登录</h2>
|
||||
</div>
|
||||
<form @submit.prevent="handelSubmit">
|
||||
<input type="email" v-model="email" placeholder="请输入你的邮箱" required />
|
||||
<button type="submit">提交</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../base";
|
||||
.verify-email-layout{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
*{
|
||||
@include SC_Font
|
||||
}
|
||||
}
|
||||
.info{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
h1{
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
h2{
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
form{
|
||||
display: flex;
|
||||
gap: $gap;
|
||||
input{
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #D4D7E3;
|
||||
background: #eaeaea;
|
||||
|
||||
}
|
||||
button{
|
||||
border: $border;
|
||||
padding: $padding;
|
||||
border-radius: $radius;
|
||||
background: $primary-color;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
BIN
web/public/1.jpg
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 2.8 MiB |
Before Width: | Height: | Size: 162 KiB |
|
@ -1,10 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="6" fill="url(#paint0_linear_6_67)"/>
|
||||
<path d="M15 9.00002L9 15M8.99997 9L14.9999 15" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6_67" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E88B76"/>
|
||||
<stop offset="1" stop-color="#CA5048"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 485 B |
|
@ -1,19 +1,11 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Facebook" filter="url(#filter0_d_1820_6787)">
|
||||
<g id="Facebook_2">
|
||||
<path id="XMLID 21" d="M32.0021 15.9992C32.0021 9.37225 26.6299 4 20.002 4C13.3744 4 8.00195 9.37225 8.00195 15.9992C8.00195 22.6262 13.3744 28 20.002 28C26.63 28 32.0021 22.6262 32.0021 15.9992Z" fill="#395196"/>
|
||||
<path id="XMLID 19" d="M21.0595 13.158V12.0213C21.0595 11.4677 21.429 11.3383 21.6876 11.3383C21.9472 11.3383 23.283 11.3383 23.283 11.3383V8.90422L21.0866 8.89453C18.6486 8.89453 18.095 10.711 18.095 11.8739V13.158H16.6855V16.0014H18.1072C18.1072 19.2233 18.1072 23.1082 18.1072 23.1082H20.9497C20.9497 23.1082 20.9497 19.1837 20.9497 16.0014H23.0595L23.319 13.158H21.0595Z" fill="white"/>
|
||||
</g>
|
||||
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Facebook " clip-path="url(#clip0_202_115)">
|
||||
<path id="Vector" d="M28.5 14.5C28.5 6.76801 22.232 0.5 14.5 0.5C6.76801 0.5 0.5 6.76801 0.5 14.5C0.5 21.4877 5.61957 27.2796 12.3125 28.3299V18.5469H8.75781V14.5H12.3125V11.4156C12.3125 7.90687 14.4027 5.96875 17.6005 5.96875C19.1318 5.96875 20.7344 6.24219 20.7344 6.24219V9.6875H18.9691C17.23 9.6875 16.6875 10.7668 16.6875 11.875V14.5H20.5703L19.9496 18.5469H16.6875V28.3299C23.3804 27.2796 28.5 21.4877 28.5 14.5Z" fill="#1877F2"/>
|
||||
<path id="Vector_2" d="M19.9496 18.5469L20.5703 14.5H16.6875V11.875C16.6875 10.7679 17.23 9.6875 18.9691 9.6875H20.7344V6.24219C20.7344 6.24219 19.1323 5.96875 17.6005 5.96875C14.4027 5.96875 12.3125 7.90688 12.3125 11.4156V14.5H8.75781V18.5469H12.3125V28.3299C13.762 28.5567 15.238 28.5567 16.6875 28.3299V18.5469H19.9496Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1820_6787" x="0.00195312" y="0" width="40" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0627451 0 0 0 0 0.160784 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1820_6787"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1820_6787" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_202_115">
|
||||
<rect width="28" height="28" fill="white" transform="translate(0.5 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.0 KiB |
|
@ -1,80 +0,0 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:800);
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#world {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#instructions {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
margin: auto;
|
||||
margin-top: 120px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: #653f4c;
|
||||
font-size: .9em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.lightInstructions {
|
||||
color: #993f4c;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#credits {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
bottom: 0;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: #b297a2;
|
||||
font-size: 0.7em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#credits a {
|
||||
color: #b297a2;
|
||||
}
|
||||
|
||||
#credits .society6 {
|
||||
color: #993f4c;
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
right: 35px;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
color: black;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.page-footer a {
|
||||
display: flex;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.touxiang {
|
||||
bottom: 0px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>海拥 | 爱吹风的小狮子</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
|
||||
<div id="world"></div>
|
||||
|
||||
<link href="css/style.css" rel="stylesheet"></link>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.min.js"></script>
|
||||
|
||||
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/OrbitControls.js"></script>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
|
@ -1,730 +0,0 @@
|
|||
//THREEJS RELATED VARIABLES
|
||||
|
||||
var scene,
|
||||
camera,
|
||||
controls,
|
||||
fieldOfView,
|
||||
aspectRatio,
|
||||
nearPlane,
|
||||
farPlane,
|
||||
shadowLight,
|
||||
backLight,
|
||||
light,
|
||||
renderer,
|
||||
container;
|
||||
|
||||
//SCENE
|
||||
var floor, lion, fan,
|
||||
isBlowing = false;
|
||||
|
||||
//SCREEN VARIABLES
|
||||
|
||||
var HEIGHT,
|
||||
WIDTH,
|
||||
windowHalfX,
|
||||
windowHalfY,
|
||||
mousePos = {x: 0, y: 0};
|
||||
dist = 0;
|
||||
|
||||
//INIT THREE JS, SCREEN AND MOUSE EVENTS
|
||||
|
||||
function init() {
|
||||
scene = new THREE.Scene();
|
||||
HEIGHT = window.innerHeight;
|
||||
WIDTH = window.innerWidth;
|
||||
aspectRatio = WIDTH / HEIGHT;
|
||||
fieldOfView = 60;
|
||||
nearPlane = 1;
|
||||
farPlane = 2000;
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
fieldOfView,
|
||||
aspectRatio,
|
||||
nearPlane,
|
||||
farPlane);
|
||||
camera.position.z = 800;
|
||||
camera.position.y = 0;
|
||||
camera.lookAt(new THREE.Vector3(0, 0, 0));
|
||||
renderer = new THREE.WebGLRenderer({alpha: true, antialias: true});
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(WIDTH, HEIGHT);
|
||||
renderer.shadowMapEnabled = true;
|
||||
container = document.getElementById('world');
|
||||
container.appendChild(renderer.domElement);
|
||||
windowHalfX = WIDTH / 2;
|
||||
windowHalfY = HEIGHT / 2;
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
document.addEventListener('mousemove', handleMouseMove, false);
|
||||
document.addEventListener('mousedown', handleMouseDown, false);
|
||||
document.addEventListener('mouseup', handleMouseUp, false);
|
||||
document.addEventListener('touchstart', handleTouchStart, false);
|
||||
document.addEventListener('touchend', handleTouchEnd, false);
|
||||
document.addEventListener('touchmove', handleTouchMove, false);
|
||||
/*
|
||||
controls = new THREE.OrbitControls( camera, renderer.domElement);
|
||||
//*/
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
HEIGHT = window.innerHeight;
|
||||
WIDTH = window.innerWidth;
|
||||
windowHalfX = WIDTH / 2;
|
||||
windowHalfY = HEIGHT / 2;
|
||||
renderer.setSize(WIDTH, HEIGHT);
|
||||
camera.aspect = WIDTH / HEIGHT;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
function handleMouseMove(event) {
|
||||
mousePos = {x: event.clientX, y: event.clientY};
|
||||
}
|
||||
|
||||
function handleMouseDown(event) {
|
||||
isBlowing = true;
|
||||
}
|
||||
|
||||
function handleMouseUp(event) {
|
||||
isBlowing = false;
|
||||
}
|
||||
|
||||
function handleTouchStart(event) {
|
||||
if (event.touches.length > 1) {
|
||||
event.preventDefault();
|
||||
mousePos = {x: event.touches[0].pageX, y: event.touches[0].pageY};
|
||||
isBlowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd(event) {
|
||||
//mousePos = {x:windowHalfX, y:windowHalfY};
|
||||
isBlowing = false;
|
||||
}
|
||||
|
||||
function handleTouchMove(event) {
|
||||
if (event.touches.length == 1) {
|
||||
event.preventDefault();
|
||||
mousePos = {x: event.touches[0].pageX, y: event.touches[0].pageY};
|
||||
isBlowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
function createLights() {
|
||||
light = new THREE.HemisphereLight(0xffffff, 0xffffff, .5)
|
||||
|
||||
shadowLight = new THREE.DirectionalLight(0xffffff, .8);
|
||||
shadowLight.position.set(200, 200, 200);
|
||||
shadowLight.castShadow = true;
|
||||
shadowLight.shadowDarkness = .2;
|
||||
|
||||
backLight = new THREE.DirectionalLight(0xffffff, .4);
|
||||
backLight.position.set(-100, 200, 50);
|
||||
backLight.shadowDarkness = .1;
|
||||
backLight.castShadow = true;
|
||||
|
||||
scene.add(backLight);
|
||||
scene.add(light);
|
||||
scene.add(shadowLight);
|
||||
}
|
||||
|
||||
function createFloor() {
|
||||
floor = new THREE.Mesh(new THREE.PlaneBufferGeometry(1000, 500), new THREE.MeshBasicMaterial({
|
||||
color: 0xebe5e7,
|
||||
opacity: 0,
|
||||
transparent: true
|
||||
}));
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.y = -100;
|
||||
floor.receiveShadow = true;
|
||||
scene.add(floor);
|
||||
}
|
||||
|
||||
function createLion() {
|
||||
lion = new Lion();
|
||||
scene.add(lion.threegroup);
|
||||
}
|
||||
|
||||
function createFan() {
|
||||
fan = new Fan();
|
||||
fan.threegroup.position.z = 350;
|
||||
scene.add(fan.threegroup);
|
||||
}
|
||||
|
||||
Fan = function () {
|
||||
this.isBlowing = false;
|
||||
this.speed = 0;
|
||||
this.acc = 0;
|
||||
this.redMat = new THREE.MeshLambertMaterial({
|
||||
color: 0xad3525,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
this.greyMat = new THREE.MeshLambertMaterial({
|
||||
color: 0x653f4c,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
this.yellowMat = new THREE.MeshLambertMaterial({
|
||||
color: 0xfdd276,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
var coreGeom = new THREE.BoxGeometry(10, 10, 20);
|
||||
var sphereGeom = new THREE.BoxGeometry(10, 10, 3);
|
||||
var propGeom = new THREE.BoxGeometry(10, 30, 2);
|
||||
propGeom.applyMatrix(new THREE.Matrix4().makeTranslation(0, 25, 0));
|
||||
|
||||
this.core = new THREE.Mesh(coreGeom, this.greyMat);
|
||||
|
||||
// propellers
|
||||
var prop1 = new THREE.Mesh(propGeom, this.redMat);
|
||||
prop1.position.z = 15;
|
||||
var prop2 = prop1.clone();
|
||||
prop2.rotation.z = Math.PI / 2;
|
||||
var prop3 = prop1.clone();
|
||||
prop3.rotation.z = Math.PI;
|
||||
var prop4 = prop1.clone();
|
||||
prop4.rotation.z = -Math.PI / 2;
|
||||
|
||||
this.sphere = new THREE.Mesh(sphereGeom, this.yellowMat);
|
||||
this.sphere.position.z = 15;
|
||||
|
||||
this.propeller = new THREE.Group();
|
||||
this.propeller.add(prop1);
|
||||
this.propeller.add(prop2);
|
||||
this.propeller.add(prop3);
|
||||
this.propeller.add(prop4);
|
||||
|
||||
this.threegroup = new THREE.Group();
|
||||
this.threegroup.add(this.core);
|
||||
this.threegroup.add(this.propeller);
|
||||
this.threegroup.add(this.sphere);
|
||||
}
|
||||
|
||||
Fan.prototype.update = function (xTarget, yTarget) {
|
||||
this.threegroup.lookAt(new THREE.Vector3(0, 80, 60));
|
||||
this.tPosX = rule3(xTarget, -200, 200, -250, 250);
|
||||
this.tPosY = rule3(yTarget, -200, 200, 250, -250);
|
||||
|
||||
this.threegroup.position.x += (this.tPosX - this.threegroup.position.x) / 10;
|
||||
this.threegroup.position.y += (this.tPosY - this.threegroup.position.y) / 10;
|
||||
|
||||
this.targetSpeed = (this.isBlowing) ? .3 : .01;
|
||||
if (this.isBlowing && this.speed < .5) {
|
||||
this.acc += .001;
|
||||
this.speed += this.acc;
|
||||
} else if (!this.isBlowing) {
|
||||
this.acc = 0;
|
||||
this.speed *= .98;
|
||||
}
|
||||
this.propeller.rotation.z += this.speed;
|
||||
}
|
||||
|
||||
Lion = function () {
|
||||
this.windTime = 0;
|
||||
this.bodyInitPositions = [];
|
||||
this.maneParts = [];
|
||||
this.threegroup = new THREE.Group();
|
||||
this.yellowMat = new THREE.MeshLambertMaterial({
|
||||
color: 0xfdd276,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
this.redMat = new THREE.MeshLambertMaterial({
|
||||
color: 0xad3525,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
this.pinkMat = new THREE.MeshLambertMaterial({
|
||||
color: 0xe55d2b,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
this.whiteMat = new THREE.MeshLambertMaterial({
|
||||
color: 0xffffff,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
this.purpleMat = new THREE.MeshLambertMaterial({
|
||||
color: 0x451954,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
this.greyMat = new THREE.MeshLambertMaterial({
|
||||
color: 0x653f4c,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
this.blackMat = new THREE.MeshLambertMaterial({
|
||||
color: 0x302925,
|
||||
shading: THREE.FlatShading
|
||||
});
|
||||
|
||||
|
||||
var bodyGeom = new THREE.CylinderGeometry(30, 80, 140, 4);
|
||||
var maneGeom = new THREE.BoxGeometry(40, 40, 15);
|
||||
var faceGeom = new THREE.BoxGeometry(80, 80, 80);
|
||||
var spotGeom = new THREE.BoxGeometry(4, 4, 4);
|
||||
var mustacheGeom = new THREE.BoxGeometry(30, 2, 1);
|
||||
mustacheGeom.applyMatrix(new THREE.Matrix4().makeTranslation(15, 0, 0));
|
||||
|
||||
var earGeom = new THREE.BoxGeometry(20, 20, 20);
|
||||
var noseGeom = new THREE.BoxGeometry(40, 40, 20);
|
||||
var eyeGeom = new THREE.BoxGeometry(5, 30, 30);
|
||||
var irisGeom = new THREE.BoxGeometry(4, 10, 10);
|
||||
var mouthGeom = new THREE.BoxGeometry(20, 20, 10);
|
||||
var smileGeom = new THREE.TorusGeometry(12, 4, 2, 10, Math.PI);
|
||||
var lipsGeom = new THREE.BoxGeometry(40, 15, 20);
|
||||
var kneeGeom = new THREE.BoxGeometry(25, 80, 80);
|
||||
kneeGeom.applyMatrix(new THREE.Matrix4().makeTranslation(0, 50, 0));
|
||||
var footGeom = new THREE.BoxGeometry(40, 20, 20);
|
||||
|
||||
// body
|
||||
this.body = new THREE.Mesh(bodyGeom, this.yellowMat);
|
||||
this.body.position.z = -60;
|
||||
this.body.position.y = -30;
|
||||
this.bodyVertices = [0, 1, 2, 3, 4, 10];
|
||||
|
||||
for (var i = 0; i < this.bodyVertices.length; i++) {
|
||||
var tv = this.body.geometry.vertices[this.bodyVertices[i]];
|
||||
tv.z = 70;
|
||||
//tv.x = 0;
|
||||
this.bodyInitPositions.push({x: tv.x, y: tv.y, z: tv.z});
|
||||
}
|
||||
|
||||
// knee
|
||||
this.leftKnee = new THREE.Mesh(kneeGeom, this.yellowMat);
|
||||
this.leftKnee.position.x = 65;
|
||||
this.leftKnee.position.z = -20;
|
||||
this.leftKnee.position.y = -110;
|
||||
this.leftKnee.rotation.z = -.3;
|
||||
|
||||
this.rightKnee = new THREE.Mesh(kneeGeom, this.yellowMat);
|
||||
this.rightKnee.position.x = -65;
|
||||
this.rightKnee.position.z = -20;
|
||||
this.rightKnee.position.y = -110;
|
||||
this.rightKnee.rotation.z = .3;
|
||||
|
||||
// feet
|
||||
this.backLeftFoot = new THREE.Mesh(footGeom, this.yellowMat);
|
||||
this.backLeftFoot.position.z = 30;
|
||||
this.backLeftFoot.position.x = 75;
|
||||
this.backLeftFoot.position.y = -90;
|
||||
|
||||
this.backRightFoot = new THREE.Mesh(footGeom, this.yellowMat);
|
||||
this.backRightFoot.position.z = 30;
|
||||
this.backRightFoot.position.x = -75;
|
||||
this.backRightFoot.position.y = -90;
|
||||
|
||||
this.frontRightFoot = new THREE.Mesh(footGeom, this.yellowMat);
|
||||
this.frontRightFoot.position.z = 40;
|
||||
this.frontRightFoot.position.x = -22;
|
||||
this.frontRightFoot.position.y = -90;
|
||||
|
||||
this.frontLeftFoot = new THREE.Mesh(footGeom, this.yellowMat);
|
||||
this.frontLeftFoot.position.z = 40;
|
||||
this.frontLeftFoot.position.x = 22;
|
||||
this.frontLeftFoot.position.y = -90;
|
||||
|
||||
// mane
|
||||
|
||||
this.mane = new THREE.Group();
|
||||
|
||||
for (var j = 0; j < 4; j++) {
|
||||
for (var k = 0; k < 4; k++) {
|
||||
var manePart = new THREE.Mesh(maneGeom, this.redMat);
|
||||
manePart.position.x = (j * 40) - 60;
|
||||
manePart.position.y = (k * 40) - 60;
|
||||
|
||||
var amp;
|
||||
var zOffset;
|
||||
var periodOffset = Math.random() * Math.PI * 2;
|
||||
var angleOffsetY, angleOffsetX;
|
||||
var angleAmpY, angleAmpX;
|
||||
var xInit, yInit;
|
||||
|
||||
|
||||
if ((j == 0 && k == 0) || (j == 0 && k == 3) || (j == 3 && k == 0) || (j == 3 && k == 3)) {
|
||||
amp = -10 - Math.floor(Math.random() * 5);
|
||||
zOffset = -5;
|
||||
} else if (j == 0 || k == 0 || j == 3 || k == 3) {
|
||||
amp = -5 - Math.floor(Math.random() * 5);
|
||||
zOffset = 0;
|
||||
} else {
|
||||
amp = 0;
|
||||
zOffset = 0;
|
||||
}
|
||||
|
||||
this.maneParts.push({
|
||||
mesh: manePart,
|
||||
amp: amp,
|
||||
zOffset: zOffset,
|
||||
periodOffset: periodOffset,
|
||||
xInit: manePart.position.x,
|
||||
yInit: manePart.position.y
|
||||
});
|
||||
this.mane.add(manePart);
|
||||
}
|
||||
}
|
||||
|
||||
this.mane.position.y = -10;
|
||||
this.mane.position.z = 80;
|
||||
//this.mane.rotation.z = Math.PI/4;
|
||||
|
||||
// face
|
||||
this.face = new THREE.Mesh(faceGeom, this.yellowMat);
|
||||
this.face.position.z = 135;
|
||||
|
||||
// Mustaches
|
||||
|
||||
this.mustaches = [];
|
||||
|
||||
this.mustache1 = new THREE.Mesh(mustacheGeom, this.greyMat);
|
||||
this.mustache1.position.x = 30;
|
||||
this.mustache1.position.y = -5;
|
||||
this.mustache1.position.z = 175;
|
||||
this.mustache2 = this.mustache1.clone();
|
||||
this.mustache2.position.x = 35;
|
||||
this.mustache2.position.y = -12;
|
||||
this.mustache3 = this.mustache1.clone();
|
||||
this.mustache3.position.y = -19;
|
||||
this.mustache3.position.x = 30;
|
||||
this.mustache4 = this.mustache1.clone();
|
||||
this.mustache4.rotation.z = Math.PI;
|
||||
this.mustache4.position.x = -30;
|
||||
this.mustache5 = new THREE.Mesh(mustacheGeom, this.blackMat);
|
||||
this.mustache5 = this.mustache2.clone();
|
||||
this.mustache5.rotation.z = Math.PI;
|
||||
this.mustache5.position.x = -35;
|
||||
this.mustache6 = new THREE.Mesh(mustacheGeom, this.blackMat);
|
||||
this.mustache6 = this.mustache3.clone();
|
||||
this.mustache6.rotation.z = Math.PI;
|
||||
this.mustache6.position.x = -30;
|
||||
|
||||
this.mustaches.push(this.mustache1);
|
||||
this.mustaches.push(this.mustache2);
|
||||
this.mustaches.push(this.mustache3);
|
||||
this.mustaches.push(this.mustache4);
|
||||
this.mustaches.push(this.mustache5);
|
||||
this.mustaches.push(this.mustache6);
|
||||
|
||||
// spots
|
||||
this.spot1 = new THREE.Mesh(spotGeom, this.redMat);
|
||||
this.spot1.position.x = 39;
|
||||
this.spot1.position.z = 150;
|
||||
|
||||
this.spot2 = this.spot1.clone();
|
||||
this.spot2.position.z = 160;
|
||||
this.spot2.position.y = -10;
|
||||
|
||||
this.spot3 = this.spot1.clone();
|
||||
this.spot3.position.z = 140;
|
||||
this.spot3.position.y = -15;
|
||||
|
||||
this.spot4 = this.spot1.clone();
|
||||
this.spot4.position.z = 150;
|
||||
this.spot4.position.y = -20;
|
||||
|
||||
this.spot5 = this.spot1.clone();
|
||||
this.spot5.position.x = -39;
|
||||
this.spot6 = this.spot2.clone();
|
||||
this.spot6.position.x = -39;
|
||||
this.spot7 = this.spot3.clone();
|
||||
this.spot7.position.x = -39;
|
||||
this.spot8 = this.spot4.clone();
|
||||
this.spot8.position.x = -39;
|
||||
|
||||
// eyes
|
||||
this.leftEye = new THREE.Mesh(eyeGeom, this.whiteMat);
|
||||
this.leftEye.position.x = 40;
|
||||
this.leftEye.position.z = 120;
|
||||
this.leftEye.position.y = 25;
|
||||
|
||||
this.rightEye = new THREE.Mesh(eyeGeom, this.whiteMat);
|
||||
this.rightEye.position.x = -40;
|
||||
this.rightEye.position.z = 120;
|
||||
this.rightEye.position.y = 25;
|
||||
|
||||
// iris
|
||||
this.leftIris = new THREE.Mesh(irisGeom, this.purpleMat);
|
||||
this.leftIris.position.x = 42;
|
||||
this.leftIris.position.z = 120;
|
||||
this.leftIris.position.y = 25;
|
||||
|
||||
this.rightIris = new THREE.Mesh(irisGeom, this.purpleMat);
|
||||
this.rightIris.position.x = -42;
|
||||
this.rightIris.position.z = 120;
|
||||
this.rightIris.position.y = 25;
|
||||
|
||||
// mouth
|
||||
this.mouth = new THREE.Mesh(mouthGeom, this.blackMat);
|
||||
this.mouth.position.z = 171;
|
||||
this.mouth.position.y = -30;
|
||||
this.mouth.scale.set(.5, .5, 1);
|
||||
|
||||
// smile
|
||||
this.smile = new THREE.Mesh(smileGeom, this.greyMat);
|
||||
this.smile.position.z = 173;
|
||||
this.smile.position.y = -15;
|
||||
this.smile.rotation.z = -Math.PI;
|
||||
|
||||
// lips
|
||||
this.lips = new THREE.Mesh(lipsGeom, this.yellowMat);
|
||||
this.lips.position.z = 165;
|
||||
this.lips.position.y = -45;
|
||||
|
||||
|
||||
// ear
|
||||
this.rightEar = new THREE.Mesh(earGeom, this.yellowMat);
|
||||
this.rightEar.position.x = -50;
|
||||
this.rightEar.position.y = 50;
|
||||
this.rightEar.position.z = 105;
|
||||
|
||||
this.leftEar = new THREE.Mesh(earGeom, this.yellowMat);
|
||||
this.leftEar.position.x = 50;
|
||||
this.leftEar.position.y = 50;
|
||||
this.leftEar.position.z = 105;
|
||||
|
||||
// nose
|
||||
this.nose = new THREE.Mesh(noseGeom, this.greyMat);
|
||||
this.nose.position.z = 170;
|
||||
this.nose.position.y = 25;
|
||||
|
||||
// head
|
||||
this.head = new THREE.Group();
|
||||
this.head.add(this.face);
|
||||
this.head.add(this.mane);
|
||||
this.head.add(this.rightEar);
|
||||
this.head.add(this.leftEar);
|
||||
this.head.add(this.nose);
|
||||
this.head.add(this.leftEye);
|
||||
this.head.add(this.rightEye);
|
||||
this.head.add(this.leftIris);
|
||||
this.head.add(this.rightIris);
|
||||
this.head.add(this.mouth);
|
||||
this.head.add(this.smile);
|
||||
this.head.add(this.lips);
|
||||
this.head.add(this.spot1);
|
||||
this.head.add(this.spot2);
|
||||
this.head.add(this.spot3);
|
||||
this.head.add(this.spot4);
|
||||
this.head.add(this.spot5);
|
||||
this.head.add(this.spot6);
|
||||
this.head.add(this.spot7);
|
||||
this.head.add(this.spot8);
|
||||
this.head.add(this.mustache1);
|
||||
this.head.add(this.mustache2);
|
||||
this.head.add(this.mustache3);
|
||||
this.head.add(this.mustache4);
|
||||
this.head.add(this.mustache5);
|
||||
this.head.add(this.mustache6);
|
||||
|
||||
|
||||
this.head.position.y = 60;
|
||||
|
||||
this.threegroup.add(this.body);
|
||||
this.threegroup.add(this.head);
|
||||
this.threegroup.add(this.leftKnee);
|
||||
this.threegroup.add(this.rightKnee);
|
||||
this.threegroup.add(this.backLeftFoot);
|
||||
this.threegroup.add(this.backRightFoot);
|
||||
this.threegroup.add(this.frontRightFoot);
|
||||
this.threegroup.add(this.frontLeftFoot);
|
||||
|
||||
this.threegroup.traverse(function (object) {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Lion.prototype.updateBody = function (speed) {
|
||||
|
||||
this.head.rotation.y += (this.tHeagRotY - this.head.rotation.y) / speed;
|
||||
this.head.rotation.x += (this.tHeadRotX - this.head.rotation.x) / speed;
|
||||
this.head.position.x += (this.tHeadPosX - this.head.position.x) / speed;
|
||||
this.head.position.y += (this.tHeadPosY - this.head.position.y) / speed;
|
||||
this.head.position.z += (this.tHeadPosZ - this.head.position.z) / speed;
|
||||
|
||||
this.leftEye.scale.y += (this.tEyeScale - this.leftEye.scale.y) / (speed * 2);
|
||||
this.rightEye.scale.y = this.leftEye.scale.y;
|
||||
|
||||
this.leftIris.scale.y += (this.tIrisYScale - this.leftIris.scale.y) / (speed * 2);
|
||||
this.rightIris.scale.y = this.leftIris.scale.y;
|
||||
|
||||
this.leftIris.scale.z += (this.tIrisZScale - this.leftIris.scale.z) / (speed * 2);
|
||||
this.rightIris.scale.z = this.leftIris.scale.z;
|
||||
|
||||
this.leftIris.position.y += (this.tIrisPosY - this.leftIris.position.y) / speed;
|
||||
this.rightIris.position.y = this.leftIris.position.y;
|
||||
this.leftIris.position.z += (this.tLeftIrisPosZ - this.leftIris.position.z) / speed;
|
||||
this.rightIris.position.z += (this.tRightIrisPosZ - this.rightIris.position.z) / speed;
|
||||
|
||||
this.rightKnee.rotation.z += (this.tRightKneeRotZ - this.rightKnee.rotation.z) / speed;
|
||||
this.leftKnee.rotation.z += (this.tLeftKneeRotZ - this.leftKnee.rotation.z) / speed;
|
||||
|
||||
this.lips.position.x += (this.tLipsPosX - this.lips.position.x) / speed;
|
||||
this.lips.position.y += (this.tLipsPosY - this.lips.position.y) / speed;
|
||||
this.smile.position.x += (this.tSmilePosX - this.smile.position.x) / speed;
|
||||
this.mouth.position.z += (this.tMouthPosZ - this.mouth.position.z) / speed;
|
||||
this.smile.position.z += (this.tSmilePosZ - this.smile.position.z) / speed;
|
||||
this.smile.position.y += (this.tSmilePosY - this.smile.position.y) / speed;
|
||||
this.smile.rotation.z += (this.tSmileRotZ - this.smile.rotation.z) / speed;
|
||||
}
|
||||
|
||||
Lion.prototype.look = function (xTarget, yTarget) {
|
||||
this.tHeagRotY = rule3(xTarget, -200, 200, -Math.PI / 4, Math.PI / 4);
|
||||
this.tHeadRotX = rule3(yTarget, -200, 200, -Math.PI / 4, Math.PI / 4);
|
||||
this.tHeadPosX = rule3(xTarget, -200, 200, 70, -70);
|
||||
this.tHeadPosY = rule3(yTarget, -140, 260, 20, 100);
|
||||
this.tHeadPosZ = 0;
|
||||
|
||||
|
||||
this.tEyeScale = 1;
|
||||
this.tIrisYScale = 1;
|
||||
this.tIrisZScale = 1;
|
||||
this.tIrisPosY = rule3(yTarget, -200, 200, 35, 15);
|
||||
this.tLeftIrisPosZ = rule3(xTarget, -200, 200, 130, 110);
|
||||
this.tRightIrisPosZ = rule3(xTarget, -200, 200, 110, 130);
|
||||
|
||||
this.tLipsPosX = 0;
|
||||
this.tLipsPosY = -45;
|
||||
|
||||
this.tSmilePosX = 0;
|
||||
this.tMouthPosZ = 174;
|
||||
this.tSmilePosZ = 173;
|
||||
this.tSmilePosY = -15;
|
||||
this.tSmileRotZ = -Math.PI;
|
||||
|
||||
this.tRightKneeRotZ = rule3(xTarget, -200, 200, .3 - Math.PI / 8, .3 + Math.PI / 8);
|
||||
this.tLeftKneeRotZ = rule3(xTarget, -200, 200, -.3 - Math.PI / 8, -.3 + Math.PI / 8)
|
||||
|
||||
|
||||
this.updateBody(10);
|
||||
|
||||
this.mane.rotation.y = 0;
|
||||
this.mane.rotation.x = 0;
|
||||
|
||||
for (var i = 0; i < this.maneParts.length; i++) {
|
||||
var m = this.maneParts[i].mesh;
|
||||
m.position.z = 0;
|
||||
m.rotation.y = 0;
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.mustaches.length; i++) {
|
||||
var m = this.mustaches[i];
|
||||
m.rotation.y = 0;
|
||||
}
|
||||
|
||||
|
||||
for (var i = 0; i < this.bodyVertices.length; i++) {
|
||||
var tvInit = this.bodyInitPositions[i];
|
||||
var tv = this.body.geometry.vertices[this.bodyVertices[i]];
|
||||
tv.x = tvInit.x + this.head.position.x;
|
||||
}
|
||||
this.body.geometry.verticesNeedUpdate = true;
|
||||
}
|
||||
|
||||
Lion.prototype.cool = function (xTarget, yTarget) {
|
||||
this.tHeagRotY = rule3(xTarget, -200, 200, Math.PI / 4, -Math.PI / 4);
|
||||
this.tHeadRotX = rule3(yTarget, -200, 200, Math.PI / 4, -Math.PI / 4);
|
||||
this.tHeadPosX = rule3(xTarget, -200, 200, -70, 70);
|
||||
this.tHeadPosY = rule3(yTarget, -140, 260, 100, 20);
|
||||
this.tHeadPosZ = 100;
|
||||
|
||||
this.tEyeScale = 0.1;
|
||||
this.tIrisYScale = 0.1;
|
||||
this.tIrisZScale = 3;
|
||||
|
||||
this.tIrisPosY = 20;
|
||||
this.tLeftIrisPosZ = 120;
|
||||
this.tRightIrisPosZ = 120;
|
||||
|
||||
this.tLipsPosX = rule3(xTarget, -200, 200, -15, 15);
|
||||
this.tLipsPosY = rule3(yTarget, -200, 200, -45, -40);
|
||||
|
||||
this.tMouthPosZ = 168;
|
||||
this.tSmilePosX = rule3(xTarget, -200, 200, -15, 15);
|
||||
this.tSmilePosY = rule3(yTarget, -200, 200, -20, -8);
|
||||
this.tSmilePosZ = 176;
|
||||
this.tSmileRotZ = rule3(xTarget, -200, 200, -Math.PI - .3, -Math.PI + .3);
|
||||
|
||||
this.tRightKneeRotZ = rule3(xTarget, -200, 200, .3 + Math.PI / 8, .3 - Math.PI / 8);
|
||||
this.tLeftKneeRotZ = rule3(xTarget, -200, 200, -.3 + Math.PI / 8, -.3 - Math.PI / 8);
|
||||
|
||||
this.updateBody(10);
|
||||
|
||||
this.mane.rotation.y = -.8 * this.head.rotation.y;
|
||||
this.mane.rotation.x = -.8 * this.head.rotation.x;
|
||||
|
||||
var dt = 20000 / (xTarget * xTarget + yTarget * yTarget);
|
||||
dt = Math.max(Math.min(dt, 1), .5);
|
||||
this.windTime += dt;
|
||||
|
||||
for (var i = 0; i < this.maneParts.length; i++) {
|
||||
var m = this.maneParts[i].mesh;
|
||||
var amp = this.maneParts[i].amp;
|
||||
var zOffset = this.maneParts[i].zOffset;
|
||||
var periodOffset = this.maneParts[i].periodOffset;
|
||||
|
||||
m.position.z = zOffset + Math.cos(this.windTime + periodOffset) * amp * dt * 2;
|
||||
}
|
||||
|
||||
this.leftEar.rotation.x = Math.cos(this.windTime) * Math.PI / 16 * dt;
|
||||
this.rightEar.rotation.x = -Math.cos(this.windTime) * Math.PI / 16 * dt;
|
||||
|
||||
|
||||
for (var i = 0; i < this.mustaches.length; i++) {
|
||||
var m = this.mustaches[i];
|
||||
var amp = (i < 3) ? -Math.PI / 8 : Math.PI / 8;
|
||||
m.rotation.y = amp + Math.cos(this.windTime + i) * dt * amp;
|
||||
}
|
||||
|
||||
|
||||
for (var i = 0; i < this.bodyVertices.length; i++) {
|
||||
var tvInit = this.bodyInitPositions[i];
|
||||
var tv = this.body.geometry.vertices[this.bodyVertices[i]];
|
||||
tv.x = tvInit.x + this.head.position.x;
|
||||
}
|
||||
this.body.geometry.verticesNeedUpdate = true;
|
||||
}
|
||||
|
||||
function loop() {
|
||||
render();
|
||||
var xTarget = (mousePos.x - windowHalfX);
|
||||
var yTarget = (mousePos.y - windowHalfY);
|
||||
|
||||
fan.isBlowing = isBlowing;
|
||||
fan.update(xTarget, yTarget);
|
||||
if (isBlowing) {
|
||||
lion.cool(xTarget, yTarget);
|
||||
} else {
|
||||
lion.look(xTarget, yTarget);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (controls) controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
|
||||
init();
|
||||
createLights();
|
||||
createFloor();
|
||||
createLion();
|
||||
createFan();
|
||||
loop();
|
||||
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
|
||||
function rule3(v, vmin, vmax, tmin, tmax) {
|
||||
var nv = Math.max(Math.min(v, vmax), vmin);
|
||||
var dv = vmax - vmin;
|
||||
var pc = (nv - vmin) / dv;
|
||||
var dt = tmax - tmin;
|
||||
var tv = tmin + (pc * dt);
|
||||
return tv;
|
||||
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Gmail" filter="url(#filter0_d_1820_6779)">
|
||||
<rect x="8.00195" y="4" width="24" height="24" rx="12" fill="white"/>
|
||||
<g id="Gmail_2">
|
||||
<mask id="mask0_1820_6779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="12" y="8" width="17" height="16">
|
||||
<path id="Union" d="M20.002 24C24.4202 24 28.002 20.4183 28.002 16C28.002 15.4116 27.9384 14.838 27.8179 14.2857H20.002V17.6H24.4074C23.7535 19.4 22.0279 20.6857 20.002 20.6857C17.4141 20.6857 15.3162 18.5878 15.3162 16C15.3162 13.4122 17.4141 11.3143 20.002 11.3143C21.321 11.3143 22.5128 11.8593 23.3644 12.7365L25.6965 10.3811C24.2459 8.91109 22.2303 8 20.002 8C15.5837 8 12.002 11.5817 12.002 16C12.002 20.4183 15.5837 24 20.002 24Z" fill="#C4C4C4"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1820_6779)">
|
||||
<g id="Gmail_3">
|
||||
<path id="Rectangle 527" d="M6.85938 16.0312L12.0022 11.6593L17.7165 16.1164L12.0022 20.345L6.85938 16.0312Z" fill="#FEBD01"/>
|
||||
<path id="Rectangle 528" d="M12.002 11.6602L18.4591 5.20301L25.4305 7.7173L28.4591 13.0887L19.8305 13.4316L17.7162 16.1173L12.002 11.6602Z" fill="#FF4131"/>
|
||||
<path id="Vector 9" d="M19.6602 13.9434L28.9173 13.7148L28.803 17.8291L26.5173 22.7434L21.8316 19.0863L19.6602 18.0577V13.9434Z" fill="#0286F7"/>
|
||||
<path id="Vector 10" d="M12.002 20.3458L17.7162 16.1172L18.3448 18.9743L21.8305 19.0886L26.5162 22.7458L21.7734 24.8029L15.8877 24.46L12.002 20.3458Z" fill="#01A94D"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1820_6779" x="0.00195312" y="0" width="40" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0627451 0 0 0 0 0.160784 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1820_6779"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1820_6779" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,13 @@
|
|||
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Google " clip-path="url(#clip0_202_73)">
|
||||
<path id="Vector" d="M28.227 14.8225C28.227 13.8708 28.1499 12.914 27.9852 11.9777H14.78V17.3689H22.342C22.0282 19.1076 21.02 20.6457 19.5436 21.6231V25.1212H24.055C26.7043 22.6828 28.227 19.0819 28.227 14.8225Z" fill="#4285F4"/>
|
||||
<path id="Vector_2" d="M14.78 28.5009C18.5558 28.5009 21.7401 27.2612 24.0602 25.1212L19.5487 21.6231C18.2935 22.4771 16.6731 22.9606 14.7851 22.9606C11.1327 22.9606 8.03591 20.4965 6.92476 17.1837H2.26924V20.7898C4.64587 25.5173 9.48658 28.5009 14.78 28.5009Z" fill="#34A853"/>
|
||||
<path id="Vector_3" d="M6.91963 17.1837C6.33319 15.4449 6.33319 13.5621 6.91963 11.8234V8.21729H2.26925C0.283581 12.1732 0.283581 16.8339 2.26925 20.7898L6.91963 17.1837Z" fill="#FBBC04"/>
|
||||
<path id="Vector_4" d="M14.78 6.04127C16.7759 6.01041 18.705 6.76146 20.1505 8.14012L24.1476 4.14305C21.6166 1.76642 18.2575 0.45979 14.78 0.500943C9.48658 0.500943 4.64587 3.48459 2.26924 8.21728L6.91961 11.8234C8.02562 8.50536 11.1276 6.04127 14.78 6.04127Z" fill="#EA4335"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_202_73">
|
||||
<rect width="28" height="28" fill="white" transform="translate(0.5 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -1,11 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="6" fill="url(#paint0_linear_6_57)"/>
|
||||
<path d="M12 16V11" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="1" cy="1" r="1" transform="matrix(1 0 0 -1 11 9)" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6_57" x1="12" y1="-4.5" x2="12" y2="28" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4DCAFF"/>
|
||||
<stop offset="1" stop-color="#4EA3E0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 537 B |
|
@ -1,36 +0,0 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Instagram" filter="url(#filter0_d_1820_6791)">
|
||||
<g id="Instagram_2">
|
||||
<path id="Vector" d="M20.0008 4C13.3747 4 8.00195 9.37276 8.00195 16.0013C8.00195 22.6276 13.3747 28 20.0008 28C26.6272 28 31.9998 22.6276 31.9998 16.0013C31.9998 9.37276 26.6272 4 20.0008 4Z" fill="url(#paint0_linear_1820_6791)"/>
|
||||
<g id="Group">
|
||||
<g id="Group_2">
|
||||
<path id="Vector_2" d="M20.0018 20.2841C22.3646 20.2841 24.2869 18.3628 24.2869 16.0008C24.2869 13.6375 22.3646 11.7148 20.0018 11.7148C17.639 11.7148 15.7168 13.6375 15.7168 16.0008C15.7168 18.3626 17.6392 20.2841 20.0018 20.2841ZM16.7562 16.0008C16.7562 14.2098 18.2121 12.7527 20.0018 12.7527C21.7915 12.7527 23.2476 14.2098 23.2476 16.0008C23.2476 17.7905 21.7915 19.2466 20.0018 19.2466C18.2121 19.2466 16.7562 17.7905 16.7562 16.0008Z" fill="white"/>
|
||||
</g>
|
||||
<path id="Vector_3" d="M24.2746 23.1178H15.7275C14.1234 23.1178 12.8184 21.8127 12.8184 20.2086V11.792C12.8184 10.1879 14.1234 8.88281 15.7275 8.88281H24.2746C25.8788 8.88281 27.1838 10.1879 27.1838 11.792V20.2086C27.1838 21.8127 25.8788 23.1178 24.2746 23.1178ZM15.7275 9.94421C14.7089 9.94421 13.8798 10.7731 13.8798 11.792V20.2086C13.8798 21.2273 14.7087 22.0564 15.7275 22.0564H24.2746C25.2933 22.0564 26.1224 21.2274 26.1224 20.2086V11.792C26.1224 10.7731 25.2935 9.94421 24.2746 9.94421H15.7275Z" fill="white"/>
|
||||
</g>
|
||||
<path id="Vector_4" d="M24.3444 11.2266C23.9697 11.2266 23.668 11.5316 23.668 11.9038C23.668 12.2783 23.9697 12.5793 24.3444 12.5793C24.7164 12.5793 25.0214 12.2783 25.0214 11.9038C25.0214 11.5316 24.7164 11.2266 24.3444 11.2266Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1820_6791" x="0.00195312" y="0" width="39.998" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0627451 0 0 0 0 0.160784 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1820_6791"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1820_6791" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1820_6791" x1="27.8733" y1="25.0565" x2="12.1285" y2="6.94412" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5D3FD2"/>
|
||||
<stop offset="0.4244" stop-color="#B829A9"/>
|
||||
<stop offset="0.5009" stop-color="#BE289D"/>
|
||||
<stop offset="0.6383" stop-color="#D0257E"/>
|
||||
<stop offset="0.7849" stop-color="#E62256"/>
|
||||
<stop offset="0.8147" stop-color="#E83156"/>
|
||||
<stop offset="0.8718" stop-color="#EE5755"/>
|
||||
<stop offset="0.9495" stop-color="#F89554"/>
|
||||
<stop offset="1" stop-color="#FFC153"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1,23 +0,0 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Linkedin" filter="url(#filter0_d_1820_6799)">
|
||||
<g id="Linked in">
|
||||
<path id="XMLID 108" d="M31.9998 16C31.9998 9.37234 26.6273 4 20.0007 4C13.372 4 8 9.37218 8 16C8 22.6268 13.3722 28 20.0007 28C26.6275 28 31.9998 22.6268 31.9998 16Z" fill="#3D83D9"/>
|
||||
<g id="Group">
|
||||
<path id="Vector" d="M17.0649 13.0703H14.2871V21.0091H17.0649V13.0703Z" fill="white"/>
|
||||
<path id="Vector_2" d="M15.6761 9.10156C14.806 9.10156 14.1249 9.90117 14.321 10.8019C14.435 11.3251 14.8618 11.7457 15.3869 11.8511C16.2822 12.031 17.0649 11.3532 17.0649 10.4906C17.0649 9.7247 16.4443 9.10156 15.6761 9.10156Z" fill="white"/>
|
||||
<path id="Vector_3" d="M26.1947 15.5091C26.0079 14.0128 25.2594 13.0703 23.2368 13.0703C21.8012 13.0703 21.2303 13.2942 20.9011 13.9209V13.0703H18.6523V21.0091H20.9666V16.8535C20.9666 15.8161 21.1631 15.0325 22.4459 15.0325C23.7102 15.0325 23.8118 15.9969 23.8118 16.9208V21.0093H26.1949C26.1947 21.0093 26.2389 15.8588 26.1947 15.5091Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1820_6799" x="0" y="0" width="40" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0627451 0 0 0 0 0.160784 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1820_6799"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1820_6799" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,10 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="6" fill="url(#paint0_linear_6_55)"/>
|
||||
<path d="M8.5 12.5L10.5 14.5L15.5 9.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6_55" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#48CA93"/>
|
||||
<stop offset="1" stop-color="#48BACA"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 500 B |
|
@ -1,12 +0,0 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="6" fill="url(#paint0_linear_6_62)"/>
|
||||
<path d="M12 9V14" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="17" r="1" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6_62" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DBA948"/>
|
||||
<stop offset="0.0001" stop-color="#FFC46B"/>
|
||||
<stop offset="1" stop-color="#FFA318"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 546 B |
Before Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 135 KiB |
Before Width: | Height: | Size: 2.9 MiB |
After Width: | Height: | Size: 17 KiB |
|
@ -12,6 +12,7 @@ export type UserInfoType = {
|
|||
avatar: string,
|
||||
desc: string,
|
||||
posts: string,
|
||||
Role: string,
|
||||
}
|
||||
|
||||
export type UserInfoListType = {
|
||||
|
|