创建项目
This commit is contained in:
BIN
.vs/Kx.SeaTime.slnx/DesignTimeBuild/.dtbcache.v2
Normal file
BIN
.vs/Kx.SeaTime.slnx/DesignTimeBuild/.dtbcache.v2
Normal file
Binary file not shown.
Binary file not shown.
BIN
.vs/Kx.SeaTime.slnx/v18/.futdcache.v2
Normal file
BIN
.vs/Kx.SeaTime.slnx/v18/.futdcache.v2
Normal file
Binary file not shown.
BIN
.vs/Kx.SeaTime.slnx/v18/.suo
Normal file
BIN
.vs/Kx.SeaTime.slnx/v18/.suo
Normal file
Binary file not shown.
27
.vs/Kx.SeaTime.slnx/v18/DocumentLayout.backup.json
Normal file
27
.vs/Kx.SeaTime.slnx/v18/DocumentLayout.backup.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "D:\\Program\\Kg.SeaTime\\",
|
||||||
|
"Documents": [],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 200,
|
||||||
|
"SelectedChildIndex": -1,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
.vs/Kx.SeaTime.slnx/v18/DocumentLayout.json
Normal file
27
.vs/Kx.SeaTime.slnx/v18/DocumentLayout.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "D:\\Program\\Kg.SeaTime\\",
|
||||||
|
"Documents": [],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 200,
|
||||||
|
"SelectedChildIndex": -1,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
.vs/ProjectEvaluation/kx.seatime.metadata.v10.bin
Normal file
BIN
.vs/ProjectEvaluation/kx.seatime.metadata.v10.bin
Normal file
Binary file not shown.
BIN
.vs/ProjectEvaluation/kx.seatime.projects.v10.bin
Normal file
BIN
.vs/ProjectEvaluation/kx.seatime.projects.v10.bin
Normal file
Binary file not shown.
BIN
.vs/ProjectEvaluation/kx.seatime.strings.v10.bin
Normal file
BIN
.vs/ProjectEvaluation/kx.seatime.strings.v10.bin
Normal file
Binary file not shown.
7
Kx.SeaTime.slnx
Normal file
7
Kx.SeaTime.slnx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Service/Application.Domain.Entity/Application.Domain.Entity.csproj" Id="2c4b3532-d29e-4dec-bcf3-eeceb1c374fc" />
|
||||||
|
<Project Path="Service/Application.Domain/Application.Domain.csproj" Id="78231809-eabc-4800-9c91-479adb485405" />
|
||||||
|
<Project Path="Service/Application.Service.Pub/Application.Service.Pub.csproj" Id="3d90f467-0fcb-4074-bc96-33b36035ee23" />
|
||||||
|
<Project Path="Service/Application.Service.Tool/Application.Service.Tool.csproj" Id="6d8f1349-ed04-4907-b9c4-3d621ad5188e" />
|
||||||
|
<Project Path="Service/Application.Web/Application.Web.csproj" Id="28f22f27-cc3e-403c-b646-5cbd2e3112ae" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Application.Service.Pub\Application.Service.Pub.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
7
Service/Application.Domain.Entity/Class1.cs
Normal file
7
Service/Application.Domain.Entity/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Application.Domain.Entity
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Service/Application.Domain/Application.Domain.csproj
Normal file
13
Service/Application.Domain/Application.Domain.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Application.Domain.Entity\Application.Domain.Entity.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
7
Service/Application.Domain/Class1.cs
Normal file
7
Service/Application.Domain/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Application.Domain
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Service/Application.Domain/RequestParms/Login/LoginParms.cs
Normal file
12
Service/Application.Domain/RequestParms/Login/LoginParms.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Application.Domain
|
||||||
|
{
|
||||||
|
public class LoginParms
|
||||||
|
{
|
||||||
|
public string name { get; set; }
|
||||||
|
public string pwd { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Photon.Core" Version="1.0.0.1" />
|
||||||
|
<PackageReference Include="Photon.Core.Assist" Version="1.0.0.1" />
|
||||||
|
<PackageReference Include="Photon.Core.Jwt" Version="1.0.0.2" />
|
||||||
|
<PackageReference Include="Photon.Core.Queue" Version="1.0.0.1" />
|
||||||
|
<PackageReference Include="Photon.Core.Redis" Version="1.0.0.1" />
|
||||||
|
<PackageReference Include="Photon.Core.SqlSugar" Version="1.0.0.1" />
|
||||||
|
<PackageReference Include="Photon.Core.Timer" Version="1.0.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
7
Service/Application.Service.Pub/Class1.cs
Normal file
7
Service/Application.Service.Pub/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Application.Service.Pub
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
7
Service/Application.Service.Tool/Class1.cs
Normal file
7
Service/Application.Service.Tool/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Application.Service.Tool
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Service/Application.Web/Application.Web.csproj
Normal file
18
Service/Application.Web/Application.Web.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Application.Domain\Application.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
Service/Application.Web/Application.Web.csproj.user
Normal file
8
Service/Application.Web/Application.Web.csproj.user
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ActiveDebugProfile>https</ActiveDebugProfile>
|
||||||
|
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||||
|
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
40
Service/Application.Web/Common/JwtHandle.cs
Normal file
40
Service/Application.Web/Common/JwtHandle.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
|
namespace Application.Web
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JWT验证
|
||||||
|
/// </summary>
|
||||||
|
public class JwtHandle : IJwtValidator
|
||||||
|
{
|
||||||
|
public int Priority => 1;
|
||||||
|
public void ValidateAsync(TokenValidatedContext context, JwtTokenInfo tokenInfo)
|
||||||
|
{
|
||||||
|
string userId = StateHelper.userId;
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
throw new JwtValidationException(
|
||||||
|
message: "未登录",
|
||||||
|
statusCode: 200,
|
||||||
|
errorCode: 401); // 自定义错误码:用户禁用
|
||||||
|
}
|
||||||
|
//var userService = App.GetService<IUnitUserService>();
|
||||||
|
|
||||||
|
//var userInfo = userService.GetUserInfoAsyc(userId);
|
||||||
|
//if (userInfo == null)
|
||||||
|
//{
|
||||||
|
// throw new JwtValidationException(
|
||||||
|
// message: "未登录",
|
||||||
|
// statusCode: 200,
|
||||||
|
// errorCode: 401); // 自定义错误码:用户禁用
|
||||||
|
//}
|
||||||
|
//if (userInfo.token != StateHelper.sid)
|
||||||
|
//{
|
||||||
|
// throw new JwtValidationException(
|
||||||
|
// message: "未登录",
|
||||||
|
// statusCode: 200,
|
||||||
|
// errorCode: 401); // 自定义错误码:用户禁用
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Service/Application.Web/Common/StateHelper.cs
Normal file
21
Service/Application.Web/Common/StateHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Application.Web
|
||||||
|
{
|
||||||
|
public class StateHelper
|
||||||
|
{
|
||||||
|
public static string userId
|
||||||
|
{
|
||||||
|
get {
|
||||||
|
var token = App.HttpContext.GetTokenInfo();
|
||||||
|
return token.Claims["userId"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static string sid
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var token = App.HttpContext.GetTokenInfo();
|
||||||
|
return token.Claims["sid"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Service/Application.Web/Controllers/Login/LoginController.cs
Normal file
26
Service/Application.Web/Controllers/Login/LoginController.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Application.Web.Controllers.Login
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 登录接口
|
||||||
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(GroupName = "Login")]
|
||||||
|
[Route("[controller]/[action]")]
|
||||||
|
[ApiController]
|
||||||
|
public class LoginController : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登录接口
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parms"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IPoAction> Login([FromBody] LoginParms parms)
|
||||||
|
{
|
||||||
|
return PoAction.Ok(parms.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Service/Application.Web/GlobalUsings.cs
Normal file
16
Service/Application.Web/GlobalUsings.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
global using Photon.Core;
|
||||||
|
global using Photon.Core.Jwt;
|
||||||
|
global using Photon.Core.Services;
|
||||||
|
global using Photon.Core.SqlSugar;
|
||||||
|
global using Photon.Core.Assist;
|
||||||
|
global using Photon.Core.Timer;
|
||||||
|
|
||||||
|
global using Application.Service.Pub;
|
||||||
|
global using Application.Domain;
|
||||||
|
global using Application.Domain.Entity;
|
||||||
|
global using Microsoft.AspNetCore.Authorization;
|
||||||
|
global using Microsoft.AspNetCore.Mvc;
|
||||||
|
global using Application.Web;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
22
Service/Application.Web/Plug/Timer/AutoJob.cs
Normal file
22
Service/Application.Web/Plug/Timer/AutoJob.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Quartz;
|
||||||
|
namespace Application.Web;
|
||||||
|
|
||||||
|
public class AutoJob: ITimerAutoJob
|
||||||
|
{
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
string jobId = context.JobDetail.Key.Name;
|
||||||
|
if (jobId == "DayHandle")
|
||||||
|
{
|
||||||
|
|
||||||
|
////处理日红包
|
||||||
|
//await businessService.HandleRedPack();
|
||||||
|
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MMMM-dd")}:红包奖金池已处理!");
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("定时程序测试执行");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Service/Application.Web/Plug/Timer/JobStart.cs
Normal file
68
Service/Application.Web/Plug/Timer/JobStart.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Quartz;
|
||||||
|
using Quartz.Spi;
|
||||||
|
|
||||||
|
namespace Application.Web;
|
||||||
|
public class JobStart: ITimerAutoStart
|
||||||
|
{
|
||||||
|
private readonly ISchedulerFactory _schedulerFactory;
|
||||||
|
private readonly IJobFactory _jobFactory;
|
||||||
|
|
||||||
|
public JobStart(
|
||||||
|
ISchedulerFactory schedulerFactory,
|
||||||
|
IJobFactory jobFactory)
|
||||||
|
{
|
||||||
|
_schedulerFactory = schedulerFactory;
|
||||||
|
_jobFactory = jobFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IScheduler Scheduler { get; private set; }
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
|
||||||
|
Scheduler.JobFactory = _jobFactory;
|
||||||
|
|
||||||
|
// 创建作业
|
||||||
|
var job = JobBuilder.Create<AutoJob>()
|
||||||
|
.WithIdentity("DayHandle", "Handle")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// 创建触发器
|
||||||
|
var trigger = TriggerBuilder.Create()
|
||||||
|
.WithIdentity($"DayHandle_trigger", "Handle")
|
||||||
|
.WithCronSchedule("0 0 0 * * ? ")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
|
||||||
|
await Scheduler.Start(cancellationToken);
|
||||||
|
|
||||||
|
Console.WriteLine("----航海时代V3自动程序已启动-----");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Begin()
|
||||||
|
{
|
||||||
|
Scheduler = await _schedulerFactory.GetScheduler();
|
||||||
|
Scheduler.JobFactory = _jobFactory;
|
||||||
|
|
||||||
|
// 创建作业
|
||||||
|
var job = JobBuilder.Create<AutoJob>()
|
||||||
|
.WithIdentity("111", "test")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// 创建触发器
|
||||||
|
var trigger = TriggerBuilder.Create()
|
||||||
|
.WithIdentity($"111_trigger", "test")
|
||||||
|
.WithCronSchedule("0/1 * * * * ? ")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await Scheduler.ScheduleJob(job, trigger);
|
||||||
|
await Scheduler.Start();
|
||||||
|
|
||||||
|
Console.WriteLine("----探玩驿站自动程序已启动-----");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await Scheduler?.Shutdown(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Service/Application.Web/Plug/Timer/TimerJobManager.cs
Normal file
56
Service/Application.Web/Plug/Timer/TimerJobManager.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace Application.Web;
|
||||||
|
|
||||||
|
public class TimerJobManager : ITimerJobManager
|
||||||
|
{
|
||||||
|
private readonly IScheduler _scheduler;
|
||||||
|
|
||||||
|
public TimerJobManager(IScheduler scheduler)
|
||||||
|
{
|
||||||
|
_scheduler = scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAndStartTask(string jobId, string group, string cronExpression)
|
||||||
|
{
|
||||||
|
// 创建作业
|
||||||
|
var job = JobBuilder.Create<AutoJob>()
|
||||||
|
.WithIdentity(jobId, group)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// 创建触发器
|
||||||
|
var trigger = TriggerBuilder.Create()
|
||||||
|
.WithIdentity($"{jobId}_trigger", group)
|
||||||
|
.WithCronSchedule(cronExpression)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// 安排作业
|
||||||
|
await _scheduler.ScheduleJob(job, trigger);
|
||||||
|
|
||||||
|
// 启动调度器(如果尚未启动)
|
||||||
|
if (!_scheduler.IsStarted)
|
||||||
|
{
|
||||||
|
await _scheduler.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PauseTask(string jobId)
|
||||||
|
{
|
||||||
|
await _scheduler.PauseJob(new JobKey(jobId, "default"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResumeTask(string jobId, string group)
|
||||||
|
{
|
||||||
|
await _scheduler.ResumeJob(new JobKey(jobId, group));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteTask(string jobId, string group)
|
||||||
|
{
|
||||||
|
await _scheduler.DeleteJob(new JobKey(jobId, group));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteTask(string jobId)
|
||||||
|
{
|
||||||
|
await _scheduler.DeleteJob(new JobKey(jobId));
|
||||||
|
}
|
||||||
|
}
|
||||||
109
Service/Application.Web/Program.cs
Normal file
109
Service/Application.Web/Program.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.Inject(new List<string> {
|
||||||
|
"Application.Web","Application.Domain"
|
||||||
|
});//框架初始化
|
||||||
|
|
||||||
|
builder.Configuration.RegisterConfig();//注入配置
|
||||||
|
builder.Services.InjectMemory();
|
||||||
|
builder.Services.InjectSql(builder.Configuration);
|
||||||
|
|
||||||
|
#region JWT与接口配置
|
||||||
|
|
||||||
|
Dictionary<string, OpenApiInfo> groups = new Dictionary<string, OpenApiInfo>();
|
||||||
|
groups.Add("Login", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "登录接口文档",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "航海时代登录相关接口。",
|
||||||
|
});
|
||||||
|
groups.Add("User", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "用户接口文档",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "航海时代用户相关接口。",
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.InjectJwt(builder.Configuration, new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "航海时代接口文档",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "航海时代接口文档,包含航海时代的所有接口,项目采用AI开发。",
|
||||||
|
}, "Application.Web", op =>
|
||||||
|
{
|
||||||
|
op.AddTransient<IJwtValidator, Application.Web.JwtHandle>();
|
||||||
|
}, groups);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
//定时功能
|
||||||
|
builder.Services.InjectTimer(services =>
|
||||||
|
{
|
||||||
|
services.AddTransient<ITimerJobManager, TimerJobManager>();//注册管理器
|
||||||
|
services.AddSingleton<AutoJob>();
|
||||||
|
services.AddHostedService<JobStart>();
|
||||||
|
});
|
||||||
|
|
||||||
|
//日志
|
||||||
|
builder.Logging.InjectLog();
|
||||||
|
// Add services to the container.
|
||||||
|
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
|
|
||||||
|
#region 配置跨域处理,允许所有来源
|
||||||
|
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("all", builder =>
|
||||||
|
{
|
||||||
|
builder.AllowAnyOrigin() //允许任何来源的主机访问
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
#endregion 配置跨域处理,允许所有来源
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.Services.UseInject();//启用框架
|
||||||
|
app.Configuration.UseConfig();//启用配置
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Home/Error");
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
app.UseHsts();
|
||||||
|
|
||||||
|
|
||||||
|
#region 测试swagger配置
|
||||||
|
app.UseJwtSwagger("swagger", "航海时代接口文档",new List<JwtGroupConfig> {
|
||||||
|
new JwtGroupConfig(){ groupId="Login", groupName="登录接口文档"},
|
||||||
|
new JwtGroupConfig(){ groupId="User", groupName="用户接口文档"}
|
||||||
|
});
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseCors("all");
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "default",
|
||||||
|
pattern: "{controller=Home}/{action=Index}/{id?}")
|
||||||
|
.WithStaticAssets();
|
||||||
|
|
||||||
|
SnowflakeAssist.Initialize(0, 0);
|
||||||
|
app.Run();
|
||||||
23
Service/Application.Web/Properties/launchSettings.json
Normal file
23
Service/Application.Web/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5031",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "https://localhost:7198;http://localhost:5031",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Service/Application.Web/applicationsettings.json
Normal file
33
Service/Application.Web/applicationsettings.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"AutoProgram": 0,
|
||||||
|
"Redis": {
|
||||||
|
"connection": "127.0.0.1,password=,defaultdatabase=1"
|
||||||
|
},
|
||||||
|
"SqlData": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"name": "KgDb",
|
||||||
|
"connect": "data source=kx.iyba.cn;database=kx.petera;user id=root;password=1f5ozxRGE3Y;pooling=true;port=23306;sslmode=Required;charset=utf8mb4;"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"JwtTokenOptions": {
|
||||||
|
"Issuer": "kx.petera",
|
||||||
|
"Audience": "kx.petera",
|
||||||
|
"SecurityKey": "DAbdA9fF9MYXDCFBEb75WKWWDAbDBWQi"
|
||||||
|
},
|
||||||
|
"ResUrl": {
|
||||||
|
"local": "http://192.168.0.142:5298",
|
||||||
|
"resUrl": "http://localhost:12206",
|
||||||
|
"imgCDN": "/",
|
||||||
|
"videoCDN": "/"
|
||||||
|
},
|
||||||
|
"Sms": {
|
||||||
|
"SmsType": 1,
|
||||||
|
"AccessKey": "AKIDVptgCRP5UcT4PTGm1yf5E6pKYVBajeKn",
|
||||||
|
"Secret": "FG2atxlKflcEclgKhnc9XeU3LM6YjdGf",
|
||||||
|
"signName": "探玩驿站",
|
||||||
|
"TemplateCode": "963929",
|
||||||
|
"SmsSdkAppId": "1400523979",
|
||||||
|
"SmsOnTime": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Service/Application.Web/appsettings.Development.json
Normal file
8
Service/Application.Web/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Service/Application.Web/appsettings.json
Normal file
9
Service/Application.Web/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
54
Web/nuxt.config.ts
Normal file
54
Web/nuxt.config.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
srcDir: 'src/',
|
||||||
|
|
||||||
|
future: {
|
||||||
|
compatibilityVersion: 4
|
||||||
|
},
|
||||||
|
|
||||||
|
devtools: { enabled: true },
|
||||||
|
|
||||||
|
// 开发服务器配置
|
||||||
|
devServer: {
|
||||||
|
port: 5068,
|
||||||
|
},
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
'@pinia/nuxt'
|
||||||
|
],
|
||||||
|
|
||||||
|
// 自动导入配置 - 使用完整路径
|
||||||
|
imports: {
|
||||||
|
dirs: [
|
||||||
|
'stores',
|
||||||
|
'composables',
|
||||||
|
'extends',
|
||||||
|
'services'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
pinia: {
|
||||||
|
storesDirs: ['./src/stores/**']
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: {
|
||||||
|
strict: true,
|
||||||
|
shim: false
|
||||||
|
},
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
title: '航海时代',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ name: 'keywords', content: '航海时代2|纵横四海|雄霸四海' },
|
||||||
|
{ name: 'description', content: '航海时代是2022年推出的一款以航海为背景的wap文字游戏,让玩家穿越到15世纪的大航海时代,了解各地不同的风土人情;淋漓尽致的演绎航海时代的血雨腥风。' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
css: ['~/assets/css/style.css'],
|
||||||
|
|
||||||
|
compatibilityDate: '2024-04-03'
|
||||||
|
})
|
||||||
11099
Web/package-lock.json
generated
Normal file
11099
Web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
Web/package.json
Normal file
24
Web/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "sea-time",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "航海时代是2022年推出的一款以航海为背景的wap文字游戏,让玩家穿越到15世纪的大航海时代,了解各地不同的风土人情;淋漓尽致的演绎航海时代的血雨腥风。",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"@nuxt/devtools": "^2.0.0",
|
||||||
|
"nuxt": "^4.4.2",
|
||||||
|
"typescript": "^5.4.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Web/src/app.vue
Normal file
42
Web/src/app.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 项目根入口文件
|
||||||
|
// Nuxt4会自动注入,无需手动配置
|
||||||
|
// 全局样式已移至 src/assets/css/style.css 并在 nuxt.config.ts 中全局引用
|
||||||
|
|
||||||
|
// 可以在这里进行全局初始化
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
// 初始化应用配置
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始化屏幕尺寸
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
appStore.updateScreenSize(window.innerWidth, window.innerHeight)
|
||||||
|
|
||||||
|
// 监听屏幕尺寸变化
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
appStore.updateScreenSize(window.innerWidth, window.innerHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听网络状态变化
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
appStore.setOnlineStatus(true)
|
||||||
|
})
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
appStore.setOnlineStatus(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 页面级样式可以在这里定义 */
|
||||||
|
/* 全局样式已移至 src/assets/css/style.css */
|
||||||
|
</style>
|
||||||
199
Web/src/assets/css/style.css
Normal file
199
Web/src/assets/css/style.css
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* 全局样式定义
|
||||||
|
* 此文件通过 nuxt.config.ts 全局引用
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* CSS变量定义 - 主题色 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #409eff;
|
||||||
|
--success-color: #67c23a;
|
||||||
|
--warning-color: #e6a23c;
|
||||||
|
--danger-color: #f56c6c;
|
||||||
|
--info-color: #909399;
|
||||||
|
|
||||||
|
--text-color: #303133;
|
||||||
|
--text-color-secondary: #606266;
|
||||||
|
--text-color-placeholder: #c0c4cc;
|
||||||
|
|
||||||
|
--border-color: #dcdfe6;
|
||||||
|
--border-color-light: #e4e7ed;
|
||||||
|
--border-color-lighter: #ebeef5;
|
||||||
|
|
||||||
|
--bg-color: #ffffff;
|
||||||
|
--bg-color-page: #f5f7fa;
|
||||||
|
--bg-color-overlay: #ffffff;
|
||||||
|
|
||||||
|
--border-radius: 4px;
|
||||||
|
--border-radius-small: 2px;
|
||||||
|
|
||||||
|
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
--box-shadow-light: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色主题 */
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--text-color: #e5eaf3;
|
||||||
|
--text-color-secondary: #a3a6ad;
|
||||||
|
--text-color-placeholder: #8d9095;
|
||||||
|
|
||||||
|
--border-color: #4c4d4f;
|
||||||
|
--border-color-light: #414243;
|
||||||
|
--border-color-lighter: #363637;
|
||||||
|
|
||||||
|
--bg-color: #1d1e1f;
|
||||||
|
--bg-color-page: #141414;
|
||||||
|
--bg-color-overlay: #262727;
|
||||||
|
|
||||||
|
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
--box-shadow-light: 0 2px 8px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 重置样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接样式 */
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框样式 */
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c0c4cc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具类 - 文本对齐 */
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具类 - 间距 */
|
||||||
|
.mt-10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-10 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-20 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-20 {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具类 - 颜色 */
|
||||||
|
.text-primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-info {
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 工具类 - 背景 */
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-success {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-warning {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-info {
|
||||||
|
background-color: var(--info-color);
|
||||||
|
}
|
||||||
19
Web/src/composables/layout.ts
Normal file
19
Web/src/composables/layout.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 页面布局常量
|
||||||
|
* 用于统一管理页面布局配置
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* import { LAYOUT } from '@/composables/layout'
|
||||||
|
* definePageMeta({
|
||||||
|
* layout: LAYOUT.EMPTY
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 定义布局常量
|
||||||
|
export const layout = {
|
||||||
|
default: 'default',
|
||||||
|
empty: 'empty'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export type LayoutValue = typeof layout.default | typeof layout.empty
|
||||||
164
Web/src/composables/useAuth.ts
Normal file
164
Web/src/composables/useAuth.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* 权限校验组合式函数
|
||||||
|
* 提供登录检查、权限验证等功能
|
||||||
|
*/
|
||||||
|
import { useUserStore } from '~/stores/user'
|
||||||
|
import { useAppStore } from '~/stores/app'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限校验hook
|
||||||
|
* 用于页面/组件中的权限校验
|
||||||
|
*/
|
||||||
|
export const useAuth = () => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已登录
|
||||||
|
* @returns boolean 是否已登录
|
||||||
|
*/
|
||||||
|
const isAuthenticated = (): boolean => {
|
||||||
|
return userStore.isLogin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否未登录
|
||||||
|
* @returns boolean 是否未登录
|
||||||
|
*/
|
||||||
|
const isGuest = (): boolean => {
|
||||||
|
return !userStore.isLogin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否拥有指定角色
|
||||||
|
* @param roles 角色数组
|
||||||
|
* @returns boolean 是否有权限
|
||||||
|
*/
|
||||||
|
const hasRole = (roles: string | string[]): boolean => {
|
||||||
|
if (!userStore.isLogin) return false
|
||||||
|
|
||||||
|
const userRole = userStore.userRole
|
||||||
|
const roleList = Array.isArray(roles) ? roles : [roles]
|
||||||
|
|
||||||
|
return roleList.includes(userRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为管理员
|
||||||
|
* @returns boolean 是否为管理员
|
||||||
|
*/
|
||||||
|
const isAdmin = (): boolean => {
|
||||||
|
return hasRole('admin')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户ID
|
||||||
|
* @returns number 用户ID
|
||||||
|
*/
|
||||||
|
const getUserId = (): number => {
|
||||||
|
return userStore.userId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
* @returns IUserInfo | null 用户信息
|
||||||
|
*/
|
||||||
|
const getUserInfo = () => {
|
||||||
|
return userStore.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户昵称
|
||||||
|
* @returns string 用户昵称
|
||||||
|
*/
|
||||||
|
const getNickname = (): string => {
|
||||||
|
return userStore.userNickname
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到登录页(如果未登录)
|
||||||
|
* @param redirectUrl 登录后重定向的URL
|
||||||
|
*/
|
||||||
|
const requireAuth = (redirectUrl?: string) => {
|
||||||
|
if (!userStore.isLogin) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const url = redirectUrl || window.location.href
|
||||||
|
window.location.href = `/login?redirect=${encodeURIComponent(url)}`
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到登录页(如果已登录)
|
||||||
|
* @param redirectUrl 登录后重定向的URL
|
||||||
|
*/
|
||||||
|
const requireGuest = (redirectUrl: string = '/') => {
|
||||||
|
if (userStore.isLogin) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = redirectUrl
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查功能权限(基于角色)
|
||||||
|
* @param permission 权限标识
|
||||||
|
* @returns boolean 是否有权限
|
||||||
|
*/
|
||||||
|
const hasPermission = (permission: string): boolean => {
|
||||||
|
// 简化实现,实际项目中可以结合后端返回的权限列表
|
||||||
|
if (!userStore.isLogin) return false
|
||||||
|
|
||||||
|
// 管理员拥有所有权限
|
||||||
|
if (userStore.userRole === 'admin') return true
|
||||||
|
|
||||||
|
// TODO: 可以从用户信息中获取权限列表进行匹配
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
* @param redirectUrl 退出后重定向的URL
|
||||||
|
*/
|
||||||
|
const logout = async (redirectUrl: string = '/login') => {
|
||||||
|
try {
|
||||||
|
// 调用退出登录API(如果需要)
|
||||||
|
// const userService = new UserSERVICE()
|
||||||
|
// await userService.logout()
|
||||||
|
} finally {
|
||||||
|
// 清除用户状态
|
||||||
|
userStore.clearUserInfo()
|
||||||
|
|
||||||
|
// 跳转到登录页
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = redirectUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
isLogin: computed(() => userStore.isLogin),
|
||||||
|
userInfo: computed(() => userStore.userInfo),
|
||||||
|
userId: computed(() => userStore.userId),
|
||||||
|
nickname: computed(() => userStore.userNickname),
|
||||||
|
userRole: computed(() => userStore.userRole),
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
isAuthenticated,
|
||||||
|
isGuest,
|
||||||
|
hasRole,
|
||||||
|
isAdmin,
|
||||||
|
getUserId,
|
||||||
|
getUserInfo,
|
||||||
|
getNickname,
|
||||||
|
requireAuth,
|
||||||
|
requireGuest,
|
||||||
|
hasPermission,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
}
|
||||||
182
Web/src/extends/cryptoEXTEND.ts
Normal file
182
Web/src/extends/cryptoEXTEND.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* 加密解密工具类(支持实例化)
|
||||||
|
* 提供Base64加密解密、AES加解密等功能
|
||||||
|
*/
|
||||||
|
export class CryptoEXTEND {
|
||||||
|
// 默认加密密钥
|
||||||
|
private static readonly DEFAULT_KEY = 'kx-ui-framework-key'
|
||||||
|
|
||||||
|
// 实例密钥
|
||||||
|
private key: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数:初始化加密密钥
|
||||||
|
* @param key 加密密钥(不同实例可传入不同密钥)
|
||||||
|
*/
|
||||||
|
constructor(key?: string) {
|
||||||
|
this.key = key || CryptoEXTEND.DEFAULT_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64加密字符串
|
||||||
|
* @param data 需要加密的字符串
|
||||||
|
* @returns 加密后的Base64字符串
|
||||||
|
*/
|
||||||
|
encryptBase64(data: string): string {
|
||||||
|
if (!data) return ''
|
||||||
|
try {
|
||||||
|
return btoa(encodeURIComponent(data))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Base64加密失败:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64解密字符串
|
||||||
|
* @param data 需要解密的Base64字符串
|
||||||
|
* @returns 解密后的原始字符串
|
||||||
|
*/
|
||||||
|
decryptBase64(data: string): string {
|
||||||
|
if (!data) return ''
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(atob(data))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Base64解密失败:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密字符串(Base64 + 密钥拼接)
|
||||||
|
* @param data 需要加密的字符串
|
||||||
|
* @returns 加密后的字符串
|
||||||
|
*/
|
||||||
|
encrypt(data: string): string {
|
||||||
|
if (!data) return ''
|
||||||
|
try {
|
||||||
|
const encryptStr = data + this.key
|
||||||
|
return btoa(encodeURIComponent(encryptStr))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加密失败:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密字符串
|
||||||
|
* @param data 需要解密的字符串
|
||||||
|
* @returns 解密后的原始字符串
|
||||||
|
*/
|
||||||
|
decrypt(data: string): string {
|
||||||
|
if (!data) return ''
|
||||||
|
try {
|
||||||
|
const decryptStr = decodeURIComponent(atob(data))
|
||||||
|
return decryptStr.replace(this.key, '')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解密失败:', error)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证加密字符串是否有效(匹配当前密钥)
|
||||||
|
* @param data 加密后的字符串
|
||||||
|
* @returns boolean 有效返回true,否则返回false
|
||||||
|
*/
|
||||||
|
validate(data: string): boolean {
|
||||||
|
try {
|
||||||
|
const decryptStr = this.decrypt(data)
|
||||||
|
return decryptStr !== data && decryptStr.length > 0
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MD5加密(简化版,实际项目建议使用crypto-js)
|
||||||
|
* @param data 需要加密的字符串
|
||||||
|
* @returns MD5哈希值
|
||||||
|
*/
|
||||||
|
static md5(data: string): string {
|
||||||
|
if (!data) return ''
|
||||||
|
// 简化实现,实际项目中建议使用专业的MD5库
|
||||||
|
let hash = 0
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const char = data.charCodeAt(i)
|
||||||
|
hash = ((hash << 5) - hash) + char
|
||||||
|
hash = hash & hash
|
||||||
|
}
|
||||||
|
return Math.abs(hash).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHA256加密
|
||||||
|
* @param data 需要加密的字符串
|
||||||
|
* @returns SHA256哈希值
|
||||||
|
*/
|
||||||
|
static sha256(data: string): Promise<string> {
|
||||||
|
if (!data) return Promise.resolve('')
|
||||||
|
// 使用Web Crypto API
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const dataBuffer = encoder.encode(data)
|
||||||
|
return crypto.subtle.digest('SHA-256', dataBuffer).then(hash => {
|
||||||
|
return Array.from(new Uint8Array(hash))
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串
|
||||||
|
* @param length 字符串长度
|
||||||
|
* @returns 随机字符串
|
||||||
|
*/
|
||||||
|
static randomString(length: number = 32): string {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
let result = ''
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机数字
|
||||||
|
* @param min 最小值
|
||||||
|
* @param max 最大值
|
||||||
|
* @returns 随机数字
|
||||||
|
*/
|
||||||
|
static randomNumber(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL参数编码
|
||||||
|
* @param data 需要编码的数据对象
|
||||||
|
* @returns 编码后的URL参数字符串
|
||||||
|
*/
|
||||||
|
static urlEncode(data: Record<string, any>): string {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
for (const key in data) {
|
||||||
|
if (data[key] !== undefined && data[key] !== null) {
|
||||||
|
params.append(key, String(data[key]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL参数解码
|
||||||
|
* @param query URL参数字符串
|
||||||
|
* @returns 解码后的数据对象
|
||||||
|
*/
|
||||||
|
static urlDecode(query: string): Record<string, string> {
|
||||||
|
const params = new URLSearchParams(query)
|
||||||
|
const result: Record<string, string> = {}
|
||||||
|
params.forEach((value, key) => {
|
||||||
|
result[key] = value
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
181
Web/src/extends/dateEXTEND.ts
Normal file
181
Web/src/extends/dateEXTEND.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* 日期处理工具类(静态工具)
|
||||||
|
* 提供日期格式化、日期差计算等功能
|
||||||
|
*/
|
||||||
|
export class DateEXTEND {
|
||||||
|
/**
|
||||||
|
* 格式化时间戳为本地时间字符串
|
||||||
|
* @param timestamp 时间戳(毫秒)
|
||||||
|
* @returns 格式化后的时间字符串(如:2026-04-09 23:59:59)
|
||||||
|
*/
|
||||||
|
static format(timestamp: number | Date): string {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const second = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期(仅日期部分)
|
||||||
|
* @param timestamp 时间戳(毫秒)
|
||||||
|
* @returns 格式化后的日期字符串(如:2026-04-09)
|
||||||
|
*/
|
||||||
|
static formatDate(timestamp: number | Date): string {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间(仅时间部分)
|
||||||
|
* @param timestamp 时间戳(毫秒)
|
||||||
|
* @returns 格式化后的时间字符串(如:23:59:59)
|
||||||
|
*/
|
||||||
|
static formatTime(timestamp: number | Date): string {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const second = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${hour}:${minute}:${second}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两个时间戳的天数差
|
||||||
|
* @param start 开始时间戳
|
||||||
|
* @param end 结束时间戳
|
||||||
|
* @returns 天数差(正数:end在start之后;负数:end在start之前)
|
||||||
|
*/
|
||||||
|
static diffDay(start: number, end: number): number {
|
||||||
|
const oneDay = 1000 * 60 * 60 * 24
|
||||||
|
return Math.floor((end - start) / oneDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两个时间戳的小时差
|
||||||
|
* @param start 开始时间戳
|
||||||
|
* @param end 结束时间戳
|
||||||
|
* @returns 小时差
|
||||||
|
*/
|
||||||
|
static diffHour(start: number, end: number): number {
|
||||||
|
const oneHour = 1000 * 60 * 60
|
||||||
|
return Math.floor((end - start) / oneHour)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两个时间戳的分钟差
|
||||||
|
* @param start 开始时间戳
|
||||||
|
* @param end 结束时间戳
|
||||||
|
* @returns 分钟差
|
||||||
|
*/
|
||||||
|
static diffMinute(start: number, end: number): number {
|
||||||
|
const oneMinute = 1000 * 60
|
||||||
|
return Math.floor((end - start) / oneMinute)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为今天
|
||||||
|
* @param timestamp 时间戳(毫秒)
|
||||||
|
* @returns boolean 是今天返回true,否则返回false
|
||||||
|
*/
|
||||||
|
static isToday(timestamp: number | Date): boolean {
|
||||||
|
const today = new Date()
|
||||||
|
const target = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||||
|
return (
|
||||||
|
today.getFullYear() === target.getFullYear() &&
|
||||||
|
today.getMonth() === target.getMonth() &&
|
||||||
|
today.getDate() === target.getDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为昨天
|
||||||
|
* @param timestamp 时间戳(毫秒)
|
||||||
|
* @returns boolean 是昨天返回true,否则返回false
|
||||||
|
*/
|
||||||
|
static isYesterday(timestamp: number | Date): boolean {
|
||||||
|
const yesterday = new Date()
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
const target = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
||||||
|
return (
|
||||||
|
yesterday.getFullYear() === target.getFullYear() &&
|
||||||
|
yesterday.getMonth() === target.getMonth() &&
|
||||||
|
yesterday.getDate() === target.getDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能时间显示(聊天场景使用)
|
||||||
|
* @param timestamp 时间戳(毫秒)
|
||||||
|
* @returns 智能时间字符串
|
||||||
|
*/
|
||||||
|
static smartFormat(timestamp: number | Date): string {
|
||||||
|
const now = Date.now()
|
||||||
|
const target = timestamp instanceof Date ? timestamp.getTime() : timestamp
|
||||||
|
const diff = now - target
|
||||||
|
|
||||||
|
// 小于1分钟
|
||||||
|
if (diff < 60 * 1000) {
|
||||||
|
return '刚刚'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小于1小时
|
||||||
|
if (diff < 60 * 60 * 1000) {
|
||||||
|
return `${Math.floor(diff / (60 * 1000))}分钟前`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小于24小时
|
||||||
|
if (diff < 24 * 60 * 60 * 1000) {
|
||||||
|
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 今天是显示时间
|
||||||
|
if (this.isToday(target)) {
|
||||||
|
return this.formatTime(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 昨天显示昨天 + 时间
|
||||||
|
if (this.isYesterday(target)) {
|
||||||
|
return `昨天 ${this.formatTime(target)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他显示日期时间
|
||||||
|
return this.format(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前时间戳(毫秒)
|
||||||
|
* @returns 当前时间戳
|
||||||
|
*/
|
||||||
|
static now(): number {
|
||||||
|
return Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今天开始的时间戳
|
||||||
|
* @returns 今天开始的时间戳
|
||||||
|
*/
|
||||||
|
static getTodayStart(): number {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
return today.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今天结束的时间戳
|
||||||
|
* @returns 今天结束的时间戳
|
||||||
|
*/
|
||||||
|
static getTodayEnd(): number {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(23, 59, 59, 999)
|
||||||
|
return today.getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
356
Web/src/extends/requestEXTEND.ts
Normal file
356
Web/src/extends/requestEXTEND.ts
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* 网络请求工具类(支持实例化)
|
||||||
|
* 基于ofetch封装统一的请求逻辑,支持拦截器配置
|
||||||
|
*/
|
||||||
|
export class RequestEXTEND {
|
||||||
|
private baseURL: string
|
||||||
|
private timeout: number
|
||||||
|
private headers: Record<string, string>
|
||||||
|
|
||||||
|
// 静态默认配置
|
||||||
|
private static defaultBaseURL = ''
|
||||||
|
private static defaultTimeout = 30000
|
||||||
|
private static defaultHeaders: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态拦截器
|
||||||
|
private static requestInterceptors: Array<{
|
||||||
|
onFulfilled?: (config: any) => any
|
||||||
|
onRejected?: (error: any) => any
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
private static responseInterceptors: Array<{
|
||||||
|
onFulfilled?: (response: any) => any
|
||||||
|
onRejected?: (error: any) => any
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数:初始化请求配置
|
||||||
|
* @param config 自定义请求配置(可选)
|
||||||
|
*/
|
||||||
|
constructor(config?: {
|
||||||
|
baseURL?: string
|
||||||
|
timeout?: number
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}) {
|
||||||
|
this.baseURL = config?.baseURL || RequestEXTEND.defaultBaseURL
|
||||||
|
this.timeout = config?.timeout || RequestEXTEND.defaultTimeout
|
||||||
|
this.headers = {
|
||||||
|
...RequestEXTEND.defaultHeaders,
|
||||||
|
...config?.headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置静态默认配置
|
||||||
|
*/
|
||||||
|
static setDefaultConfig(config: {
|
||||||
|
baseURL?: string
|
||||||
|
timeout?: number
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}) {
|
||||||
|
if (config.baseURL) this.defaultBaseURL = config.baseURL
|
||||||
|
if (config.timeout) this.defaultTimeout = config.timeout
|
||||||
|
if (config.headers) this.defaultHeaders = { ...this.defaultHeaders, ...config.headers }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加请求拦截器(静态方法)
|
||||||
|
*/
|
||||||
|
static addRequestInterceptor(interceptor: {
|
||||||
|
onFulfilled?: (config: any) => any
|
||||||
|
onRejected?: (error: any) => any
|
||||||
|
}) {
|
||||||
|
this.requestInterceptors.push(interceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加响应拦截器(静态方法)
|
||||||
|
*/
|
||||||
|
static addResponseInterceptor(interceptor: {
|
||||||
|
onFulfilled?: (response: any) => any
|
||||||
|
onRejected?: (error: any) => any
|
||||||
|
}) {
|
||||||
|
this.responseInterceptors.push(interceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行请求拦截器链
|
||||||
|
*/
|
||||||
|
private async executeRequestInterceptors(config: any): Promise<any> {
|
||||||
|
let result = config
|
||||||
|
for (const interceptor of RequestEXTEND.requestInterceptors) {
|
||||||
|
try {
|
||||||
|
if (interceptor.onFulfilled) {
|
||||||
|
result = await interceptor.onFulfilled(result)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (interceptor.onRejected) {
|
||||||
|
result = await interceptor.onRejected(error)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行响应拦截器链
|
||||||
|
*/
|
||||||
|
private async executeResponseInterceptors(response: any): Promise<any> {
|
||||||
|
let result = response
|
||||||
|
for (const interceptor of RequestEXTEND.responseInterceptors) {
|
||||||
|
try {
|
||||||
|
if (interceptor.onFulfilled) {
|
||||||
|
result = await interceptor.onFulfilled(result)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (interceptor.onRejected) {
|
||||||
|
result = await interceptor.onRejected(error)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建请求URL
|
||||||
|
*/
|
||||||
|
private buildURL(url: string, params?: Record<string, any>): string {
|
||||||
|
let fullURL = url
|
||||||
|
// 处理相对路径
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
fullURL = this.baseURL + url
|
||||||
|
}
|
||||||
|
// 处理查询参数
|
||||||
|
if (params && Object.keys(params).length > 0) {
|
||||||
|
const searchParams = new URLSearchParams()
|
||||||
|
for (const key in params) {
|
||||||
|
const value = params[key]
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
searchParams.append(key, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const queryString = searchParams.toString()
|
||||||
|
if (queryString) {
|
||||||
|
fullURL += (fullURL.includes('?') ? '&' : '?') + queryString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fullURL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用请求方法
|
||||||
|
*/
|
||||||
|
private async request<T = any>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
options: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
data?: any
|
||||||
|
headers?: Record<string, string>
|
||||||
|
} = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const { params, data, headers } = options
|
||||||
|
|
||||||
|
// 构建配置
|
||||||
|
const config: any = {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
...headers
|
||||||
|
},
|
||||||
|
timeout: this.timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加Body(GET/HEAD请求不添加body)
|
||||||
|
if (data && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
config.body = data
|
||||||
|
delete config.headers['Content-Type']
|
||||||
|
} else {
|
||||||
|
config.body = JSON.stringify(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理查询参数(GET请求)
|
||||||
|
if (params && method.toUpperCase() === 'GET') {
|
||||||
|
url = this.buildURL(url, params)
|
||||||
|
} else {
|
||||||
|
url = this.buildURL(url, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行请求拦截器
|
||||||
|
config.url = url
|
||||||
|
const interceptedConfig = await this.executeRequestInterceptors(config)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 发起请求
|
||||||
|
const response = await fetch(interceptedConfig.url, {
|
||||||
|
method: interceptedConfig.method,
|
||||||
|
headers: interceptedConfig.headers,
|
||||||
|
body: interceptedConfig.body,
|
||||||
|
signal: interceptedConfig.timeout ? AbortSignal.timeout(interceptedConfig.timeout) : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
let result
|
||||||
|
const contentType = response.headers.get('content-type')
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
result = await response.json()
|
||||||
|
} else {
|
||||||
|
result = await response.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装响应
|
||||||
|
const wrappedResponse = {
|
||||||
|
data: result,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: response.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行响应拦截器
|
||||||
|
return await this.executeResponseInterceptors(wrappedResponse)
|
||||||
|
} catch (error: any) {
|
||||||
|
// 执行错误拦截器
|
||||||
|
const errorResponse = {
|
||||||
|
message: error.message || '网络请求失败',
|
||||||
|
code: error.code || 'NETWORK_ERROR',
|
||||||
|
status: error.status || 0
|
||||||
|
}
|
||||||
|
throw errorResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET请求
|
||||||
|
*/
|
||||||
|
async get<T = any>(url: string, options?: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}): Promise<T> {
|
||||||
|
return this.request<T>('GET', url, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST请求
|
||||||
|
*/
|
||||||
|
async post<T = any>(url: string, data?: any, options?: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}): Promise<T> {
|
||||||
|
return this.request<T>('POST', url, { ...options, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT请求
|
||||||
|
*/
|
||||||
|
async put<T = any>(url: string, data?: any, options?: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}): Promise<T> {
|
||||||
|
return this.request<T>('PUT', url, { ...options, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE请求
|
||||||
|
*/
|
||||||
|
async delete<T = any>(url: string, options?: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}): Promise<T> {
|
||||||
|
return this.request<T>('DELETE', url, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH请求
|
||||||
|
*/
|
||||||
|
async patch<T = any>(url: string, data?: any, options?: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}): Promise<T> {
|
||||||
|
return this.request<T>('PATCH', url, { ...options, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传
|
||||||
|
*/
|
||||||
|
async upload<T = any>(url: string, file: File | FormData, options?: {
|
||||||
|
params?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}): Promise<T> {
|
||||||
|
const formData = file instanceof FormData ? file : new FormData()
|
||||||
|
if (file instanceof File) {
|
||||||
|
formData.append('file', file)
|
||||||
|
}
|
||||||
|
return this.request<T>('POST', url, {
|
||||||
|
...options,
|
||||||
|
data: formData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件下载
|
||||||
|
*/
|
||||||
|
async download(url: string, filename?: string): Promise<void> {
|
||||||
|
const response = await fetch(this.buildURL(url), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.headers
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('下载失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
const downloadURL = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadURL
|
||||||
|
link.download = filename || 'download'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(downloadURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出默认实例
|
||||||
|
export const request = new RequestEXTEND()
|
||||||
|
|
||||||
|
// 添加默认的Token拦截器(示例)
|
||||||
|
RequestEXTEND.addRequestInterceptor({
|
||||||
|
onFulfilled: (config) => {
|
||||||
|
// 从localStorage获取Token
|
||||||
|
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('token') : ''
|
||||||
|
if (token) {
|
||||||
|
config.headers = {
|
||||||
|
...config.headers,
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加默认的响应错误处理拦截器
|
||||||
|
RequestEXTEND.addResponseInterceptor({
|
||||||
|
onRejected: (error: any) => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
// Token过期,清除登录状态
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
}
|
||||||
|
// 跳转到登录页
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
260
Web/src/extends/validateEXTEND.ts
Normal file
260
Web/src/extends/validateEXTEND.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* 表单校验工具类(静态工具)
|
||||||
|
* 提供手机号、邮箱、身份证等常用校验功能
|
||||||
|
*/
|
||||||
|
export class ValidateEXTEND {
|
||||||
|
/**
|
||||||
|
* 校验手机号(中国大陆)
|
||||||
|
* @param phone 手机号
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isPhone(phone: string): boolean {
|
||||||
|
if (!phone) return false
|
||||||
|
const reg = /^1[3-9]\d{9}$/
|
||||||
|
return reg.test(phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验邮箱
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isEmail(email: string): boolean {
|
||||||
|
if (!email) return false
|
||||||
|
const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
return reg.test(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验身份证号(中国大陆)
|
||||||
|
* @param idCard 身份证号
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isIdCard(idCard: string): boolean {
|
||||||
|
if (!idCard) return false
|
||||||
|
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||||
|
return reg.test(idCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验URL
|
||||||
|
* @param url URL地址
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isUrl(url: string): boolean {
|
||||||
|
if (!url) return false
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验IP地址
|
||||||
|
* @param ip IP地址
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isIP(ip: string): boolean {
|
||||||
|
if (!ip) return false
|
||||||
|
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
|
||||||
|
return reg.test(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验邮政编码(中国大陆)
|
||||||
|
* @param code 邮政编码
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isPostalCode(code: string): boolean {
|
||||||
|
if (!code) return false
|
||||||
|
const reg = /^[1-9]\d{5}$/
|
||||||
|
return reg.test(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验车牌号(中国大陆)
|
||||||
|
* @param plate 车牌号
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isCarPlate(plate: string): boolean {
|
||||||
|
if (!plate) return false
|
||||||
|
const reg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]$/
|
||||||
|
return reg.test(plate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验用户名(字母开头,允许字母数字下划线)
|
||||||
|
* @param username 用户名
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isUsername(username: string): boolean {
|
||||||
|
if (!username) return false
|
||||||
|
const reg = /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/
|
||||||
|
return reg.test(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验密码强度(8-20位,包含字母和数字)
|
||||||
|
* @param password 密码
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isPassword(password: string): boolean {
|
||||||
|
if (!password) return false
|
||||||
|
const reg = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,20}$/
|
||||||
|
return reg.test(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验强密码(8-20位,包含大小写字母和数字)
|
||||||
|
* @param password 密码
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isStrongPassword(password: string): boolean {
|
||||||
|
if (!password) return false
|
||||||
|
const reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/
|
||||||
|
return reg.test(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验金额(正数,最多两位小数)
|
||||||
|
* @param amount 金额
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isAmount(amount: string | number): boolean {
|
||||||
|
if (amount === '' || amount === null || amount === undefined) return false
|
||||||
|
const reg = /^[0-9]+(\.[0-9]{1,2})?$/
|
||||||
|
return reg.test(String(amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验中文名(2-10个中文字符)
|
||||||
|
* @param name 姓名
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isChineseName(name: string): boolean {
|
||||||
|
if (!name) return false
|
||||||
|
const reg = /^[\u4e00-\u9fa5]{2,10}$/
|
||||||
|
return reg.test(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验QQ号
|
||||||
|
* @param qq QQ号
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isQQ(qq: string): boolean {
|
||||||
|
if (!qq) return false
|
||||||
|
const reg = /^[1-9]\d{4,10}$/
|
||||||
|
return reg.test(qq)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验微信号
|
||||||
|
* @param wechat 微信号
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isWeChat(wechat: string): boolean {
|
||||||
|
if (!wechat) return false
|
||||||
|
const reg = /^[a-zA-Z][a-zA-Z0-9_-]{5,19}$/
|
||||||
|
return reg.test(wechat)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验银行卡号(简单校验Luhn算法)
|
||||||
|
* @param cardNo 银行卡号
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isBankCard(cardNo: string): boolean {
|
||||||
|
if (!cardNo) return false
|
||||||
|
// 去除空格
|
||||||
|
const card = cardNo.replace(/\s/g, '')
|
||||||
|
// 只能是16或19位数字
|
||||||
|
if (!/^\d{16}|\d{19}$/.test(card)) return false
|
||||||
|
// Luhn算法校验
|
||||||
|
let sum = 0
|
||||||
|
let isEven = false
|
||||||
|
for (let i = card.length - 1; i >= 0; i--) {
|
||||||
|
let digit = parseInt(card.charAt(i))
|
||||||
|
if (isEven) {
|
||||||
|
digit *= 2
|
||||||
|
if (digit > 9) {
|
||||||
|
digit -= 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sum += digit
|
||||||
|
isEven = !isEven
|
||||||
|
}
|
||||||
|
return sum % 10 === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验座机电话(中国大陆)
|
||||||
|
* @param phone 座机电话
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isLandline(phone: string): boolean {
|
||||||
|
if (!phone) return false
|
||||||
|
const reg = /^(0\d{2,3}-?)?\d{7,8}$/
|
||||||
|
return reg.test(phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验非空
|
||||||
|
* @param value 值
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static isRequired(value: any): boolean {
|
||||||
|
if (value === null || value === undefined) return false
|
||||||
|
if (typeof value === 'string') return value.trim().length > 0
|
||||||
|
if (Array.isArray(value)) return value.length > 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验最小长度
|
||||||
|
* @param value 值
|
||||||
|
* @param min 最小长度
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static minLength(value: string, min: number): boolean {
|
||||||
|
if (!value) return false
|
||||||
|
return value.trim().length >= min
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验最大长度
|
||||||
|
* @param value 值
|
||||||
|
* @param max 最大长度
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static maxLength(value: string, max: number): boolean {
|
||||||
|
if (!value) return false
|
||||||
|
return value.trim().length <= max
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验范围
|
||||||
|
* @param value 值
|
||||||
|
* @param min 最小值
|
||||||
|
* @param max 最大值
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static inRange(value: number, min: number, max: number): boolean {
|
||||||
|
if (value === null || value === undefined) return false
|
||||||
|
return value >= min && value <= max
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验数组长度范围
|
||||||
|
* @param value 数组
|
||||||
|
* @param min 最小长度
|
||||||
|
* @param max 最大长度
|
||||||
|
* @returns boolean 是否有效
|
||||||
|
*/
|
||||||
|
static arrayLengthInRange(value: any[], min: number, max: number): boolean {
|
||||||
|
if (!Array.isArray(value)) return false
|
||||||
|
return value.length >= min && value.length <= max
|
||||||
|
}
|
||||||
|
}
|
||||||
211
Web/src/layouts/default.vue
Normal file
211
Web/src/layouts/default.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<div class="layout-default">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<header class="layout-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h1 class="logo">Kx UI Framework</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="user-info" v-if="userStore.isLogin">
|
||||||
|
欢迎, {{ userStore.userNickname }}
|
||||||
|
</span>
|
||||||
|
<NuxtLink to="/home" class="header-link" v-if="!userStore.isLogin">登录</NuxtLink>
|
||||||
|
<a href="javascript:;" class="header-link" v-else @click="handleLogout">退出</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 主体内容区域 -->
|
||||||
|
<div class="layout-body">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<aside class="layout-sidebar" :class="{ collapsed: appStore.sidebarCollapsed }">
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<NuxtLink to="/home" class="nav-item" :class="{ active: route.path === '/home' }">
|
||||||
|
<span class="nav-icon">🏠</span>
|
||||||
|
<span class="nav-text">首页</span>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/auth/login" class="nav-item" :class="{ active: route.path === '/auth/login' }">
|
||||||
|
<span class="nav-icon">🔐</span>
|
||||||
|
<span class="nav-text">登录页</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="layout-main">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部 -->
|
||||||
|
<footer class="layout-footer">
|
||||||
|
<p>© 2026 Kx UI Framework. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- 全局加载遮罩 -->
|
||||||
|
<div class="loading-mask" v-if="appStore.isLoading">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>{{ appStore.loadingText || '加载中...' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserStore } from '~/stores/user'
|
||||||
|
import { useAppStore } from '~/stores/app'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
userStore.clearUserInfo()
|
||||||
|
router.push('/auth/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-default {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 20px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left .logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link:hover {
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sidebar {
|
||||||
|
width: 200px;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-sidebar.collapsed {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-footer {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
Web/src/layouts/empty.vue
Normal file
17
Web/src/layouts/empty.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="layout-empty">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 空白布局 - 用于登录页、弹窗、独立页面等不需要导航栏的场景
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-empty {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
469
Web/src/pages/auth/login.vue
Normal file
469
Web/src/pages/auth/login.vue
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-login">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1 class="login-title">登录</h1>
|
||||||
|
<p class="login-subtitle">欢迎使用 Kx UI Framework</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="login-form" @submit.prevent="handleLogin">
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<div class="form-item">
|
||||||
|
<label class="form-label" for="username">用户名</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="loginForm.username"
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
@blur="validateField('username')"
|
||||||
|
/>
|
||||||
|
<span class="form-error" v-if="errors.username">{{ errors.username }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码 -->
|
||||||
|
<div class="form-item">
|
||||||
|
<label class="form-label" for="password">密码</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
@blur="validateField('password')"
|
||||||
|
/>
|
||||||
|
<span class="form-error" v-if="errors.password">{{ errors.password }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证码 -->
|
||||||
|
<div class="form-item">
|
||||||
|
<label class="form-label" for="captcha">验证码</label>
|
||||||
|
<div class="captcha-wrapper">
|
||||||
|
<input
|
||||||
|
id="captcha"
|
||||||
|
v-model="loginForm.captcha"
|
||||||
|
type="text"
|
||||||
|
class="form-input captcha-input"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
@blur="validateField('captcha')"
|
||||||
|
/>
|
||||||
|
<div class="captcha-code" @click="refreshCaptcha">
|
||||||
|
{{ captchaCode }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="form-error" v-if="errors.captcha">{{ errors.captcha }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 记住密码 & 忘记密码 -->
|
||||||
|
<div class="form-options">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="loginForm.remember" type="checkbox" />
|
||||||
|
<span>记住密码</span>
|
||||||
|
</label>
|
||||||
|
<a href="javascript:;" class="forgot-link">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<button type="submit" class="login-btn" :disabled="isLoading">
|
||||||
|
{{ isLoading ? '登录中...' : '登录' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 注册链接 -->
|
||||||
|
<div class="login-footer">
|
||||||
|
<span class="register-tip">还没有账号?</span>
|
||||||
|
<a href="javascript:;" class="register-link">立即注册</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 提示信息 -->
|
||||||
|
<div class="login-tips" v-if="tips.show">
|
||||||
|
<p :class="['tip-text', tips.type]">{{ tips.message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// stores、extends、services 目录下的内容已自动导入
|
||||||
|
// types目录需要手动import type
|
||||||
|
|
||||||
|
import type { ILoginParams } from '~/types/api'
|
||||||
|
|
||||||
|
// 设置页面布局
|
||||||
|
definePageMeta({
|
||||||
|
layout: layout.empty
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pinia状态仓库(自动导入)
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 登录表单数据
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
captcha: '',
|
||||||
|
remember: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单校验错误
|
||||||
|
const errors = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
captcha: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 验证码
|
||||||
|
const captchaCode = ref('')
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
const tips = reactive({
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
type: 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成随机验证码
|
||||||
|
const generateCaptcha = () => {
|
||||||
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||||
|
let code = ''
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
captchaCode.value = code
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新验证码
|
||||||
|
const refreshCaptcha = () => {
|
||||||
|
generateCaptcha()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验单个字段
|
||||||
|
const validateField = (field: keyof typeof errors) => {
|
||||||
|
errors[field] = ''
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case 'username':
|
||||||
|
if (!loginForm.username) {
|
||||||
|
errors.username = '请输入用户名'
|
||||||
|
} else if (!ValidateEXTEND.minLength(loginForm.username, 3)) {
|
||||||
|
errors.username = '用户名至少3个字符'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'password':
|
||||||
|
if (!loginForm.password) {
|
||||||
|
errors.password = '请输入密码'
|
||||||
|
} else if (!ValidateEXTEND.minLength(loginForm.password, 6)) {
|
||||||
|
errors.password = '密码至少6个字符'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'captcha':
|
||||||
|
if (!loginForm.captcha) {
|
||||||
|
errors.captcha = '请输入验证码'
|
||||||
|
} else if (loginForm.captcha.toUpperCase() !== captchaCode.value.toUpperCase()) {
|
||||||
|
errors.captcha = '验证码错误'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return !errors[field]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验整个表单
|
||||||
|
const validateForm = () => {
|
||||||
|
const usernameValid = validateField('username')
|
||||||
|
const passwordValid = validateField('password')
|
||||||
|
const captchaValid = validateField('captcha')
|
||||||
|
return usernameValid && passwordValid && captchaValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提示
|
||||||
|
const showTips = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||||
|
tips.message = message
|
||||||
|
tips.type = type
|
||||||
|
tips.show = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
tips.show = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 校验表单
|
||||||
|
if (!validateForm()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始加载
|
||||||
|
isLoading.value = true
|
||||||
|
userStore.setLoginLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟登录(实际项目中调用API)
|
||||||
|
// const userService = new UserSERVICE()
|
||||||
|
// const result = await userService.login(loginForm as ILoginParams)
|
||||||
|
|
||||||
|
// 模拟登录成功
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// 设置用户信息
|
||||||
|
userStore.setUserInfo(
|
||||||
|
{
|
||||||
|
id: 1001,
|
||||||
|
username: loginForm.username,
|
||||||
|
nickname: loginForm.username,
|
||||||
|
role: 'admin'
|
||||||
|
},
|
||||||
|
'demo-token-' + Date.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
showTips('登录成功!', 'success')
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/home')
|
||||||
|
}, 1000)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
showTips(error.message || '登录失败,请稍后重试', 'error')
|
||||||
|
// 刷新验证码
|
||||||
|
refreshCaptcha()
|
||||||
|
loginForm.captcha = ''
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
userStore.setLoginLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果已登录,跳转到首页
|
||||||
|
if (userStore.isLogin) {
|
||||||
|
router.push('/home')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成验证码
|
||||||
|
generateCaptcha()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-login {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: var(--text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-code {
|
||||||
|
width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-code:hover {
|
||||||
|
background-color: var(--border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type='checkbox'] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover:not(:disabled) {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tips {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text.success {
|
||||||
|
color: var(--success-color);
|
||||||
|
background-color: rgba(103, 194, 58, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text.error {
|
||||||
|
color: var(--danger-color);
|
||||||
|
background-color: rgba(245, 108, 108, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text.info {
|
||||||
|
color: var(--info-color);
|
||||||
|
background-color: rgba(144, 147, 153, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
438
Web/src/pages/home/index.vue
Normal file
438
Web/src/pages/home/index.vue
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-home">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-title">Nuxt4 + Pinia 前端框架 Demo</h1>
|
||||||
|
<p class="page-desc">本页面展示了框架各核心模块的调用方式</p>
|
||||||
|
|
||||||
|
<!-- 1. Pinia状态管理Demo -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h2 class="section-title">1. Pinia 状态管理</h2>
|
||||||
|
<div class="demo-card">
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">登录状态:</span>
|
||||||
|
<span class="value">{{ userStore.isLogin ? '已登录' : '未登录' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">用户ID:</span>
|
||||||
|
<span class="value">{{ userStore.userId || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">用户昵称:</span>
|
||||||
|
<span class="value">{{ userStore.userNickname }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">用户角色:</span>
|
||||||
|
<span class="value">{{ userStore.userRole }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="setUserInfo">设置用户信息</button>
|
||||||
|
<button class="btn btn-danger" @click="clearUserInfo">清除用户信息</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. EXTEND工具库Demo -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h2 class="section-title">2. EXTEND 工具库</h2>
|
||||||
|
<div class="demo-card">
|
||||||
|
<h3 class="card-title">DateEXTEND - 日期工具</h3>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">当前时间戳:</span>
|
||||||
|
<span class="value">{{ currentTimestamp }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">格式化时间:</span>
|
||||||
|
<span class="value">{{ formattedTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">智能时间显示:</span>
|
||||||
|
<span class="value">{{ smartTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">是否今天:</span>
|
||||||
|
<span class="value">{{ isToday ? '是' : '否' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h3 class="card-title">CryptoEXTEND - 加密工具</h3>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">原始字符串:</span>
|
||||||
|
<span class="value">{{ originalText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">Base64加密:</span>
|
||||||
|
<span class="value">{{ encryptedBase64 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">自定义密钥加密:</span>
|
||||||
|
<span class="value">{{ encryptedWithKey }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">自定义密钥解密:</span>
|
||||||
|
<span class="value">{{ decryptedWithKey }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="testCrypto">测试加密解密</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h3 class="card-title">ValidateEXTEND - 校验工具</h3>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">手机号校验:</span>
|
||||||
|
<span class="value">{{ validateResult.phone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">邮箱校验:</span>
|
||||||
|
<span class="value">{{ validateResult.email }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">身份证校验:</span>
|
||||||
|
<span class="value">{{ validateResult.idCard }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">密码强度校验:</span>
|
||||||
|
<span class="value">{{ validateResult.password }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="testValidate">测试校验</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. API服务Demo -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h2 class="section-title">3. API 服务调用</h2>
|
||||||
|
<div class="demo-card">
|
||||||
|
<h3 class="card-title">UserSERVICE - 用户服务</h3>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">服务调用方式:</span>
|
||||||
|
<span class="value">const userService = new UserSERVICE()</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">可用方法:</span>
|
||||||
|
<span class="value">login(), getInfo(), logout(), updateInfo(), changePassword()</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="testUserService">测试用户服务</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-card">
|
||||||
|
<h3 class="card-title">CommonSERVICE - 通用服务</h3>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">服务调用方式:</span>
|
||||||
|
<span class="value">const commonService = new CommonSERVICE()</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">可用方法:</span>
|
||||||
|
<span class="value">getDict(), upload(), getConfig(), sendSmsCode()</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="testCommonService">测试通用服务</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Composables Demo -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h2 class="section-title">4. Composables 组合式函数</h2>
|
||||||
|
<div class="demo-card">
|
||||||
|
<h3 class="card-title">useAuth - 权限校验</h3>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">是否已登录:</span>
|
||||||
|
<span class="value">{{ authState.isLogin ? '是' : '否' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">是否为管理员:</span>
|
||||||
|
<span class="value">{{ authState.isAdmin ? '是' : '否' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="testAuth">测试权限校验</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. App状态Demo -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h2 class="section-title">5. 应用状态管理</h2>
|
||||||
|
<div class="demo-card">
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">当前主题:</span>
|
||||||
|
<span class="value">{{ appStore.theme }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">当前语言:</span>
|
||||||
|
<span class="value">{{ appStore.locale }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">设备类型:</span>
|
||||||
|
<span class="value">{{ appStore.device }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">屏幕尺寸:</span>
|
||||||
|
<span class="value">{{ appStore.screenWidth }} x {{ appStore.screenHeight }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<button class="btn btn-primary" @click="toggleTheme">切换主题</button>
|
||||||
|
<button class="btn" @click="setLoading">测试加载态</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. 路由Demo -->
|
||||||
|
<section class="demo-section">
|
||||||
|
<h2 class="section-title">6. 路由导航</h2>
|
||||||
|
<div class="demo-card">
|
||||||
|
<div class="demo-item">
|
||||||
|
<span class="label">当前路径:</span>
|
||||||
|
<span class="value">{{ route.path }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="demo-actions">
|
||||||
|
<NuxtLink to="/auth/login" class="btn btn-primary">跳转到登录页</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// stores、composables、extends、services 目录下的内容已自动导入
|
||||||
|
// 无需手动import,直接使用即可
|
||||||
|
|
||||||
|
// 使用Pinia状态仓库(自动导入)
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
// 使用Composables
|
||||||
|
const auth = useAuth()
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// DateEXTEND Demo
|
||||||
|
// 使用客户端时间,避免SSR水合问题
|
||||||
|
const currentTimestamp = ref(0)
|
||||||
|
const formattedTime = ref('')
|
||||||
|
const smartTime = ref('')
|
||||||
|
const isToday = ref(false)
|
||||||
|
|
||||||
|
// 在客户端初始化时间
|
||||||
|
if (import.meta.client) {
|
||||||
|
currentTimestamp.value = DateEXTEND.now()
|
||||||
|
formattedTime.value = DateEXTEND.format(Date.now())
|
||||||
|
smartTime.value = DateEXTEND.smartFormat(Date.now() - 3600000)
|
||||||
|
isToday.value = DateEXTEND.isToday(Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CryptoEXTEND Demo
|
||||||
|
const originalText = ref('Hello Kx Framework')
|
||||||
|
const encryptedBase64 = ref('')
|
||||||
|
const encryptedWithKey = ref('')
|
||||||
|
const decryptedWithKey = ref('')
|
||||||
|
|
||||||
|
// ValidateEXTEND Demo
|
||||||
|
const validateResult = ref({
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
idCard: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auth Demo
|
||||||
|
const authState = computed(() => ({
|
||||||
|
isLogin: auth.isLogin.value,
|
||||||
|
isAdmin: auth.isAdmin()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 测试设置用户信息
|
||||||
|
const setUserInfo = () => {
|
||||||
|
userStore.setUserInfo(
|
||||||
|
{
|
||||||
|
id: 1001,
|
||||||
|
username: 'admin',
|
||||||
|
nickname: '管理员',
|
||||||
|
role: 'admin'
|
||||||
|
},
|
||||||
|
'demo-token-12345'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试清除用户信息
|
||||||
|
const clearUserInfo = () => {
|
||||||
|
userStore.clearUserInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试加密解密
|
||||||
|
const testCrypto = () => {
|
||||||
|
// Base64加密
|
||||||
|
const crypto = new CryptoEXTEND()
|
||||||
|
encryptedBase64.value = crypto.encryptBase64(originalText.value)
|
||||||
|
|
||||||
|
// 自定义密钥加密
|
||||||
|
const customCrypto = new CryptoEXTEND('my-secret-key')
|
||||||
|
encryptedWithKey.value = customCrypto.encrypt(originalText.value)
|
||||||
|
decryptedWithKey.value = customCrypto.decrypt(encryptedWithKey.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试校验工具
|
||||||
|
const testValidate = () => {
|
||||||
|
validateResult.value = {
|
||||||
|
phone: ValidateEXTEND.isPhone('13800138000') ? '有效' : '无效',
|
||||||
|
email: ValidateEXTEND.isEmail('test@example.com') ? '有效' : '无效',
|
||||||
|
idCard: ValidateEXTEND.isIdCard('110101199001011234') ? '有效' : '无效',
|
||||||
|
password: ValidateEXTEND.isPassword('Aa123456') ? '有效' : '无效'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试用户服务
|
||||||
|
const testUserService = () => {
|
||||||
|
const userService = new UserSERVICE()
|
||||||
|
console.log('UserSERVICE 实例创建成功:', userService)
|
||||||
|
console.log('可用方法: login(), getInfo(), logout(), updateInfo(), changePassword()')
|
||||||
|
alert('UserSERVICE 实例已创建,请查看控制台输出')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试通用服务
|
||||||
|
const testCommonService = () => {
|
||||||
|
const commonService = new CommonSERVICE()
|
||||||
|
console.log('CommonSERVICE 实例创建成功:', commonService)
|
||||||
|
console.log('可用方法: getDict(), upload(), getConfig(), sendSmsCode()')
|
||||||
|
alert('CommonSERVICE 实例已创建,请查看控制台输出')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试权限校验
|
||||||
|
const testAuth = () => {
|
||||||
|
console.log('isLogin:', auth.isLogin.value)
|
||||||
|
console.log('userId:', auth.getUserId())
|
||||||
|
console.log('nickname:', auth.getNickname())
|
||||||
|
console.log('hasRole(admin):', auth.hasRole('admin'))
|
||||||
|
console.log('isAdmin:', auth.isAdmin())
|
||||||
|
alert('权限校验测试完成,请查看控制台输出')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换主题
|
||||||
|
const toggleTheme = () => {
|
||||||
|
appStore.toggleTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试加载态
|
||||||
|
const setLoading = () => {
|
||||||
|
appStore.startLoading('正在加载数据...')
|
||||||
|
setTimeout(() => {
|
||||||
|
appStore.endLoading()
|
||||||
|
alert('加载完成')
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化测试
|
||||||
|
onMounted(() => {
|
||||||
|
testCrypto()
|
||||||
|
testValidate()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-home {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-card {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-item .label {
|
||||||
|
width: 160px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-item .value {
|
||||||
|
color: var(--text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--bg-color-page);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
Web/src/pages/index.vue
Normal file
13
Web/src/pages/index.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 重定向到 /home -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
router.replace('/home')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
209
Web/src/services/commonSERVICE.ts
Normal file
209
Web/src/services/commonSERVICE.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* 通用API服务
|
||||||
|
* 提供字典查询、文件上传等通用接口
|
||||||
|
*/
|
||||||
|
import type { IDictParams, IDictResponse, IUploadParams } from '~/types/api'
|
||||||
|
import type { IUploadResponse, IPageResponse, IPageParams } from '~/types/common'
|
||||||
|
import { RequestEXTEND } from '~/extends/requestEXTEND'
|
||||||
|
|
||||||
|
export class CommonSERVICE {
|
||||||
|
// 私有属性:请求工具实例
|
||||||
|
private request: RequestEXTEND
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数:初始化请求工具
|
||||||
|
* @param config 自定义请求配置(可选)
|
||||||
|
*/
|
||||||
|
constructor(config?: { timeout?: number; headers?: Record<string, string>; baseURL?: string }) {
|
||||||
|
this.request = new RequestEXTEND(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典查询接口
|
||||||
|
* @param type 字典类型
|
||||||
|
* @returns 字典项列表
|
||||||
|
*/
|
||||||
|
async getDict(type: string): Promise<IDictResponse['data']> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.get<IDictResponse>('/api/common/dict', {
|
||||||
|
params: { type }
|
||||||
|
})
|
||||||
|
if (response.code !== 200 && response.code !== 0) {
|
||||||
|
throw new Error(response.message || '获取字典失败')
|
||||||
|
}
|
||||||
|
return response.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取字典接口异常:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传接口
|
||||||
|
* @param file 要上传的文件
|
||||||
|
* @returns 上传后的文件信息
|
||||||
|
*/
|
||||||
|
async upload(file: File): Promise<IUploadResponse['data']> {
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const response = await this.request.post<IUploadResponse>('/api/common/upload', formData)
|
||||||
|
if (response.code !== 200 && response.code !== 0) {
|
||||||
|
throw new Error(response.message || '文件上传失败')
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('文件上传接口异常:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多文件上传接口
|
||||||
|
* @param files 要上传的文件数组
|
||||||
|
* @returns 上传后的文件信息列表
|
||||||
|
*/
|
||||||
|
async uploadMultiple(files: File[]): Promise<IUploadResponse['data'][]> {
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
formData.append(`files_${index}`, file)
|
||||||
|
})
|
||||||
|
const response = await this.request.post<{ code: number; message: string; data: IUploadResponse['data'][] }>(
|
||||||
|
'/api/common/uploadMultiple',
|
||||||
|
formData
|
||||||
|
)
|
||||||
|
if (response.code !== 200 && response.code !== 0) {
|
||||||
|
throw new Error(response.message || '文件上传失败')
|
||||||
|
}
|
||||||
|
return response.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('多文件上传接口异常:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取上传进度(需要自行实现)
|
||||||
|
* @param file 要上传的文件
|
||||||
|
* @param onProgress 上传进度回调
|
||||||
|
* @returns 上传后的文件信息
|
||||||
|
*/
|
||||||
|
async uploadWithProgress(
|
||||||
|
file: File,
|
||||||
|
onProgress: (percent: number) => void
|
||||||
|
): Promise<IUploadResponse['data']> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
const percent = Math.round((event.loaded / event.total) * 100)
|
||||||
|
onProgress(percent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText)
|
||||||
|
if (response.code === 200 || response.code === 0) {
|
||||||
|
resolve(response.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error(response.message || '文件上传失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error('文件上传失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new Error('文件上传失败'))
|
||||||
|
})
|
||||||
|
|
||||||
|
xhr.open('POST', '/api/common/upload')
|
||||||
|
xhr.send(formData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置信息
|
||||||
|
* @param key 配置键
|
||||||
|
* @returns 配置值
|
||||||
|
*/
|
||||||
|
async getConfig(key: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.get<{ code: number; message: string; data: { value: string } }>(
|
||||||
|
'/api/common/config',
|
||||||
|
{ params: { key } }
|
||||||
|
)
|
||||||
|
if (response.code === 200 || response.code === 0) {
|
||||||
|
return response.data?.value || null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置接口异常:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用版本信息
|
||||||
|
* @returns 版本信息
|
||||||
|
*/
|
||||||
|
async getVersion(): Promise<{ version: string; buildTime: string }> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.get<{ code: number; message: string; data: { version: string; buildTime: string } }>('/api/common/version')
|
||||||
|
if (response.code === 200 || response.code === 0) {
|
||||||
|
return response.data || { version: '1.0.0', buildTime: '' }
|
||||||
|
}
|
||||||
|
return { version: '1.0.0', buildTime: '' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取版本信息接口异常:', error)
|
||||||
|
return { version: '1.0.0', buildTime: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码
|
||||||
|
* @param phone 手机号
|
||||||
|
* @param type 验证码类型(login/register/reset)
|
||||||
|
* @returns 发送结果
|
||||||
|
*/
|
||||||
|
async sendSmsCode(phone: string, type: string = 'login'): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.post<{ code: number; message: string }>('/api/common/sms/send', {
|
||||||
|
phone,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
return response.code === 200 || response.code === 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送短信验证码接口异常:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证短信验证码
|
||||||
|
* @param phone 手机号
|
||||||
|
* @param code 验证码
|
||||||
|
* @returns 验证结果
|
||||||
|
*/
|
||||||
|
async verifySmsCode(phone: string, code: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.post<{ code: number; message: string }>('/api/common/sms/verify', {
|
||||||
|
phone,
|
||||||
|
code
|
||||||
|
})
|
||||||
|
return response.code === 200 || response.code === 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证短信验证码接口异常:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
Web/src/services/userSERVICE.ts
Normal file
105
Web/src/services/userSERVICE.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关API服务
|
||||||
|
* 提供用户登录、获取用户信息、退出登录等接口
|
||||||
|
*/
|
||||||
|
import type { ILoginParams, ILoginResponse, IGetUserInfoResponse, ILogoutResponse } from '~/types/api'
|
||||||
|
import { RequestEXTEND } from '~/extends/requestEXTEND'
|
||||||
|
|
||||||
|
export class UserSERVICE {
|
||||||
|
// 私有属性:请求工具实例
|
||||||
|
private request: RequestEXTEND
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数:初始化请求工具
|
||||||
|
* @param config 自定义请求配置(可选)
|
||||||
|
*/
|
||||||
|
constructor(config?: { timeout?: number; headers?: Record<string, string>; baseURL?: string }) {
|
||||||
|
this.request = new RequestEXTEND(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录接口
|
||||||
|
* @param params 登录请求参数
|
||||||
|
* @returns 登录响应数据
|
||||||
|
*/
|
||||||
|
async login(params: ILoginParams): Promise<ILoginResponse['data']> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.post<ILoginResponse>('/api/user/login', params)
|
||||||
|
if (response.code !== 200 && response.code !== 0) {
|
||||||
|
throw new Error(response.message || '登录失败')
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录接口异常:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息接口(需携带Token)
|
||||||
|
* @param userId 用户ID(可选,默认取当前登录用户ID)
|
||||||
|
* @returns 用户信息
|
||||||
|
*/
|
||||||
|
async getInfo(userId?: number): Promise<IGetUserInfoResponse['data']> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.get<IGetUserInfoResponse>('/api/user/info', {
|
||||||
|
params: userId ? { userId } : undefined
|
||||||
|
})
|
||||||
|
if (response.code !== 200 && response.code !== 0) {
|
||||||
|
throw new Error(response.message || '获取用户信息失败')
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息接口异常:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录接口
|
||||||
|
* @returns 退出结果
|
||||||
|
*/
|
||||||
|
async logout(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.post<ILogoutResponse>('/api/user/logout')
|
||||||
|
return response.code === 200 || response.code === 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('退出登录接口异常:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
* @param data 用户信息
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateInfo(data: Partial<IGetUserInfoResponse['data']>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.put<ILogoutResponse>('/api/user/update', data)
|
||||||
|
return response.code === 200 || response.code === 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户信息接口异常:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param oldPassword 旧密码
|
||||||
|
* @param newPassword 新密码
|
||||||
|
* @returns 修改结果
|
||||||
|
*/
|
||||||
|
async changePassword(oldPassword: string, newPassword: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.request.post<ILogoutResponse>('/api/user/changePassword', {
|
||||||
|
oldPassword,
|
||||||
|
newPassword
|
||||||
|
})
|
||||||
|
return response.code === 200 || response.code === 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码接口异常:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
Web/src/stores/app.ts
Normal file
216
Web/src/stores/app.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* 应用状态仓库
|
||||||
|
* 管理全局应用状态,如主题、加载态、配置等
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
// 主题类型
|
||||||
|
export type ThemeMode = 'light' | 'dark' | 'auto'
|
||||||
|
|
||||||
|
// 语言类型
|
||||||
|
export type Locale = 'zh-CN' | 'en-US'
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
export interface IAppConfig {
|
||||||
|
theme: ThemeMode
|
||||||
|
locale: Locale
|
||||||
|
sidebarCollapsed: boolean
|
||||||
|
showDebug: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// localStorage key
|
||||||
|
const STORAGE_KEY = 'app-config-store'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', {
|
||||||
|
// 1. 原始状态
|
||||||
|
state: () => ({
|
||||||
|
// 主题模式
|
||||||
|
theme: 'light' as ThemeMode,
|
||||||
|
// 当前语言
|
||||||
|
locale: 'zh-CN' as Locale,
|
||||||
|
// 侧边栏是否收起
|
||||||
|
sidebarCollapsed: false,
|
||||||
|
// 是否显示调试信息
|
||||||
|
showDebug: false,
|
||||||
|
// 全局加载态
|
||||||
|
isLoading: false,
|
||||||
|
// 加载提示文字
|
||||||
|
loadingText: '',
|
||||||
|
// 设备类型
|
||||||
|
device: 'desktop' as 'desktop' | 'mobile' | 'tablet',
|
||||||
|
// 浏览器是否在线
|
||||||
|
isOnline: true,
|
||||||
|
// 屏幕宽度
|
||||||
|
screenWidth: 0,
|
||||||
|
// 屏幕高度
|
||||||
|
screenHeight: 0
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 2. 只读计算属性
|
||||||
|
getters: {
|
||||||
|
// 判断是否为浅色主题
|
||||||
|
isLightTheme: (state) => state.theme === 'light',
|
||||||
|
|
||||||
|
// 判断是否为深色主题
|
||||||
|
isDarkTheme: (state) => state.theme === 'dark',
|
||||||
|
|
||||||
|
// 判断是否为移动端
|
||||||
|
isMobile: (state) => state.device === 'mobile',
|
||||||
|
|
||||||
|
// 判断是否为平板
|
||||||
|
isTablet: (state) => state.device === 'tablet',
|
||||||
|
|
||||||
|
// 判断是否为桌面端
|
||||||
|
isDesktop: (state) => state.device === 'desktop',
|
||||||
|
|
||||||
|
// 获取主题class
|
||||||
|
themeClass: (state) => `theme-${state.theme}`,
|
||||||
|
|
||||||
|
// 判断是否有加载态
|
||||||
|
hasLoading: (state) => state.isLoading
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. 状态修改入口
|
||||||
|
actions: {
|
||||||
|
// 设置主题
|
||||||
|
setTheme(theme: ThemeMode) {
|
||||||
|
this.theme = theme
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
// 应用主题到html元素
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换主题
|
||||||
|
toggleTheme() {
|
||||||
|
this.theme = this.theme === 'light' ? 'dark' : 'light'
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.setAttribute('data-theme', this.theme)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置语言
|
||||||
|
setLocale(locale: Locale) {
|
||||||
|
this.locale = locale
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换侧边栏收起状态
|
||||||
|
toggleSidebar() {
|
||||||
|
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置侧边栏收起状态
|
||||||
|
setSidebarCollapsed(collapsed: boolean) {
|
||||||
|
this.sidebarCollapsed = collapsed
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置调试模式
|
||||||
|
setShowDebug(show: boolean) {
|
||||||
|
this.showDebug = show
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 开始加载
|
||||||
|
startLoading(text: string = '加载中...') {
|
||||||
|
this.isLoading = true
|
||||||
|
this.loadingText = text
|
||||||
|
},
|
||||||
|
|
||||||
|
// 结束加载
|
||||||
|
endLoading() {
|
||||||
|
this.isLoading = false
|
||||||
|
this.loadingText = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置设备类型
|
||||||
|
setDevice(device: 'desktop' | 'mobile' | 'tablet') {
|
||||||
|
this.device = device
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置网络状态
|
||||||
|
setOnlineStatus(isOnline: boolean) {
|
||||||
|
this.isOnline = isOnline
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新屏幕尺寸
|
||||||
|
updateScreenSize(width: number, height: number) {
|
||||||
|
this.screenWidth = width
|
||||||
|
this.screenHeight = height
|
||||||
|
|
||||||
|
// 根据宽度自动判断设备类型
|
||||||
|
if (width < 768) {
|
||||||
|
this.device = 'mobile'
|
||||||
|
} else if (width < 1024) {
|
||||||
|
this.device = 'tablet'
|
||||||
|
} else {
|
||||||
|
this.device = 'desktop'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化应用配置
|
||||||
|
initConfig(config: Partial<IAppConfig>) {
|
||||||
|
if (config.theme) this.setTheme(config.theme)
|
||||||
|
if (config.locale) this.setLocale(config.locale)
|
||||||
|
if (config.sidebarCollapsed !== undefined) this.setSidebarCollapsed(config.sidebarCollapsed)
|
||||||
|
if (config.showDebug !== undefined) this.setShowDebug(config.showDebug)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置所有状态
|
||||||
|
reset() {
|
||||||
|
this.theme = 'light'
|
||||||
|
this.locale = 'zh-CN'
|
||||||
|
this.sidebarCollapsed = false
|
||||||
|
this.showDebug = false
|
||||||
|
this.isLoading = false
|
||||||
|
this.loadingText = ''
|
||||||
|
this.clearLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 同步到localStorage
|
||||||
|
syncToLocalStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const data = {
|
||||||
|
theme: this.theme,
|
||||||
|
locale: this.locale,
|
||||||
|
sidebarCollapsed: this.sidebarCollapsed,
|
||||||
|
showDebug: this.showDebug
|
||||||
|
}
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从localStorage恢复
|
||||||
|
restoreFromLocalStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(stored)
|
||||||
|
this.theme = data.theme || 'light'
|
||||||
|
this.locale = data.locale || 'zh-CN'
|
||||||
|
this.sidebarCollapsed = data.sidebarCollapsed || false
|
||||||
|
this.showDebug = data.showDebug || false
|
||||||
|
// 应用主题
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.setAttribute('data-theme', this.theme)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('恢复应用配置失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除localStorage
|
||||||
|
clearLocalStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
106
Web/src/stores/user.ts
Normal file
106
Web/src/stores/user.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 用户状态仓库
|
||||||
|
* 管理用户登录信息、Token等核心状态
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { IUserInfo } from '~/types/user'
|
||||||
|
|
||||||
|
// 仓库命名规范:use+业务域+Store
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
// 1. 原始状态:仅存基础数据,不做任何计算、判断
|
||||||
|
state: () => ({
|
||||||
|
userInfo: null as IUserInfo | null,
|
||||||
|
token: '',
|
||||||
|
isLoading: false as boolean
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 2. 只读计算属性:封装派生逻辑,全局只读
|
||||||
|
getters: {
|
||||||
|
// 判断是否登录
|
||||||
|
isLogin: (state) => !!state.token,
|
||||||
|
|
||||||
|
// 获取用户ID
|
||||||
|
userId: (state) => state.userInfo?.id ?? 0,
|
||||||
|
|
||||||
|
// 格式化用户昵称
|
||||||
|
userNickname: (state) => state.userInfo?.nickname || state.userInfo?.username || '未知用户',
|
||||||
|
|
||||||
|
// 获取用户头像
|
||||||
|
userAvatar: (state) => state.userInfo?.avatar || '',
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
userRole: (state) => state.userInfo?.role || 'user'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. 唯一状态修改入口:所有状态变更必须走actions
|
||||||
|
actions: {
|
||||||
|
// 设置用户信息与Token
|
||||||
|
setUserInfo(data: IUserInfo, token: string) {
|
||||||
|
this.userInfo = data
|
||||||
|
this.token = token
|
||||||
|
this.isLoading = false
|
||||||
|
// 同步到localStorage
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 仅更新Token
|
||||||
|
setToken(token: string) {
|
||||||
|
this.token = token
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 退出登录:清空用户状态
|
||||||
|
clearUserInfo() {
|
||||||
|
this.userInfo = null
|
||||||
|
this.token = ''
|
||||||
|
this.isLoading = false
|
||||||
|
this.clearLocalStorage()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置登录加载态
|
||||||
|
setLoginLoading(loading: boolean) {
|
||||||
|
this.isLoading = loading
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新用户信息(部分更新)
|
||||||
|
updateUserInfo(partialData: Partial<IUserInfo>) {
|
||||||
|
if (this.userInfo) {
|
||||||
|
this.userInfo = { ...this.userInfo, ...partialData }
|
||||||
|
this.syncToLocalStorage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 同步到localStorage
|
||||||
|
syncToLocalStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('user-auth-store', JSON.stringify({
|
||||||
|
token: this.token,
|
||||||
|
userInfo: this.userInfo
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从localStorage恢复
|
||||||
|
restoreFromLocalStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const stored = localStorage.getItem('user-auth-store')
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(stored)
|
||||||
|
this.token = data.token || ''
|
||||||
|
this.userInfo = data.userInfo || null
|
||||||
|
} catch (e) {
|
||||||
|
console.error('恢复用户状态失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除localStorage
|
||||||
|
clearLocalStorage() {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.removeItem('user-auth-store')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
54
Web/src/types/api.ts
Normal file
54
Web/src/types/api.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* API服务相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IUserInfo } from './user'
|
||||||
|
import type { ICommonResponse, IPageParams, IDictItem } from './common'
|
||||||
|
|
||||||
|
// 用户登录请求参数
|
||||||
|
export type ILoginParams = {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登录响应体
|
||||||
|
export type ILoginResponse = ICommonResponse<{
|
||||||
|
token: string
|
||||||
|
userInfo: IUserInfo
|
||||||
|
}>
|
||||||
|
|
||||||
|
// 获取用户信息响应体
|
||||||
|
export type IGetUserInfoResponse = ICommonResponse<IUserInfo>
|
||||||
|
|
||||||
|
// 退出登录响应体
|
||||||
|
export type ILogoutResponse = ICommonResponse
|
||||||
|
|
||||||
|
// 字典查询请求参数
|
||||||
|
export type IDictParams = {
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典查询响应体
|
||||||
|
export type IDictResponse = ICommonResponse<IDictItem[]>
|
||||||
|
|
||||||
|
// 文件上传请求参数
|
||||||
|
export type IUploadParams = FormData
|
||||||
|
|
||||||
|
// 请求配置选项
|
||||||
|
export interface IRequestConfig {
|
||||||
|
timeout?: number
|
||||||
|
headers?: Record<string, string>
|
||||||
|
baseURL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
export interface IRequestInterceptor {
|
||||||
|
onFulfilled?: (config: any) => any
|
||||||
|
onRejected?: (error: any) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
export interface IResponseInterceptor {
|
||||||
|
onFulfilled?: (response: any) => any
|
||||||
|
onRejected?: (error: any) => any
|
||||||
|
}
|
||||||
63
Web/src/types/common.ts
Normal file
63
Web/src/types/common.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 通用类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 分页请求参数
|
||||||
|
export interface IPageParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应数据
|
||||||
|
export interface IPageData<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应体
|
||||||
|
export interface IPageResponse<T> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: IPageData<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用响应体
|
||||||
|
export interface ICommonResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典项
|
||||||
|
export interface IDictItem {
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字典响应
|
||||||
|
export interface IDictResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: IDictItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传响应
|
||||||
|
export interface IUploadResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: {
|
||||||
|
url: string
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件信息
|
||||||
|
export interface IFileInfo {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
size: number
|
||||||
|
type: string
|
||||||
|
}
|
||||||
39
Web/src/types/user.ts
Normal file
39
Web/src/types/user.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IUserInfo {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
nickname: string
|
||||||
|
avatar?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
role?: string
|
||||||
|
createTime?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoginParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoginResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: {
|
||||||
|
token: string
|
||||||
|
userInfo: IUserInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetUserInfoResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: IUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILogoutResponse {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
44
Web/src/utils/base64.ts
Normal file
44
Web/src/utils/base64.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Base64 编解码工具(底层基础工具)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64编码
|
||||||
|
* @param str 要编码的字符串
|
||||||
|
* @returns 编码后的Base64字符串
|
||||||
|
*/
|
||||||
|
export function encode(str: string): string {
|
||||||
|
return btoa(encodeURIComponent(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64解码
|
||||||
|
* @param base64 要解码的Base64字符串
|
||||||
|
* @returns 解码后的原始字符串
|
||||||
|
*/
|
||||||
|
export function decode(base64: string): string {
|
||||||
|
return decodeURIComponent(atob(base64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL安全的Base64编码
|
||||||
|
* @param str 要编码的字符串
|
||||||
|
* @returns 编码后的Base64字符串
|
||||||
|
*/
|
||||||
|
export function encodeURLSafe(str: string): string {
|
||||||
|
return encode(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL安全的Base64解码
|
||||||
|
* @param base64 要解码的Base64字符串
|
||||||
|
* @returns 解码后的原始字符串
|
||||||
|
*/
|
||||||
|
export function decodeURLSafe(base64: string): string {
|
||||||
|
let str = base64.replace(/-/g, '+').replace(/_/g, '/')
|
||||||
|
// 补齐等号
|
||||||
|
while (str.length % 4) {
|
||||||
|
str += '='
|
||||||
|
}
|
||||||
|
return decode(str)
|
||||||
|
}
|
||||||
141
Web/src/utils/regex.ts
Normal file
141
Web/src/utils/regex.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* 正则校验工具(底层基础工具)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 常用正则表达式
|
||||||
|
export const Regexp = {
|
||||||
|
// 手机号(中国大陆)
|
||||||
|
phone: /^1[3-9]\d{9}$/,
|
||||||
|
|
||||||
|
// 邮箱
|
||||||
|
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||||
|
|
||||||
|
// 身份证号(中国大陆)
|
||||||
|
idCard: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/,
|
||||||
|
|
||||||
|
// URL
|
||||||
|
url: /^https?:\/\/([\w.-]+\.)+[\w.-]+(\/[\w.-]*)*(\?[\w=&.-]*)?$/,
|
||||||
|
|
||||||
|
// IP地址
|
||||||
|
ip: /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/,
|
||||||
|
|
||||||
|
// 邮政编码(中国大陆)
|
||||||
|
postalCode: /^[1-9]\d{5}$/,
|
||||||
|
|
||||||
|
// 用户名(字母开头,允许字母数字下划线)
|
||||||
|
username: /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/,
|
||||||
|
|
||||||
|
// 密码(8-20位,包含字母和数字)
|
||||||
|
password: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,20}$/,
|
||||||
|
|
||||||
|
// 强密码(8-20位,包含大小写字母和数字)
|
||||||
|
strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/,
|
||||||
|
|
||||||
|
// 金额(正数,最多两位小数)
|
||||||
|
amount: /^[0-9]+(\.[0-9]{1,2})?$/,
|
||||||
|
|
||||||
|
// 中文名(2-10个中文字符)
|
||||||
|
chineseName: /^[\u4e00-\u9fa5]{2,10}$/,
|
||||||
|
|
||||||
|
// QQ号
|
||||||
|
qq: /^[1-9]\d{4,10}$/,
|
||||||
|
|
||||||
|
// 微信号
|
||||||
|
wechat: /^[a-zA-Z][a-zA-Z0-9_-]{5,19}$/,
|
||||||
|
|
||||||
|
// 银行卡号(16或19位数字)
|
||||||
|
bankCard: /^\d{16}|\d{19}$/,
|
||||||
|
|
||||||
|
// 座机电话(中国大陆)
|
||||||
|
landline: /^(0\d{2,3}-?)?\d{7,8}$/,
|
||||||
|
|
||||||
|
// 车牌号(中国大陆)
|
||||||
|
carPlate: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]$/,
|
||||||
|
|
||||||
|
// 整数
|
||||||
|
integer: /^-?\d+$/,
|
||||||
|
|
||||||
|
// 正整数
|
||||||
|
positiveInteger: /^[1-9]\d*$/,
|
||||||
|
|
||||||
|
// 负整数
|
||||||
|
negativeInteger: /^-[1-9]\d*$/,
|
||||||
|
|
||||||
|
// 浮点数
|
||||||
|
float: /^-?\d+(\.\d+)?$/,
|
||||||
|
|
||||||
|
// 正浮点数
|
||||||
|
positiveFloat: /^[1-9]\d*(\.\d+)?$|^0\.\d+$/,
|
||||||
|
|
||||||
|
// 颜色值(hex)
|
||||||
|
hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
||||||
|
|
||||||
|
// 日期格式(yyyy-mm-dd)
|
||||||
|
date: /^\d{4}-\d{2}-\d{2}$/,
|
||||||
|
|
||||||
|
// 时间格式(HH:mm:ss)
|
||||||
|
time: /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/,
|
||||||
|
|
||||||
|
// 日期时间格式(yyyy-mm-dd HH:mm:ss)
|
||||||
|
dateTime: /^\d{4}-\d{2}-\d{2} ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/,
|
||||||
|
|
||||||
|
// IPv6地址(简化)
|
||||||
|
ipv6: /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/,
|
||||||
|
|
||||||
|
// Mac地址
|
||||||
|
mac: /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/,
|
||||||
|
|
||||||
|
// 端口号
|
||||||
|
port: /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/,
|
||||||
|
|
||||||
|
// 16进制颜色
|
||||||
|
hex: /^[0-9A-Fa-f]+$/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验字符串是否符合指定正则表达式
|
||||||
|
* @param value 要校验的值
|
||||||
|
* @param pattern 正则表达式
|
||||||
|
* @returns boolean 是否匹配
|
||||||
|
*/
|
||||||
|
export function test(value: string, pattern: RegExp): boolean {
|
||||||
|
if (!value) return false
|
||||||
|
return pattern.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从字符串中提取匹配的内容
|
||||||
|
* @param str 要匹配的字符串
|
||||||
|
* @param pattern 正则表达式
|
||||||
|
* @param group 分组索引(可选)
|
||||||
|
* @returns 匹配的结果数组
|
||||||
|
*/
|
||||||
|
export function match(str: string, pattern: RegExp, group?: number): string | string[] | null {
|
||||||
|
const result = str.match(pattern)
|
||||||
|
if (!result) return null
|
||||||
|
if (group !== undefined) {
|
||||||
|
return result[group] || null
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换字符串中匹配的内容
|
||||||
|
* @param str 要处理的字符串
|
||||||
|
* @param pattern 正则表达式
|
||||||
|
* @param replacement 替换内容
|
||||||
|
* @returns 替换后的字符串
|
||||||
|
*/
|
||||||
|
export function replace(str: string, pattern: RegExp, replacement: string): string {
|
||||||
|
return str.replace(pattern, replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分割字符串
|
||||||
|
* @param str 要处理的字符串
|
||||||
|
* @param pattern 正则表达式
|
||||||
|
* @returns 分割后的字符串数组
|
||||||
|
*/
|
||||||
|
export function split(str: string, pattern: RegExp | string): string[] {
|
||||||
|
return str.split(pattern)
|
||||||
|
}
|
||||||
3
Web/tsconfig.json
Normal file
3
Web/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user