From 84163660bf4a9b230528c41fcabb4af7dc3c8160 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 09:25:00 +0800 Subject: [PATCH 01/81] feat(data-protected): added the default filtering configuration for entities --- .../Abp/DataProtection/AbpDataProtectionOptions.cs | 12 +++++++++--- .../Abp/DataProtection/EntityTypeFilterBuilder.cs | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/AbpDataProtectionOptions.cs b/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/AbpDataProtectionOptions.cs index 9beede4f8..a27a4f65d 100644 --- a/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/AbpDataProtectionOptions.cs +++ b/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/AbpDataProtectionOptions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; @@ -16,15 +17,19 @@ public class AbpDataProtectionOptions /// /// 权限主体 /// - public IList SubjectContributors { get; set; } + public IList SubjectContributors { get; } /// /// 过滤字段关键字 /// - public IDictionary KeywordContributors { get; set; } + public IDictionary KeywordContributors { get; } /// /// 数据操作 /// - public IDictionary OperateContributors { get; set; } + public IDictionary OperateContributors { get; } + /// + /// 默认实体过滤 + /// + public IDictionary> DefaultEntityFilters { get; } /// /// 忽略审计字段列表 /// @@ -35,6 +40,7 @@ public class AbpDataProtectionOptions SubjectContributors = new List(); KeywordContributors = new Dictionary(); OperateContributors = new Dictionary(); + DefaultEntityFilters = new Dictionary>(); IgnoreAuditedProperties = new List { diff --git a/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/EntityTypeFilterBuilder.cs b/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/EntityTypeFilterBuilder.cs index f5f3cd4f9..0d5e358ff 100644 --- a/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/EntityTypeFilterBuilder.cs +++ b/aspnet-core/framework/data-protection/LINGYUN.Abp.DataProtection/LINGYUN/Abp/DataProtection/EntityTypeFilterBuilder.cs @@ -61,6 +61,13 @@ public class EntityTypeFilterBuilder : IEntityTypeFilterBuilder, ITransientDepen } LambdaExpression subExp = null; + + if (subjectFilterGroups.Count == 0 && + _options.DefaultEntityFilters.TryGetValue(entityType, out var filterFunc)) + { + subExp = filterFunc(_serviceProvider, entityType, operation); + } + foreach (var subGroup in subjectFilterGroups) { subExp = subExp == null ? GetExpression(entityType, subGroup) : subExp.OrElse(func, GetExpression(entityType, subGroup)); From 365ca04833e45e84dd76c92581ec2a2cac497b42 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 09:35:03 +0800 Subject: [PATCH 02/81] feat(open-api): api signature is passed from the request header --- .../OpenApiAuthorizationService.cs | 27 +- .../LINGYUN/Abp/OpenApi/AbpOpenApiConsts.cs | 8 +- .../GobalUsings.cs | 7 + ...INGYUN.MicroService.OpenApi.Gateway.csproj | 2 + .../OpenApiGatewayModule.cs | 8 - .../Program.cs | 91 ++-- .../Properties/launchSettings.json | 4 +- .../appsettings.Development.json | 2 +- .../yarp.json | 433 +----------------- 9 files changed, 68 insertions(+), 514 deletions(-) create mode 100644 gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/GobalUsings.cs diff --git a/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi.Authorization/LINGYUN/Abp/OpenApi/Authorization/OpenApiAuthorizationService.cs b/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi.Authorization/LINGYUN/Abp/OpenApi/Authorization/OpenApiAuthorizationService.cs index 31ede4797..e6a0f36a5 100644 --- a/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi.Authorization/LINGYUN/Abp/OpenApi/Authorization/OpenApiAuthorizationService.cs +++ b/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi.Authorization/LINGYUN/Abp/OpenApi/Authorization/OpenApiAuthorizationService.cs @@ -84,18 +84,18 @@ namespace LINGYUN.Abp.OpenApi.Authorization protected async virtual Task ValidatAppDescriptor(HttpContext httpContext) { - httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.AppKeyFieldName, out var appKey); - httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.SignatureFieldName, out var sign); - httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.NonceFieldName, out var nonce); - httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.TimeStampFieldName, out var timeStampString); + httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.HEADER_APP_KEY, out var appKey); + httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.HEADER_SIGNATURE, out var sign); + httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.HEADER_NONCE, out var nonce); + httpContext.Request.Headers.TryGetValue(AbpOpenApiConsts.HEADER_TIMESTAMP, out var timeStampString); if (StringValues.IsNullOrEmpty(appKey)) { var exception = new BusinessException( AbpOpenApiConsts.InvalidAccessWithAppKeyNotFound, - $"{AbpOpenApiConsts.AppKeyFieldName} Not Found", - $"{AbpOpenApiConsts.AppKeyFieldName} Not Found"); + $"{AbpOpenApiConsts.HEADER_APP_KEY} Not Found", + $"{AbpOpenApiConsts.HEADER_APP_KEY} Not Found"); await Unauthorized(httpContext, exception); return false; } @@ -104,8 +104,8 @@ namespace LINGYUN.Abp.OpenApi.Authorization { var exception = new BusinessException( AbpOpenApiConsts.InvalidAccessWithNonceNotFound, - $"{AbpOpenApiConsts.NonceFieldName} Not Found", - $"{AbpOpenApiConsts.NonceFieldName} Not Found"); + $"{AbpOpenApiConsts.HEADER_NONCE} Not Found", + $"{AbpOpenApiConsts.HEADER_NONCE} Not Found"); await Unauthorized(httpContext, exception); return false; @@ -115,8 +115,8 @@ namespace LINGYUN.Abp.OpenApi.Authorization { var exception = new BusinessException( AbpOpenApiConsts.InvalidAccessWithSignNotFound, - $"{AbpOpenApiConsts.SignatureFieldName} Not Found", - $"{AbpOpenApiConsts.SignatureFieldName} Not Found"); + $"{AbpOpenApiConsts.HEADER_SIGNATURE} Not Found", + $"{AbpOpenApiConsts.HEADER_SIGNATURE} Not Found"); await Unauthorized(httpContext, exception); return false; @@ -126,8 +126,8 @@ namespace LINGYUN.Abp.OpenApi.Authorization { var exception = new BusinessException( AbpOpenApiConsts.InvalidAccessWithTimestampNotFound, - $"{AbpOpenApiConsts.TimeStampFieldName} Not Found", - $"{AbpOpenApiConsts.TimeStampFieldName} Not Found"); + $"{AbpOpenApiConsts.HEADER_TIMESTAMP} Not Found", + $"{AbpOpenApiConsts.HEADER_TIMESTAMP} Not Found"); await Unauthorized(httpContext, exception); return false; @@ -264,7 +264,8 @@ namespace LINGYUN.Abp.OpenApi.Authorization private static string CalculationSignature(string url, IDictionary queryDictionary) { var queryString = BuildQuery(queryDictionary); - var encodeUrl = UrlEncode(string.Concat(url, "?", queryString)); + // %20 替换 + + var encodeUrl = UrlEncode(string.Concat(url, "?", queryString)).Replace("+", "%20"); return encodeUrl.ToMd5(); } diff --git a/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AbpOpenApiConsts.cs b/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AbpOpenApiConsts.cs index 983065910..c2198df38 100644 --- a/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AbpOpenApiConsts.cs +++ b/aspnet-core/framework/open-api/LINGYUN.Abp.OpenApi/LINGYUN/Abp/OpenApi/AbpOpenApiConsts.cs @@ -4,10 +4,10 @@ public static class AbpOpenApiConsts { public const string SecurityChecking = "_AbpOpenApiSecurityChecking"; - public const string AppKeyFieldName = "X-API-APPKEY"; - public const string SignatureFieldName = "X-API-SIGN"; - public const string NonceFieldName = "X-API-NONCE"; - public const string TimeStampFieldName = "X-API-TIMESTAMP"; + public const string HEADER_APP_KEY = "X-API-APPKEY"; + public const string HEADER_SIGNATURE = "X-API-SIGN"; + public const string HEADER_NONCE = "X-API-NONCE"; + public const string HEADER_TIMESTAMP = "X-API-TIMESTAMP"; public const string KeyPrefix = "AbpOpenApi"; /// diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/GobalUsings.cs b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/GobalUsings.cs new file mode 100644 index 000000000..13ccb6b51 --- /dev/null +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/GobalUsings.cs @@ -0,0 +1,7 @@ +global using LINGYUN.MicroService.OpenApi.Gateway; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Serilog; +global using System; \ No newline at end of file diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/LINGYUN.MicroService.OpenApi.Gateway.csproj b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/LINGYUN.MicroService.OpenApi.Gateway.csproj index 7d7949d1d..cb7240750 100644 --- a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/LINGYUN.MicroService.OpenApi.Gateway.csproj +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/LINGYUN.MicroService.OpenApi.Gateway.csproj @@ -4,6 +4,8 @@ Exe net8.0 LINGYUN.MicroService.Internal.Gateway + latest + enable diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/OpenApiGatewayModule.cs b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/OpenApiGatewayModule.cs index e3297b588..ea17fa6af 100644 --- a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/OpenApiGatewayModule.cs +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/OpenApiGatewayModule.cs @@ -5,20 +5,12 @@ using LINGYUN.Abp.Serilog.Enrichers.Application; using LINGYUN.Abp.Serilog.Enrichers.UniqueId; using LINGYUN.Abp.Wrapper; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.WebSockets; using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Linq; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.ApiExploring; diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Program.cs b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Program.cs index 99cb5da26..92e111d1e 100644 --- a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Program.cs +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Program.cs @@ -1,59 +1,42 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Serilog; -using System; -using System.Threading.Tasks; - -namespace LINGYUN.MicroService.OpenApi.Gateway; - -public class Program +try { - public static async Task Main(string[] args) - { - try + Log.Information("Starting OpenApi ApiGateway."); + var builder = WebApplication.CreateBuilder(args); + builder.Host.AddAppSettingsSecretsJson() + .UseAutofac() + .AddYarpJson() + .ConfigureAppConfiguration((context, config) => { - Log.Information("Starting OpenApi ApiGateway."); - var builder = WebApplication.CreateBuilder(args); - builder.Host.AddAppSettingsSecretsJson() - .UseAutofac() - .AddYarpJson() - .ConfigureAppConfiguration((context, config) => - { - var configuration = config.Build(); - var agileConfigEnabled = configuration["AgileConfig:IsEnabled"]; - if (agileConfigEnabled.IsNullOrEmpty() || bool.Parse(agileConfigEnabled)) - { - config.AddAgileConfig(new AgileConfig.Client.ConfigClient(configuration)); - } - }) - .UseSerilog((context, provider, config) => - { - config.ReadFrom.Configuration(context.Configuration); - }); - - await builder.AddApplicationAsync(options => + var configuration = config.Build(); + var agileConfigEnabled = configuration["AgileConfig:IsEnabled"]; + if (agileConfigEnabled.IsNullOrEmpty() || bool.Parse(agileConfigEnabled)) { - OpenApiGatewayModule.ApplicationName = Environment.GetEnvironmentVariable("APPLICATION_NAME") - ?? OpenApiGatewayModule.ApplicationName; - options.ApplicationName = OpenApiGatewayModule.ApplicationName; - }); - var app = builder.Build(); - await app.InitializeApplicationAsync(); - await app.RunAsync(); - - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Starting OpenApi ApiGateway terminated unexpectedly!"); - return 1; - } - finally + config.AddAgileConfig(new AgileConfig.Client.ConfigClient(configuration)); + } + }) + .UseSerilog((context, provider, config) => { - Log.CloseAndFlush(); - } - } + config.ReadFrom.Configuration(context.Configuration); + }); + + await builder.AddApplicationAsync(options => + { + OpenApiGatewayModule.ApplicationName = Environment.GetEnvironmentVariable("APPLICATION_NAME") + ?? OpenApiGatewayModule.ApplicationName; + options.ApplicationName = OpenApiGatewayModule.ApplicationName; + }); + var app = builder.Build(); + await app.InitializeApplicationAsync(); + await app.RunAsync(); + + return 0; } +catch (Exception ex) +{ + Log.Fatal(ex, "Starting OpenApi ApiGateway terminated unexpectedly!"); + return 1; +} +finally +{ + Log.CloseAndFlush(); +} \ No newline at end of file diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Properties/launchSettings.json b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Properties/launchSettings.json index 739031cad..9c577720b 100644 --- a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Properties/launchSettings.json +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/Properties/launchSettings.json @@ -5,9 +5,9 @@ "launchBrowser": false, "dotnetRunMessages": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Production" }, - "applicationUrl": "http://localhost:30000" + "applicationUrl": "http://0.0.0.0:30000" } } } \ No newline at end of file diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/appsettings.Development.json b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/appsettings.Development.json index acec8c342..29a21b380 100644 --- a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/appsettings.Development.json +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/appsettings.Development.json @@ -9,7 +9,7 @@ "tag": "BackendAdmin" }, "App": { - "CorsOrigins": "http://127.0.0.1:3100", + "CorsOrigins": "http://127.0.0.1:3100,http://localhost:9010", "ShowPii": true }, "ConnectionStrings": { diff --git a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/yarp.json b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/yarp.json index 9ef5c1910..316be7976 100644 --- a/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/yarp.json +++ b/gateways/internal/LINGYUN.MicroService.Internal.ApiGateway/src/LINGYUN.MicroService.OpenApi.Gateway/yarp.json @@ -1,434 +1,3 @@ { - "ReverseProxy": { - "Routes": { - "abp-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/abp/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "auth-server-route": { - "ClusterId": "auth-server-cluster", - "Match": { - "Path": "/connect/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "account-route": { - "ClusterId": "auth-server-api-cluster", - "Match": { - "Path": "/api/account/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "identity-route": { - "ClusterId": "auth-server-api-cluster", - "Match": { - "Path": "/api/identity/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "identity-server-route": { - "ClusterId": "auth-server-api-cluster", - "Match": { - "Path": "/api/identity-server/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "cache-management-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/caching-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "saas-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/saas/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "auditing-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/auditing/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "data-protected-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/data-protection-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "text-template-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/text-templating/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "feature-management-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/feature-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "permission-management-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/permission-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "setting-management-route": { - "ClusterId": "admin-api-cluster", - "Match": { - "Path": "/api/setting-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "localization-management-route": { - "ClusterId": "localization-api-cluster", - "Match": { - "Path": "/api/localization/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "im-route": { - "ClusterId": "messages-api-cluster", - "Match": { - "Path": "/api/im/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "notifications-route": { - "ClusterId": "messages-api-cluster", - "Match": { - "Path": "/api/notifications/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "signalr-route": { - "ClusterId": "messages-api-cluster", - "Match": { - "Path": "/signalr-hubs/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - }, - { - "RequestHeadersCopy": true - }, - { - "ResponseHeadersCopy": true - } - ] - }, - "webhooks-management-route": { - "ClusterId": "webhooks-api-cluster", - "Match": { - "Path": "/api/webhooks/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "task-management-route": { - "ClusterId": "tasks-api-cluster", - "Match": { - "Path": "/api/task-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "platform-route": { - "ClusterId": "platform-api-cluster", - "Match": { - "Path": "/api/platform/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "oss-route": { - "ClusterId": "platform-api-cluster", - "Match": { - "Path": "/api/oss-management/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - }, - "files-route": { - "ClusterId": "platform-api-cluster", - "Match": { - "Path": "/api/files/{**everything}" - }, - "Transforms": [ - { - "HeaderPrefix": "X-Forwarded-", - "X-Forwarded": "Append" - }, - { - "ResponseHeadersAllowed": "_AbpWrapResult;_AbpDontWrapResult;_AbpErrorFormat" - } - ] - } - }, - "Clusters": { - "auth-server-cluster": { - "Destinations": { - "destination1": { - "Address": "http://auth-server:44385", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:44385" - } - } - } - }, - "auth-server-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://auth-server-api:30015", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30015" - } - } - } - }, - "admin-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://admin-api:30010", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30010" - } - } - } - }, - "localization-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://localization-api:30030", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30030" - } - } - } - }, - "messages-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://messages-api:30020", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30020" - } - } - } - }, - "webhooks-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://webhooks-api:30045", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30045" - } - } - } - }, - "tasks-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://tasks-api:30040", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30040" - } - } - } - }, - "platform-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://platform-api:30025", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30025" - } - } - } - }, - "workflow-api-cluster": { - "Destinations": { - "destination1": { - "Address": "http://workflow-api:30050", - "Metadata": { - "SwaggerEndpoint": "http://127.0.0.1:30050" - } - } - } - } - } - } + "ReverseProxy": {} } \ No newline at end of file From 2804fa446fdcff299643512ff8a89fa571e0c5e9 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 09:39:56 +0800 Subject: [PATCH 03/81] feat(single): use GlobalUsings --- .../GlobalUsings.cs | 195 ++++++++++++++++++ ...LY.MicroService.Applications.Single.csproj | 2 + ...rviceApplicationsSingleModule.Configure.cs | 88 +------- .../MicroServiceApplicationsSingleModule.cs | 141 +------------ 4 files changed, 204 insertions(+), 222 deletions(-) create mode 100644 aspnet-core/services/LY.MicroService.Applications.Single/GlobalUsings.cs diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/GlobalUsings.cs b/aspnet-core/services/LY.MicroService.Applications.Single/GlobalUsings.cs new file mode 100644 index 000000000..ce2c8f83a --- /dev/null +++ b/aspnet-core/services/LY.MicroService.Applications.Single/GlobalUsings.cs @@ -0,0 +1,195 @@ +global using Elsa; +global using Elsa.Options; +global using LINGYUN.Abp.Account; +global using LINGYUN.Abp.Account.Templates; +global using LINGYUN.Abp.Aliyun.Localization; +global using LINGYUN.Abp.Aliyun.SettingManagement; +global using LINGYUN.Abp.AspNetCore.HttpOverrides; +global using LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper; +global using LINGYUN.Abp.AspNetCore.Mvc.Localization; +global using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; +global using LINGYUN.Abp.Auditing; +global using LINGYUN.Abp.AuditLogging.EntityFrameworkCore; +global using LINGYUN.Abp.Authentication.QQ; +global using LINGYUN.Abp.Authentication.WeChat; +global using LINGYUN.Abp.Authorization.OrganizationUnits; +global using LINGYUN.Abp.BackgroundTasks; +global using LINGYUN.Abp.BackgroundTasks.Activities; +global using LINGYUN.Abp.BackgroundTasks.DistributedLocking; +global using LINGYUN.Abp.BackgroundTasks.EventBus; +global using LINGYUN.Abp.BackgroundTasks.ExceptionHandling; +global using LINGYUN.Abp.BackgroundTasks.Jobs; +global using LINGYUN.Abp.BackgroundTasks.Notifications; +global using LINGYUN.Abp.BackgroundTasks.Quartz; +global using LINGYUN.Abp.CachingManagement; +global using LINGYUN.Abp.CachingManagement.StackExchangeRedis; +global using LINGYUN.Abp.Dapr.Client; +global using LINGYUN.Abp.Data.DbMigrator; +global using LINGYUN.Abp.DataProtectionManagement; +global using LINGYUN.Abp.DataProtectionManagement.EntityFrameworkCore; +global using LINGYUN.Abp.Demo; +global using LINGYUN.Abp.Demo.Books; +global using LINGYUN.Abp.Demo.EntityFrameworkCore; +global using LINGYUN.Abp.Demo.Localization; +global using LINGYUN.Abp.Elsa; +global using LINGYUN.Abp.Elsa.Activities; +global using LINGYUN.Abp.Elsa.EntityFrameworkCore; +global using LINGYUN.Abp.Elsa.EntityFrameworkCore.MySql; +global using LINGYUN.Abp.ExceptionHandling; +global using LINGYUN.Abp.ExceptionHandling.Emailing; +global using LINGYUN.Abp.Exporter.MiniExcel; +global using LINGYUN.Abp.FeatureManagement; +global using LINGYUN.Abp.FeatureManagement.HttpApi; +global using LINGYUN.Abp.Features.LimitValidation; +global using LINGYUN.Abp.Features.LimitValidation.Redis.Client; +global using LINGYUN.Abp.Http.Client.Wrapper; +global using LINGYUN.Abp.Idempotent; +global using LINGYUN.Abp.Identity; +global using LINGYUN.Abp.Identity.AspNetCore.Session; +global using LINGYUN.Abp.Identity.EntityFrameworkCore; +global using LINGYUN.Abp.Identity.Notifications; +global using LINGYUN.Abp.Identity.OrganizaztionUnits; +global using LINGYUN.Abp.Identity.Session; +global using LINGYUN.Abp.Identity.Session.AspNetCore; +global using LINGYUN.Abp.Identity.WeChat; +global using LINGYUN.Abp.IdentityServer.IdentityResources; +global using LINGYUN.Abp.IdGenerator; +global using LINGYUN.Abp.IM.SignalR; +global using LINGYUN.Abp.Localization.CultureMap; +global using LINGYUN.Abp.Localization.Persistence; +global using LINGYUN.Abp.LocalizationManagement; +global using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore; +global using LINGYUN.Abp.MessageService; +global using LINGYUN.Abp.MessageService.EntityFrameworkCore; +global using LINGYUN.Abp.MultiTenancy.Editions; +global using LINGYUN.Abp.Notifications; +global using LINGYUN.Abp.Notifications.Common; +global using LINGYUN.Abp.Notifications.Emailing; +global using LINGYUN.Abp.Notifications.EntityFrameworkCore; +global using LINGYUN.Abp.Notifications.SignalR; +global using LINGYUN.Abp.Notifications.WeChat.MiniProgram; +global using LINGYUN.Abp.OpenApi.Authorization; +global using LINGYUN.Abp.OpenIddict; +global using LINGYUN.Abp.OpenIddict.AspNetCore; +global using LINGYUN.Abp.OpenIddict.AspNetCore.Session; +global using LINGYUN.Abp.OpenIddict.LinkUser; +global using LINGYUN.Abp.OpenIddict.Permissions; +global using LINGYUN.Abp.OpenIddict.Portal; +global using LINGYUN.Abp.OpenIddict.Sms; +global using LINGYUN.Abp.OpenIddict.WeChat; +global using LINGYUN.Abp.OpenIddict.WeChat.Work; +global using LINGYUN.Abp.OssManagement; +global using LINGYUN.Abp.OssManagement.FileSystem; +global using LINGYUN.Abp.OssManagement.Imaging; +global using LINGYUN.Abp.OssManagement.SettingManagement; +global using LINGYUN.Abp.PermissionManagement; +global using LINGYUN.Abp.PermissionManagement.HttpApi; +global using LINGYUN.Abp.PermissionManagement.OrganizationUnits; +global using LINGYUN.Abp.Saas; +global using LINGYUN.Abp.Saas.EntityFrameworkCore; +global using LINGYUN.Abp.Serilog.Enrichers.Application; +global using LINGYUN.Abp.Serilog.Enrichers.UniqueId; +global using LINGYUN.Abp.SettingManagement; +global using LINGYUN.Abp.Sms.Aliyun; +global using LINGYUN.Abp.TaskManagement; +global using LINGYUN.Abp.TaskManagement.EntityFrameworkCore; +global using LINGYUN.Abp.Tencent.Localization; +global using LINGYUN.Abp.Tencent.QQ; +global using LINGYUN.Abp.Tencent.SettingManagement; +global using LINGYUN.Abp.TextTemplating; +global using LINGYUN.Abp.TextTemplating.EntityFrameworkCore; +global using LINGYUN.Abp.UI.Navigation; +global using LINGYUN.Abp.UI.Navigation.VueVbenAdmin; +global using LINGYUN.Abp.Webhooks; +global using LINGYUN.Abp.Webhooks.EventBus; +global using LINGYUN.Abp.Webhooks.Identity; +global using LINGYUN.Abp.Webhooks.Saas; +global using LINGYUN.Abp.WebhooksManagement; +global using LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore; +global using LINGYUN.Abp.WeChat.Common.Messages.Handlers; +global using LINGYUN.Abp.WeChat.Localization; +global using LINGYUN.Abp.WeChat.MiniProgram; +global using LINGYUN.Abp.WeChat.Official; +global using LINGYUN.Abp.WeChat.Official.Handlers; +global using LINGYUN.Abp.WeChat.SettingManagement; +global using LINGYUN.Abp.WeChat.Work; +global using LINGYUN.Abp.WeChat.Work.Handlers; +global using LINGYUN.Abp.Wrapper; +global using LINGYUN.Platform; +global using LINGYUN.Platform.EntityFrameworkCore; +global using LINGYUN.Platform.HttpApi; +global using LINGYUN.Platform.Localization; +global using LINGYUN.Platform.Settings.VueVbenAdmin; +global using LINGYUN.Platform.Theme.VueVbenAdmin; +global using LY.MicroService.Applications.Single.Authentication; +global using LY.MicroService.Applications.Single.EntityFrameworkCore; +global using LY.MicroService.Applications.Single.IdentityResources; +global using Medallion.Threading; +global using Medallion.Threading.Redis; +global using Microsoft.AspNetCore.Authentication.Cookies; +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Microsoft.AspNetCore.Cors; +global using Microsoft.AspNetCore.DataProtection; +global using Microsoft.AspNetCore.Identity; +global using Microsoft.AspNetCore.Server.Kestrel.Core; +global using Microsoft.Extensions.Caching.StackExchangeRedis; +global using Microsoft.IdentityModel.Logging; +global using Microsoft.OpenApi.Models; +global using MiniExcelLibs.Attributes; +global using OpenIddict.Server; +global using OpenIddict.Server.AspNetCore; +global using Quartz; +global using StackExchange.Redis; +global using System.Security.Cryptography; +global using System.Security.Cryptography.X509Certificates; +global using System.Text.Encodings.Web; +global using System.Text.Unicode; +global using Volo.Abp; +global using Volo.Abp.Account.Web; +global using Volo.Abp.AspNetCore.Authentication.JwtBearer; +global using Volo.Abp.AspNetCore.Mvc; +global using Volo.Abp.AspNetCore.Mvc.AntiForgery; +global using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +global using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; +global using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; +global using Volo.Abp.AspNetCore.Serilog; +global using Volo.Abp.Auditing; +global using Volo.Abp.Authorization.Permissions; +global using Volo.Abp.Autofac; +global using Volo.Abp.BlobStoring; +global using Volo.Abp.BlobStoring.FileSystem; +global using Volo.Abp.Caching; +global using Volo.Abp.Caching.StackExchangeRedis; +global using Volo.Abp.Data; +global using Volo.Abp.EntityFrameworkCore; +global using Volo.Abp.EntityFrameworkCore.MySQL; +global using Volo.Abp.EventBus; +global using Volo.Abp.FeatureManagement; +global using Volo.Abp.FeatureManagement.EntityFrameworkCore; +global using Volo.Abp.Features; +global using Volo.Abp.GlobalFeatures; +global using Volo.Abp.Http.Client; +global using Volo.Abp.Identity.Localization; +global using Volo.Abp.IdentityServer; +global using Volo.Abp.IdentityServer.Localization; +global using Volo.Abp.Imaging; +global using Volo.Abp.Json; +global using Volo.Abp.Json.SystemTextJson; +global using Volo.Abp.Localization; +global using Volo.Abp.Modularity; +global using Volo.Abp.MultiTenancy; +global using Volo.Abp.OpenIddict; +global using Volo.Abp.OpenIddict.EntityFrameworkCore; +global using Volo.Abp.OpenIddict.Localization; +global using Volo.Abp.PermissionManagement; +global using Volo.Abp.PermissionManagement.EntityFrameworkCore; +global using Volo.Abp.PermissionManagement.Identity; +global using Volo.Abp.PermissionManagement.OpenIddict; +global using Volo.Abp.Quartz; +global using Volo.Abp.Security.Claims; +global using Volo.Abp.SettingManagement; +global using Volo.Abp.SettingManagement.EntityFrameworkCore; +global using Volo.Abp.SettingManagement.Localization; +global using Volo.Abp.Threading; +global using Volo.Abp.UI.Navigation.Urls; +global using Volo.Abp.VirtualFileSystem; \ No newline at end of file diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj index 5d12da098..940ca5513 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj +++ b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj @@ -6,6 +6,7 @@ net8.0 enable LY.MicroService.Applications.Single + enable @@ -48,6 +49,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs index d9d66bc79..60b7ab0e9 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs @@ -1,90 +1,4 @@ -using Elsa; -using Elsa.Options; -using LINGYUN.Abp.Aliyun.Localization; -using LINGYUN.Abp.BackgroundTasks; -using LINGYUN.Abp.DataProtectionManagement; -using LINGYUN.Abp.Demo.Books; -using LINGYUN.Abp.Demo.Localization; -using LINGYUN.Abp.ExceptionHandling; -using LINGYUN.Abp.ExceptionHandling.Emailing; -using LINGYUN.Abp.Exporter.MiniExcel; -using LINGYUN.Abp.Idempotent; -using LINGYUN.Abp.Identity.Session; -using LINGYUN.Abp.IdentityServer.IdentityResources; -using LINGYUN.Abp.Localization.CultureMap; -using LINGYUN.Abp.Notifications; -using LINGYUN.Abp.OpenIddict.AspNetCore.Session; -using LINGYUN.Abp.OpenIddict.LinkUser; -using LINGYUN.Abp.OpenIddict.Permissions; -using LINGYUN.Abp.OpenIddict.Portal; -using LINGYUN.Abp.OpenIddict.Sms; -using LINGYUN.Abp.OpenIddict.WeChat; -using LINGYUN.Abp.Saas; -using LINGYUN.Abp.Serilog.Enrichers.Application; -using LINGYUN.Abp.Serilog.Enrichers.UniqueId; -using LINGYUN.Abp.Tencent.Localization; -using LINGYUN.Abp.TextTemplating; -using LINGYUN.Abp.WebhooksManagement; -using LINGYUN.Abp.WeChat.Common.Messages.Handlers; -using LINGYUN.Abp.WeChat.Localization; -using LINGYUN.Abp.WeChat.Work; -using LINGYUN.Abp.Wrapper; -using LINGYUN.Platform.Localization; -using LY.MicroService.Applications.Single.Authentication; -using LY.MicroService.Applications.Single.IdentityResources; -using Medallion.Threading; -using Medallion.Threading.Redis; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Cors; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.IdentityModel.Logging; -using Microsoft.OpenApi.Models; -using MiniExcelLibs.Attributes; -using OpenIddict.Server; -using OpenIddict.Server.AspNetCore; -using Quartz; -using StackExchange.Redis; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text.Encodings.Web; -using System.Text.Unicode; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.AntiForgery; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.Auditing; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.BlobStoring; -using Volo.Abp.BlobStoring.FileSystem; -using Volo.Abp.BlobStoring.Minio; -using Volo.Abp.Caching; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.FeatureManagement; -using Volo.Abp.Features; -using Volo.Abp.GlobalFeatures; -using Volo.Abp.Http.Client; -using Volo.Abp.Identity.Localization; -using Volo.Abp.IdentityServer; -using Volo.Abp.IdentityServer.Localization; -using Volo.Abp.Json; -using Volo.Abp.Json.SystemTextJson; -using Volo.Abp.Localization; -using Volo.Abp.MultiTenancy; -using Volo.Abp.OpenIddict; -using Volo.Abp.OpenIddict.Localization; -using Volo.Abp.PermissionManagement; -using Volo.Abp.Quartz; -using Volo.Abp.Security.Claims; -using Volo.Abp.SettingManagement; -using Volo.Abp.SettingManagement.Localization; -using Volo.Abp.Threading; -using Volo.Abp.UI.Navigation.Urls; -using Volo.Abp.VirtualFileSystem; -using VoloAbpExceptionHandlingOptions = Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingOptions; +using VoloAbpExceptionHandlingOptions = Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingOptions; namespace LY.MicroService.Applications.Single; diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs index fde8e6676..bd351f654 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs @@ -1,134 +1,4 @@ -using LINGYUN.Abp.Account; -using LINGYUN.Abp.Account.Templates; -using LINGYUN.Abp.Aliyun.SettingManagement; -using LINGYUN.Abp.AspNetCore.HttpOverrides; -using LINGYUN.Abp.AspNetCore.Mvc.Idempotent.Wrapper; -using LINGYUN.Abp.AspNetCore.Mvc.Localization; -using LINGYUN.Abp.AspNetCore.Mvc.Wrapper; -using LINGYUN.Abp.Auditing; -using LINGYUN.Abp.AuditLogging.EntityFrameworkCore; -using LINGYUN.Abp.Authentication.QQ; -using LINGYUN.Abp.Authentication.WeChat; -using LINGYUN.Abp.Authorization.OrganizationUnits; -using LINGYUN.Abp.BackgroundTasks; -using LINGYUN.Abp.BackgroundTasks.Activities; -using LINGYUN.Abp.BackgroundTasks.DistributedLocking; -using LINGYUN.Abp.BackgroundTasks.EventBus; -using LINGYUN.Abp.BackgroundTasks.ExceptionHandling; -using LINGYUN.Abp.BackgroundTasks.Jobs; -using LINGYUN.Abp.BackgroundTasks.Notifications; -using LINGYUN.Abp.BackgroundTasks.Quartz; -using LINGYUN.Abp.CachingManagement; -using LINGYUN.Abp.CachingManagement.StackExchangeRedis; -using LINGYUN.Abp.Dapr.Client; -using LINGYUN.Abp.Data.DbMigrator; -using LINGYUN.Abp.DataProtectionManagement; -using LINGYUN.Abp.DataProtectionManagement.EntityFrameworkCore; -using LINGYUN.Abp.Demo; -using LINGYUN.Abp.Demo.EntityFrameworkCore; -using LINGYUN.Abp.Elsa; -using LINGYUN.Abp.Elsa.Activities; -using LINGYUN.Abp.Elsa.EntityFrameworkCore; -using LINGYUN.Abp.Elsa.EntityFrameworkCore.MySql; -using LINGYUN.Abp.ExceptionHandling; -using LINGYUN.Abp.ExceptionHandling.Emailing; -using LINGYUN.Abp.Exporter.MiniExcel; -using LINGYUN.Abp.FeatureManagement; -using LINGYUN.Abp.FeatureManagement.HttpApi; -using LINGYUN.Abp.Features.LimitValidation; -using LINGYUN.Abp.Features.LimitValidation.Redis.Client; -using LINGYUN.Abp.Http.Client.Wrapper; -using LINGYUN.Abp.Identity; -using LINGYUN.Abp.Identity.AspNetCore.Session; -using LINGYUN.Abp.Identity.EntityFrameworkCore; -using LINGYUN.Abp.Identity.Notifications; -using LINGYUN.Abp.Identity.OrganizaztionUnits; -using LINGYUN.Abp.Identity.Session.AspNetCore; -using LINGYUN.Abp.Identity.WeChat; -using LINGYUN.Abp.IdGenerator; -using LINGYUN.Abp.IM.SignalR; -using LINGYUN.Abp.Localization.CultureMap; -using LINGYUN.Abp.Localization.Persistence; -using LINGYUN.Abp.LocalizationManagement; -using LINGYUN.Abp.LocalizationManagement.EntityFrameworkCore; -using LINGYUN.Abp.MessageService; -using LINGYUN.Abp.MessageService.EntityFrameworkCore; -using LINGYUN.Abp.MultiTenancy.Editions; -using LINGYUN.Abp.Notifications; -using LINGYUN.Abp.Notifications.Common; -using LINGYUN.Abp.Notifications.Emailing; -using LINGYUN.Abp.Notifications.EntityFrameworkCore; -using LINGYUN.Abp.Notifications.SignalR; -using LINGYUN.Abp.Notifications.WeChat.MiniProgram; -using LINGYUN.Abp.OpenApi.Authorization; -using LINGYUN.Abp.OpenIddict; -using LINGYUN.Abp.OpenIddict.AspNetCore; -using LINGYUN.Abp.OpenIddict.AspNetCore.Session; -using LINGYUN.Abp.OpenIddict.Portal; -using LINGYUN.Abp.OpenIddict.Sms; -using LINGYUN.Abp.OpenIddict.WeChat; -using LINGYUN.Abp.OpenIddict.WeChat.Work; -using LINGYUN.Abp.OssManagement; -using LINGYUN.Abp.OssManagement.FileSystem; -using LINGYUN.Abp.OssManagement.Imaging; -using LINGYUN.Abp.OssManagement.Minio; -using LINGYUN.Abp.OssManagement.SettingManagement; -using LINGYUN.Abp.PermissionManagement; -using LINGYUN.Abp.PermissionManagement.HttpApi; -using LINGYUN.Abp.PermissionManagement.OrganizationUnits; -using LINGYUN.Abp.Saas; -using LINGYUN.Abp.Saas.EntityFrameworkCore; -using LINGYUN.Abp.Serilog.Enrichers.Application; -using LINGYUN.Abp.Serilog.Enrichers.UniqueId; -using LINGYUN.Abp.SettingManagement; -using LINGYUN.Abp.Sms.Aliyun; -using LINGYUN.Abp.TaskManagement; -using LINGYUN.Abp.TaskManagement.EntityFrameworkCore; -using LINGYUN.Abp.Tencent.QQ; -using LINGYUN.Abp.Tencent.SettingManagement; -using LINGYUN.Abp.TextTemplating; -using LINGYUN.Abp.TextTemplating.EntityFrameworkCore; -using LINGYUN.Abp.UI.Navigation; -using LINGYUN.Abp.UI.Navigation.VueVbenAdmin; -using LINGYUN.Abp.Webhooks; -using LINGYUN.Abp.Webhooks.EventBus; -using LINGYUN.Abp.Webhooks.Identity; -using LINGYUN.Abp.Webhooks.Saas; -using LINGYUN.Abp.WebhooksManagement; -using LINGYUN.Abp.WebhooksManagement.EntityFrameworkCore; -using LINGYUN.Abp.WeChat.MiniProgram; -using LINGYUN.Abp.WeChat.Official; -using LINGYUN.Abp.WeChat.Official.Handlers; -using LINGYUN.Abp.WeChat.SettingManagement; -using LINGYUN.Abp.WeChat.Work; -using LINGYUN.Abp.WeChat.Work.Handlers; -using LINGYUN.Platform; -using LINGYUN.Platform.EntityFrameworkCore; -using LINGYUN.Platform.HttpApi; -using LINGYUN.Platform.Settings.VueVbenAdmin; -using LINGYUN.Platform.Theme.VueVbenAdmin; -using LY.MicroService.Applications.Single.EntityFrameworkCore; -using Volo.Abp; -using Volo.Abp.Account.Web; -using Volo.Abp.AspNetCore.Authentication.JwtBearer; -using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; -using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; -using Volo.Abp.AspNetCore.Serilog; -using Volo.Abp.Autofac; -using Volo.Abp.Caching.StackExchangeRedis; -using Volo.Abp.Data; -using Volo.Abp.EntityFrameworkCore.MySQL; -using Volo.Abp.EventBus; -using Volo.Abp.FeatureManagement.EntityFrameworkCore; -using Volo.Abp.Imaging; -using Volo.Abp.Modularity; -using Volo.Abp.OpenIddict.EntityFrameworkCore; -using Volo.Abp.PermissionManagement.EntityFrameworkCore; -using Volo.Abp.PermissionManagement.Identity; -using Volo.Abp.PermissionManagement.OpenIddict; -using Volo.Abp.SettingManagement; -using Volo.Abp.SettingManagement.EntityFrameworkCore; -using Volo.Abp.Threading; +using Volo.Abp.MailKit; namespace LY.MicroService.Applications.Single; @@ -169,7 +39,7 @@ namespace LY.MicroService.Applications.Single; //typeof(AbpIdentityServerHttpApiModule), //typeof(AbpIdentityServerEntityFrameworkCoreModule), - typeof(AbpOpenIddictAspNetCoreModule), + typeof(LINGYUN.Abp.OpenIddict.AspNetCore.AbpOpenIddictAspNetCoreModule), typeof(AbpOpenIddictAspNetCoreSessionModule), typeof(AbpOpenIddictApplicationModule), typeof(AbpOpenIddictHttpApiModule), @@ -220,8 +90,8 @@ namespace LY.MicroService.Applications.Single; typeof(WebhooksManagementHttpApiModule), typeof(WebhooksManagementEntityFrameworkCoreModule), - typeof(AbpFeatureManagementApplicationModule), - typeof(AbpFeatureManagementHttpApiModule), + typeof(LINGYUN.Abp.FeatureManagement.AbpFeatureManagementApplicationModule), + typeof(LINGYUN.Abp.FeatureManagement.HttpApi.AbpFeatureManagementHttpApiModule), typeof(AbpFeatureManagementEntityFrameworkCoreModule), typeof(AbpSettingManagementDomainModule), @@ -229,7 +99,7 @@ namespace LY.MicroService.Applications.Single; typeof(AbpSettingManagementHttpApiModule), typeof(AbpSettingManagementEntityFrameworkCoreModule), - typeof(AbpPermissionManagementApplicationModule), + typeof(LINGYUN.Abp.PermissionManagement.AbpPermissionManagementApplicationModule), typeof(AbpPermissionManagementHttpApiModule), typeof(AbpPermissionManagementDomainIdentityModule), typeof(AbpPermissionManagementDomainOpenIddictModule), @@ -322,6 +192,7 @@ namespace LY.MicroService.Applications.Single; typeof(AbpAspNetCoreMvcIdempotentWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), typeof(AbpAspNetCoreMvcUiBasicThemeModule), + typeof(AbpMailKitModule), typeof(AbpEventBusModule), typeof(AbpAutofacModule) )] From 4ecff357c5b9fe6e6db59f91bfe36ddc3894e71d Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 09:43:56 +0800 Subject: [PATCH 04/81] feat(emailing): switch from smtp to mailkit --- .../AuthServerHttpApiHostModule.cs | 2 ++ .../LY.MicroService.AuthServer.HttpApi.Host.csproj | 1 + .../services/LY.MicroService.AuthServer/AuthServerModule.cs | 2 ++ .../LY.MicroService.AuthServer.csproj | 1 + .../BackendAdminHttpApiHostModule.cs | 2 ++ .../LY.MicroService.BackendAdmin.HttpApi.Host.csproj | 1 + .../IdentityServerHttpApiHostModule.cs | 2 ++ .../LY.MicroService.identityServer.HttpApi.Host.csproj | 1 + .../LY.MicroService.IdentityServer/IdentityServerModule.cs | 2 ++ .../LY.MicroService.IdentityServer.csproj | 1 + .../LY.MicroService.LocalizationManagement.HttpApi.Host.csproj | 1 + .../LocalizationManagementHttpApiHostModule.cs | 2 ++ .../LY.MicroService.PlatformManagement.HttpApi.Host.csproj | 1 + .../PlatformManagementHttpApiHostModule.cs | 2 ++ .../LY.MicroService.RealtimeMessage.HttpApi.Host.csproj | 1 + .../RealtimeMessageHttpApiHostModule.cs | 2 ++ .../LY.MicroService.TaskManagement.HttpApi.Host.csproj | 1 + .../TaskManagementHttpApiHostModule.cs | 2 ++ .../LY.MicroService.WebhooksManagement.HttpApi.Host.csproj | 1 + .../WebhooksManagementHttpApiHostModule.cs | 2 ++ .../LY.MicroService.WechatManagement.HttpApi.Host.csproj | 1 + .../WechatManagementHttpApiHostModule.cs | 2 ++ aspnet-core/templates/content/Directory.Packages.props | 1 + 23 files changed, 34 insertions(+) diff --git a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/AuthServerHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/AuthServerHttpApiHostModule.cs index 96538315d..4656dab6a 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/AuthServerHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/AuthServerHttpApiHostModule.cs @@ -30,6 +30,7 @@ using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict.EntityFrameworkCore; using Volo.Abp.PermissionManagement.EntityFrameworkCore; @@ -63,6 +64,7 @@ namespace LY.MicroService.AuthServer; typeof(AbpCAPEventBusModule), typeof(AbpHttpClientModule), typeof(AbpAliyunSmsModule), + typeof(AbpMailKitModule), typeof(AbpCachingStackExchangeRedisModule), typeof(AbpLocalizationCultureMapModule), typeof(AbpAspNetCoreAuthenticationJwtBearerModule), diff --git a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/LY.MicroService.AuthServer.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/LY.MicroService.AuthServer.HttpApi.Host.csproj index fa8d478e7..1dcf0b53f 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/LY.MicroService.AuthServer.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.AuthServer.HttpApi.Host/LY.MicroService.AuthServer.HttpApi.Host.csproj @@ -45,6 +45,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs index 98de536d8..841b09457 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs +++ b/aspnet-core/services/LY.MicroService.AuthServer/AuthServerModule.cs @@ -37,6 +37,7 @@ using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Identity; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict.EntityFrameworkCore; using Volo.Abp.PermissionManagement.EntityFrameworkCore; @@ -81,6 +82,7 @@ namespace LY.MicroService.AuthServer; typeof(AbpLocalizationCultureMapModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), + typeof(AbpMailKitModule), typeof(AbpCAPEventBusModule), typeof(AbpAliyunSmsModule) )] diff --git a/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj b/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj index b135bf021..20e266130 100644 --- a/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj +++ b/aspnet-core/services/LY.MicroService.AuthServer/LY.MicroService.AuthServer.csproj @@ -49,6 +49,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/BackendAdminHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/BackendAdminHttpApiHostModule.cs index cde8297d8..cf5073a3a 100644 --- a/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/BackendAdminHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/BackendAdminHttpApiHostModule.cs @@ -48,6 +48,7 @@ using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client; using Volo.Abp.IdentityServer.EntityFrameworkCore; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.PermissionManagement.Identity; @@ -114,6 +115,7 @@ namespace LY.MicroService.BackendAdmin; typeof(AbpAspNetCoreAuthenticationJwtBearerModule), typeof(AbpEmailingExceptionHandlingModule), typeof(AbpHttpClientModule), + typeof(AbpMailKitModule), typeof(AbpAliyunSmsModule), typeof(AbpCachingStackExchangeRedisModule), typeof(AbpLocalizationCultureMapModule), diff --git a/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/LY.MicroService.BackendAdmin.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/LY.MicroService.BackendAdmin.HttpApi.Host.csproj index 85f647a01..f0db262fa 100644 --- a/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/LY.MicroService.BackendAdmin.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.BackendAdmin.HttpApi.Host/LY.MicroService.BackendAdmin.HttpApi.Host.csproj @@ -49,6 +49,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/IdentityServerHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/IdentityServerHttpApiHostModule.cs index e96d07959..c8b5047d0 100644 --- a/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/IdentityServerHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/IdentityServerHttpApiHostModule.cs @@ -26,6 +26,7 @@ using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -59,6 +60,7 @@ namespace LY.MicroService.IdentityServer; typeof(AbpCAPEventBusModule), typeof(AbpHttpClientModule), typeof(AbpAliyunSmsModule), + typeof(AbpMailKitModule), typeof(AbpCachingStackExchangeRedisModule), typeof(AbpLocalizationCultureMapModule), typeof(AbpIdentitySessionAspNetCoreModule), diff --git a/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/LY.MicroService.identityServer.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/LY.MicroService.identityServer.HttpApi.Host.csproj index 52d6362da..7eaf932c5 100644 --- a/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/LY.MicroService.identityServer.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.IdentityServer.HttpApi.Host/LY.MicroService.identityServer.HttpApi.Host.csproj @@ -52,6 +52,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.IdentityServer/IdentityServerModule.cs b/aspnet-core/services/LY.MicroService.IdentityServer/IdentityServerModule.cs index a58536447..665a7d937 100644 --- a/aspnet-core/services/LY.MicroService.IdentityServer/IdentityServerModule.cs +++ b/aspnet-core/services/LY.MicroService.IdentityServer/IdentityServerModule.cs @@ -39,6 +39,7 @@ using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Identity; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.PermissionManagement.Identity; @@ -87,6 +88,7 @@ namespace LY.MicroService.IdentityServer; typeof(AbpAuditLoggingElasticsearchModule), // 放在 AbpIdentity 模块之后,避免被覆盖 typeof(AbpLocalizationCultureMapModule), typeof(AbpCAPEventBusModule), + typeof(AbpMailKitModule), typeof(AbpHttpClientWrapperModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), diff --git a/aspnet-core/services/LY.MicroService.IdentityServer/LY.MicroService.IdentityServer.csproj b/aspnet-core/services/LY.MicroService.IdentityServer/LY.MicroService.IdentityServer.csproj index 01a46dd33..fca49eba4 100644 --- a/aspnet-core/services/LY.MicroService.IdentityServer/LY.MicroService.IdentityServer.csproj +++ b/aspnet-core/services/LY.MicroService.IdentityServer/LY.MicroService.IdentityServer.csproj @@ -46,6 +46,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LY.MicroService.LocalizationManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LY.MicroService.LocalizationManagement.HttpApi.Host.csproj index 9887b7be8..a24ffd530 100644 --- a/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LY.MicroService.LocalizationManagement.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LY.MicroService.LocalizationManagement.HttpApi.Host.csproj @@ -42,6 +42,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LocalizationManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LocalizationManagementHttpApiHostModule.cs index 9f6862ddf..543753190 100644 --- a/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LocalizationManagementHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.LocalizationManagement.HttpApi.Host/LocalizationManagementHttpApiHostModule.cs @@ -25,6 +25,7 @@ using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -55,6 +56,7 @@ namespace LY.MicroService.LocalizationManagement; typeof(AbpLocalizationCultureMapModule), typeof(AbpIdentitySessionAspNetCoreModule), typeof(AbpHttpClientModule), + typeof(AbpMailKitModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), typeof(AbpAutofacModule) diff --git a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj index d313a2139..531f5f3f2 100644 --- a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/LY.MicroService.PlatformManagement.HttpApi.Host.csproj @@ -43,6 +43,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs index cb2c2fbba..63a0c1cac 100644 --- a/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.PlatformManagement.HttpApi.Host/PlatformManagementHttpApiHostModule.cs @@ -45,6 +45,7 @@ using Volo.Abp.Http.Client; using Volo.Abp.Http.Client.IdentityModel.Web; using Volo.Abp.Identity; using Volo.Abp.Imaging; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -92,6 +93,7 @@ namespace LY.MicroService.PlatformManagement; typeof(AbpLocalizationCultureMapModule), typeof(AbpIdentitySessionAspNetCoreModule), typeof(AbpHttpClientModule), + typeof(AbpMailKitModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpClaimsMappingModule), typeof(AbpAspNetCoreHttpOverridesModule), diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj index 18d803cc2..25b7b47b0 100644 --- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/LY.MicroService.RealtimeMessage.HttpApi.Host.csproj @@ -48,6 +48,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs index cb04046f4..2f970c91e 100644 --- a/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/RealtimeMessageHttpApiHostModule.cs @@ -53,6 +53,7 @@ using Volo.Abp.BackgroundWorkers; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -113,6 +114,7 @@ namespace LY.MicroService.RealtimeMessage; typeof(AbpLocalizationCultureMapModule), typeof(AbpIdentitySessionAspNetCoreModule), typeof(AbpHttpClientModule), + typeof(AbpMailKitModule), typeof(AbpClaimsMappingModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), diff --git a/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/LY.MicroService.TaskManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/LY.MicroService.TaskManagement.HttpApi.Host.csproj index 5d7b0351d..8d6d7c3d6 100644 --- a/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/LY.MicroService.TaskManagement.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/LY.MicroService.TaskManagement.HttpApi.Host.csproj @@ -42,6 +42,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/TaskManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/TaskManagementHttpApiHostModule.cs index ffaeb6083..e819fa623 100644 --- a/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/TaskManagementHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.TaskManagement.HttpApi.Host/TaskManagementHttpApiHostModule.cs @@ -36,6 +36,7 @@ using Volo.Abp.DistributedLocking; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client.IdentityModel.Web; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -75,6 +76,7 @@ namespace LY.MicroService.TaskManagement; typeof(AbpCachingStackExchangeRedisModule), typeof(AbpAspNetCoreMvcModule), typeof(AbpSwashbuckleModule), + typeof(AbpMailKitModule), typeof(AbpLocalizationCultureMapModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), diff --git a/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/LY.MicroService.WebhooksManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/LY.MicroService.WebhooksManagement.HttpApi.Host.csproj index 182621ed4..1c7555931 100644 --- a/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/LY.MicroService.WebhooksManagement.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/LY.MicroService.WebhooksManagement.HttpApi.Host.csproj @@ -38,6 +38,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/WebhooksManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/WebhooksManagementHttpApiHostModule.cs index 0605cd0f2..a8a3b9d37 100644 --- a/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/WebhooksManagementHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.WebhooksManagement.HttpApi.Host/WebhooksManagementHttpApiHostModule.cs @@ -38,6 +38,7 @@ using Volo.Abp.DistributedLocking; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client.IdentityModel.Web; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -80,6 +81,7 @@ namespace LY.MicroService.WebhooksManagement; typeof(AbpHttpClientWrapperModule), typeof(AbpDaprClientWrapperModule), typeof(AbpClaimsMappingModule), + typeof(AbpMailKitModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), typeof(AbpIdentitySessionAspNetCoreModule), diff --git a/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/LY.MicroService.WechatManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/LY.MicroService.WechatManagement.HttpApi.Host.csproj index e316c49f6..7fcf66499 100644 --- a/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/LY.MicroService.WechatManagement.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/LY.MicroService.WechatManagement.HttpApi.Host.csproj @@ -38,6 +38,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/WechatManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/WechatManagementHttpApiHostModule.cs index 10b748fe0..a37956874 100644 --- a/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/WechatManagementHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.WechatManagement.HttpApi.Host/WechatManagementHttpApiHostModule.cs @@ -29,6 +29,7 @@ using Volo.Abp.DistributedLocking; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client.IdentityModel.Web; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -64,6 +65,7 @@ namespace LY.MicroService.WechatManagement; typeof(AbpDistributedLockingModule), typeof(AbpSwashbuckleModule), typeof(AbpHttpClientWrapperModule), + typeof(AbpMailKitModule), typeof(AbpClaimsMappingModule), typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), diff --git a/aspnet-core/templates/content/Directory.Packages.props b/aspnet-core/templates/content/Directory.Packages.props index f3dc78297..70ddeb357 100644 --- a/aspnet-core/templates/content/Directory.Packages.props +++ b/aspnet-core/templates/content/Directory.Packages.props @@ -153,6 +153,7 @@ + From c9913c3336223e3672bcd10c1d1c74660986df22 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 10:05:19 +0800 Subject: [PATCH 05/81] fix(tasks): add a task notification template --- .../BackgroundTasksNotificationDefinitionProvider.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Notifications/LINGYUN/Abp/BackgroundTasks/Notifications/BackgroundTasksNotificationDefinitionProvider.cs b/aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Notifications/LINGYUN/Abp/BackgroundTasks/Notifications/BackgroundTasksNotificationDefinitionProvider.cs index 43ba7873f..c29ae3b41 100644 --- a/aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Notifications/LINGYUN/Abp/BackgroundTasks/Notifications/BackgroundTasksNotificationDefinitionProvider.cs +++ b/aspnet-core/modules/task-management/LINGYUN.Abp.BackgroundTasks.Notifications/LINGYUN/Abp/BackgroundTasks/Notifications/BackgroundTasksNotificationDefinitionProvider.cs @@ -22,7 +22,8 @@ public class BackgroundTasksNotificationDefinitionProvider : NotificationDefinit allowSubscriptionToClients: true) .WithProviders( NotificationProviderNames.SignalR, - NotificationProviderNames.Emailing); + NotificationProviderNames.Emailing) + .WithTemplate(typeof(BackgroundTasksResource)); backgroundTaskGroup.AddNotification( BackgroundTasksNotificationNames.JobExecuteFailed, L("Notifications:JobExecuteFailed"), @@ -33,7 +34,8 @@ public class BackgroundTasksNotificationDefinitionProvider : NotificationDefinit allowSubscriptionToClients: true) .WithProviders( NotificationProviderNames.SignalR, - NotificationProviderNames.Emailing); + NotificationProviderNames.Emailing) + .WithTemplate(typeof(BackgroundTasksResource)); backgroundTaskGroup.AddNotification( BackgroundTasksNotificationNames.JobExecuteCompleted, L("Notifications:JobExecuteCompleted"), @@ -44,7 +46,8 @@ public class BackgroundTasksNotificationDefinitionProvider : NotificationDefinit allowSubscriptionToClients: true) .WithProviders( NotificationProviderNames.SignalR, - NotificationProviderNames.Emailing); + NotificationProviderNames.Emailing) + .WithTemplate(typeof(BackgroundTasksResource)); } protected LocalizableString L(string name) From 0bf817dc77bb00d2db3d9d7169f8710a014865f0 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 10:07:18 +0800 Subject: [PATCH 06/81] feat(emailing): switch from smtp to mailkit --- .../LY.MicroService.WorkflowManagement.HttpApi.Host.csproj | 1 + .../WorkflowManagementHttpApiHostModule.cs | 2 ++ .../PackageName.CompanyName.ProjectName.HttpApi.Host.csproj | 1 + .../ProjectNameHttpApiHostModule.cs | 2 ++ 4 files changed, 6 insertions(+) diff --git a/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/LY.MicroService.WorkflowManagement.HttpApi.Host.csproj b/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/LY.MicroService.WorkflowManagement.HttpApi.Host.csproj index d6cbaf1ef..2c026a202 100644 --- a/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/LY.MicroService.WorkflowManagement.HttpApi.Host.csproj +++ b/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/LY.MicroService.WorkflowManagement.HttpApi.Host.csproj @@ -46,6 +46,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/WorkflowManagementHttpApiHostModule.cs b/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/WorkflowManagementHttpApiHostModule.cs index 24184b66f..af2c0ce45 100644 --- a/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/WorkflowManagementHttpApiHostModule.cs +++ b/aspnet-core/services/LY.MicroService.WorkflowManagement.HttpApi.Host/WorkflowManagementHttpApiHostModule.cs @@ -40,6 +40,7 @@ using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.EntityFrameworkCore.MySQL; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Http.Client.IdentityModel.Web; +using Volo.Abp.MailKit; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.SettingManagement.EntityFrameworkCore; @@ -84,6 +85,7 @@ namespace LY.MicroService.WorkflowManagement; typeof(AbpLocalizationCultureMapModule), typeof(AbpHttpClientWrapperModule), typeof(AbpAspNetCoreMvcWrapperModule), + typeof(AbpMailKitModule), typeof(AbpClaimsMappingModule), typeof(AbpAspNetCoreMvcNewtonsoftModule), typeof(AbpAspNetCoreHttpOverridesModule), diff --git a/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/PackageName.CompanyName.ProjectName.HttpApi.Host.csproj b/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/PackageName.CompanyName.ProjectName.HttpApi.Host.csproj index 92df8c8a2..1b43640a4 100644 --- a/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/PackageName.CompanyName.ProjectName.HttpApi.Host.csproj +++ b/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/PackageName.CompanyName.ProjectName.HttpApi.Host.csproj @@ -59,6 +59,7 @@ + diff --git a/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/ProjectNameHttpApiHostModule.cs b/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/ProjectNameHttpApiHostModule.cs index 0419712fb..c385d7b69 100644 --- a/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/ProjectNameHttpApiHostModule.cs +++ b/aspnet-core/templates/content/host/PackageName.CompanyName.ProjectName.HttpApi.Host/ProjectNameHttpApiHostModule.cs @@ -28,6 +28,7 @@ using Volo.Abp.SettingManagement.EntityFrameworkCore; using Volo.Abp.Swashbuckle; using LINGYUN.Abp.AspNetCore.HttpOverrides; using LINGYUN.Abp.Identity.Session.AspNetCore; +using Volo.Abp.MailKit; namespace PackageName.CompanyName.ProjectName; @@ -56,6 +57,7 @@ namespace PackageName.CompanyName.ProjectName; typeof(AbpAspNetCoreMvcWrapperModule), typeof(AbpAspNetCoreHttpOverridesModule), typeof(AbpIdentitySessionAspNetCoreModule), + typeof(AbpMailKitModule), typeof(AbpSwashbuckleModule), typeof(AbpAutofacModule) )] From 94effc6e4991f5818d2408431dafb3662008a505 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 10:09:46 +0800 Subject: [PATCH 07/81] feat(platform): add user feedback interface --- .../RolePermissionDataSeedContributor.cs | 52 + .../20241114072301_Add-Feedback.Designer.cs | 5727 +++++++++++++++++ .../Migrations/20241114072301_Add-Feedback.cs | 122 + .../SingleMigrationsDbContextModelSnapshot.cs | 188 + .../20241114072749_Add-Feedback.Designer.cs | 1070 +++ .../Migrations/20241114072749_Add-Feedback.cs | 122 + ...latformMigrationsDbContextModelSnapshot.cs | 188 + .../Feedbacks/Dto/FeedbackAttachmentDto.cs | 10 + .../Dto/FeedbackAttachmentGetInput.cs | 14 + .../FeedbackAttachmentTempFileCreateDto.cs | 6 + .../Dto/FeedbackAttachmentTempFileDto.cs | 7 + .../Dto/FeedbackAttachmentUploadInput.cs | 12 + .../Feedbacks/Dto/FeedbackCommentCreateDto.cs | 10 + .../Dto/FeedbackCommentCreateOrUpdateDto.cs | 10 + .../Feedbacks/Dto/FeedbackCommentDto.cs | 8 + .../Feedbacks/Dto/FeedbackCommentUpdateDto.cs | 7 + .../Feedbacks/Dto/FeedbackCreateDto.cs | 17 + .../Platform/Feedbacks/Dto/FeedbackDto.cs | 13 + .../Feedbacks/Dto/FeedbackGetListInput.cs | 9 + .../Platform/Feedbacks/IFeedbackAppService.cs | 16 + .../IFeedbackAttachmentAppService.cs | 13 + .../Feedbacks/IFeedbackCommentAppService.cs | 17 + .../Feedbacks/IMyFeedbackAppService.cs | 9 + .../LINGYUN/Platform/Menus/IMenuAppService.cs | 58 +- .../PlatformPermissionDefinitionProvider.cs | 91 +- .../Permissions/PlatformPermissions.cs | 15 + .../Platform/PlatformRemoteServiceConsts.cs | 1 + .../Platform/Feedbacks/FeedbackAppService.cs | 107 + .../Feedbacks/FeedbackAttachmentAppService.cs | 69 + .../Feedbacks/FeedbackCommentAppService.cs | 10 + .../Feedbacks/MyFeedbackAppService.cs | 52 + .../PlatformApplicationMappingProfile.cs | 53 +- .../Expressions/ExpressionFuncExtensions.cs | 32 + .../Feedbacks/FeedbackAttachmentConsts.cs | 6 + .../Feedbacks/FeedbackAttachmentFile.cs | 6 + .../Feedbacks/FeedbackAttachmentTempFile.cs | 7 + .../Feedbacks/FeedbackCommentConsts.cs | 6 + .../Platform/Feedbacks/FeedbackConsts.cs | 6 + .../Platform/Feedbacks/FeedbackStatus.cs | 26 + .../Platform/Localization/Resources/en.json | 10 +- .../Localization/Resources/zh-Hans.json | 10 +- .../LINGYUN/Platform/PlatformErrorCodes.cs | 84 +- .../LINGYUN/Platform/Feedbacks/Feedback.cs | 161 + .../Platform/Feedbacks/FeedbackAttachment.cs | 34 + .../Feedbacks/FeedbackAttachmentManager.cs | 114 + .../FeedbackAttachmentNotFoundException.cs | 16 + .../Platform/Feedbacks/FeedbackComment.cs | 34 + .../Platform/Feedbacks/FeedbackContainer.cs | 8 + .../Feedbacks/FeedbackStatusException.cs | 25 + .../Platform/Feedbacks/IFeedbackRepository.cs | 21 + .../LINGYUN/Platform/PlatformDomainModule.cs | 128 +- ...PlatformDbContextModelBuilderExtensions.cs | 56 + .../PlatformEntityFrameworkCoreModule.cs | 3 + .../Feedbacks/EfCoreFeedbackRepository.cs | 51 + .../Feedbacks/FeedbackAttachmentController.cs | 43 + .../Platform/Feedbacks/FeedbackController.cs | 51 + .../Feedbacks/MyFeedbackController.cs | 27 + 57 files changed, 8873 insertions(+), 195 deletions(-) create mode 100644 aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/DataSeeder/RolePermissionDataSeedContributor.cs create mode 100644 aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.Designer.cs create mode 100644 aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.cs create mode 100644 aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.Designer.cs create mode 100644 aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentGetInput.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileCreateDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentUploadInput.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateOrUpdateDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentUpdateDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCreateDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackDto.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackGetListInput.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAttachmentAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackCommentAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IMyFeedbackAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAttachmentAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackCommentAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/MyFeedbackAppService.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Application/System/Linq/Expressions/ExpressionFuncExtensions.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentConsts.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentFile.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentTempFile.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackCommentConsts.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackConsts.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackStatus.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/Feedback.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentManager.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentNotFoundException.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackContainer.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackStatusException.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/IFeedbackRepository.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/Feedbacks/EfCoreFeedbackRepository.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackAttachmentController.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackController.cs create mode 100644 aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/MyFeedbackController.cs diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/DataSeeder/RolePermissionDataSeedContributor.cs b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/DataSeeder/RolePermissionDataSeedContributor.cs new file mode 100644 index 000000000..4c4bd0c00 --- /dev/null +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/DataSeeder/RolePermissionDataSeedContributor.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Data; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement; + +namespace LY.MicroService.Applications.Single.EntityFrameworkCore.DataSeeder; + +public class RolePermissionDataSeedContributor : IDataSeedContributor +{ + public ILogger Logger { protected get; set; } + + protected ICurrentTenant CurrentTenant { get; } + protected IPermissionDataSeeder PermissionDataSeeder { get; } + protected IPermissionDefinitionManager PermissionDefinitionManager { get; } + + public RolePermissionDataSeedContributor( + ICurrentTenant currentTenant, + IPermissionDataSeeder permissionDataSeeder, + IPermissionDefinitionManager permissionDefinitionManager) + { + CurrentTenant = currentTenant; + PermissionDataSeeder = permissionDataSeeder; + PermissionDefinitionManager = permissionDefinitionManager; + + Logger = NullLogger.Instance; + } + + public async virtual Task SeedAsync(DataSeedContext context) + { + using (CurrentTenant.Change(context.TenantId)) + { + Logger.LogInformation("Seeding the new tenant admin role permissions..."); + + var definitionPermissions = await PermissionDefinitionManager.GetPermissionsAsync(); + await PermissionDataSeeder.SeedAsync( + RolePermissionValueProvider.ProviderName, + "admin", + definitionPermissions.Select(x => x.Name), + context.TenantId); + + await PermissionDataSeeder.SeedAsync( + RolePermissionValueProvider.ProviderName, + "users", + new string[] { "Platform.Feedback.Create" }, + context.TenantId); + } + } +} diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.Designer.cs b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.Designer.cs new file mode 100644 index 000000000..0c4e65d53 --- /dev/null +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.Designer.cs @@ -0,0 +1,5727 @@ +// +using System; +using LY.MicroService.Applications.Single.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LY.MicroService.Applications.Single.EntityFrameworkCore.Migrations +{ + [DbContext(typeof(SingleMigrationsDbContext))] + [Migration("20241114072301_Add-Feedback")] + partial class AddFeedback + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.MySql) + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Abp.Demo.Authors.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BirthDate") + .HasColumnType("datetime(6)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ShortBio") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Demo_Authors", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Demo.Books.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorId") + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Price") + .HasColumnType("float"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Demo_Books", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.LocalizationManagement.Language", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("CultureName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("CultureName"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("DisplayName"); + + b.Property("Enable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("TwoLetterISOLanguageName") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("TwoLetterISOLanguageName"); + + b.Property("UiCultureName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("UiCultureName"); + + b.HasKey("Id"); + + b.HasIndex("CultureName"); + + b.ToTable("AbpLocalizationLanguages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.LocalizationManagement.Resource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DefaultCultureName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("DefaultCultureName"); + + b.Property("Description") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Description"); + + b.Property("DisplayName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("DisplayName"); + + b.Property("Enable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AbpLocalizationResources", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.LocalizationManagement.Text", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CultureName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("CultureName"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Key"); + + b.Property("ResourceName") + .HasColumnType("longtext"); + + b.Property("Value") + .HasMaxLength(2048) + .HasColumnType("varchar(2048)") + .HasColumnName("Value"); + + b.HasKey("Id"); + + b.HasIndex("Key"); + + b.ToTable("AbpLocalizationTexts", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Chat.UserChatCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("AvatarUrl") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("Birthday") + .HasColumnType("datetime(6)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LastOnlineTime") + .HasColumnType("datetime(6)"); + + b.Property("NickName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Sex") + .HasColumnType("int"); + + b.Property("Sign") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("State") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AppUserChatCards", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Chat.UserChatFriend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Black") + .HasColumnType("tinyint(1)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("DontDisturb") + .HasColumnType("tinyint(1)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("FrientId") + .HasColumnType("char(36)"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("RemarkName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("SpecialFocus") + .HasColumnType("tinyint(1)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "FrientId"); + + b.ToTable("AppUserChatFriends", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Chat.UserChatSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllowAddFriend") + .HasColumnType("tinyint(1)"); + + b.Property("AllowAnonymous") + .HasColumnType("tinyint(1)"); + + b.Property("AllowReceiveMessage") + .HasColumnType("tinyint(1)"); + + b.Property("AllowSendMessage") + .HasColumnType("tinyint(1)"); + + b.Property("RequireAddFriendValition") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AppUserChatSettings", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Chat.UserMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("MessageId") + .HasColumnType("bigint"); + + b.Property("ReceiveUserId") + .HasColumnType("char(36)"); + + b.Property("SendUserName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Source") + .HasColumnType("int"); + + b.Property("State") + .HasColumnType("tinyint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ReceiveUserId"); + + b.ToTable("AppUserMessages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Groups.ChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("AdminUserId") + .HasColumnType("char(36)"); + + b.Property("AllowAnonymous") + .HasColumnType("tinyint(1)"); + + b.Property("AllowSendMessage") + .HasColumnType("tinyint(1)"); + + b.Property("AvatarUrl") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MaxUserCount") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Notice") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Tag") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("AppChatGroups", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Groups.GroupChatBlack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("ShieldUserId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupId"); + + b.ToTable("AppGroupChatBlacks", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Groups.GroupMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("longtext"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("MessageId") + .HasColumnType("bigint"); + + b.Property("SendUserName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Source") + .HasColumnType("int"); + + b.Property("State") + .HasColumnType("tinyint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupId"); + + b.ToTable("AppGroupMessages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Groups.UserChatGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupId", "UserId"); + + b.ToTable("AppUserChatGroups", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.MessageService.Groups.UserGroupCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("NickName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("SilenceEnd") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AppUserGroupCards", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("ExpirationTime") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.Property("NotificationName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("NotificationTypeName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("Severity") + .HasColumnType("tinyint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "NotificationName"); + + b.ToTable("AppNotifications", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Notifications.NotificationDefinitionGroupRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowSubscriptionToClients") + .HasColumnType("tinyint(1)"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.ToTable("AppNotificationDefinitionGroups", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Notifications.NotificationDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowSubscriptionToClients") + .HasColumnType("tinyint(1)"); + + b.Property("ContentType") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("NotificationLifetime") + .HasColumnType("int"); + + b.Property("NotificationType") + .HasColumnType("int"); + + b.Property("Providers") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Template") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.ToTable("AppNotificationDefinitions", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Notifications.UserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("NotificationId") + .HasColumnType("bigint"); + + b.Property("ReadStatus") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "NotificationId") + .HasDatabaseName("IX_Tenant_User_Notification_Id"); + + b.ToTable("AppUserNotifications", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Notifications.UserSubscribe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("NotificationName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("UserName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasDefaultValue("/"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "NotificationName") + .IsUnique() + .HasDatabaseName("IX_Tenant_User_Notification_Name"); + + b.ToTable("AppUserSubscribes", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Saas.Editions.Edition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.HasKey("Id"); + + b.HasIndex("DisplayName"); + + b.ToTable("AbpEditions", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Saas.Tenants.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("DisableTime") + .HasColumnType("datetime(6)"); + + b.Property("EditionId") + .HasColumnType("char(36)"); + + b.Property("EnableTime") + .HasColumnType("datetime(6)"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("NormalizedName") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("Id"); + + b.HasIndex("EditionId"); + + b.HasIndex("Name"); + + b.HasIndex("NormalizedName"); + + b.ToTable("AbpTenants", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Saas.Tenants.TenantConnectionString", b => + { + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.HasKey("TenantId", "Name"); + + b.ToTable("AbpTenantConnectionStrings", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.TaskManagement.BackgroundJobAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("JobId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("JobId"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Name"); + + b.Property("Paramters") + .HasColumnType("longtext") + .HasColumnName("Paramters"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("TK_BackgroundJobActions", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.TaskManagement.BackgroundJobInfo", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Args") + .HasColumnType("longtext") + .HasColumnName("Args"); + + b.Property("BeginTime") + .HasColumnType("datetime(6)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Cron") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("Cron"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Description"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Group") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Group"); + + b.Property("Interval") + .HasColumnType("int"); + + b.Property("IsAbandoned") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("JobType") + .HasColumnType("int"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LastRunTime") + .HasColumnType("datetime(6)"); + + b.Property("LockTimeOut") + .HasColumnType("int"); + + b.Property("MaxCount") + .HasColumnType("int"); + + b.Property("MaxTryCount") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Name"); + + b.Property("NextRunTime") + .HasColumnType("datetime(6)"); + + b.Property("NodeName") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("NodeName"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Result") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("Result"); + + b.Property("Source") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("TriggerCount") + .HasColumnType("int"); + + b.Property("TryCount") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("Type"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Group"); + + b.ToTable("TK_BackgroundJobs", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.TaskManagement.BackgroundJobLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Exception") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("Exception"); + + b.Property("JobGroup") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("JobGroup"); + + b.Property("JobId") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("JobId"); + + b.Property("JobName") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("JobName"); + + b.Property("JobType") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("JobType"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("Message"); + + b.Property("RunTime") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("JobGroup", "JobName"); + + b.ToTable("TK_BackgroundJobLogs", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.TextTemplating.TextTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Content") + .HasMaxLength(1048576) + .HasColumnType("longtext") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Culture") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Culture"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("DisplayName"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_Tenant_Text_Template_Name"); + + b.ToTable("AbpTextTemplates", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.TextTemplating.TextTemplateDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("DefaultCultureName") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("DefaultCultureName"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsInlineLocalized") + .HasColumnType("tinyint(1)"); + + b.Property("IsLayout") + .HasColumnType("tinyint(1)"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("Layout") + .HasMaxLength(60) + .HasColumnType("varchar(60)") + .HasColumnName("Layout"); + + b.Property("LocalizationResourceName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("LocalizationResourceName"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("Name"); + + b.Property("RenderEngine") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("RenderEngine"); + + b.HasKey("Id"); + + b.ToTable("AbpTextTemplateDefinitions", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.WebhooksManagement.WebhookDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("RequiredFeatures") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpWebhooksWebhooks", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.WebhooksManagement.WebhookEventRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("Data") + .HasMaxLength(2147483647) + .HasColumnType("longtext") + .HasColumnName("Data"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WebhookName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("WebhookName"); + + b.HasKey("Id"); + + b.ToTable("AbpWebhooksEvents", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.WebhooksManagement.WebhookGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpWebhooksWebhookGroups", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.WebhooksManagement.WebhookSendRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("RequestHeaders") + .HasMaxLength(2147483647) + .HasColumnType("longtext") + .HasColumnName("RequestHeaders"); + + b.Property("Response") + .HasMaxLength(2147483647) + .HasColumnType("longtext") + .HasColumnName("Response"); + + b.Property("ResponseHeaders") + .HasMaxLength(2147483647) + .HasColumnType("longtext") + .HasColumnName("ResponseHeaders"); + + b.Property("ResponseStatusCode") + .HasColumnType("int"); + + b.Property("SendExactSameData") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WebhookEventId") + .HasColumnType("char(36)"); + + b.Property("WebhookSubscriptionId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("WebhookEventId"); + + b.ToTable("AbpWebhooksSendAttempts", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.WebhooksManagement.WebhookSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("Description"); + + b.Property("Headers") + .HasMaxLength(2147483647) + .HasColumnType("longtext") + .HasColumnName("Headers"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Secret") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("Secret"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeoutDuration") + .HasColumnType("int"); + + b.Property("WebhookUri") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("WebhookUri"); + + b.Property("Webhooks") + .HasMaxLength(2147483647) + .HasColumnType("longtext") + .HasColumnName("Webhooks"); + + b.HasKey("Id"); + + b.ToTable("AbpWebhooksSubscriptions", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.Data", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Description"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Name"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AppPlatformDatas", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.DataItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowBeNull") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DataId") + .HasColumnType("char(36)"); + + b.Property("DefaultValue") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DefaultValue"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Description"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Name"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("ValueType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DataId"); + + b.HasIndex("Name"); + + b.ToTable("AppPlatformDataItems", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Category"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformFeedbacks", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackAttachments", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Capacity"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackComments", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Layouts.Layout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DataId") + .HasColumnType("char(36)"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Framework") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Framework"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Path") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Path"); + + b.Property("Redirect") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Redirect"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformLayouts", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.Menu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(23) + .HasColumnType("varchar(23)") + .HasColumnName("Code"); + + b.Property("Component") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Component"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Framework") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Framework"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsPublic") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LayoutId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("Path") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Path"); + + b.Property("Redirect") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Redirect"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.RoleMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MenuId") + .HasColumnType("char(36)"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("RoleName"); + + b.Property("Startup") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleName", "MenuId"); + + b.ToTable("AppPlatformRoleMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.UserFavoriteMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AliasName") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("AliasName"); + + b.Property("Color") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Color"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("Framework") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Framework"); + + b.Property("Icon") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Icon"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MenuId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Path"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "MenuId"); + + b.ToTable("AppPlatformUserFavoriteMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.UserMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MenuId") + .HasColumnType("char(36)"); + + b.Property("Startup") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "MenuId"); + + b.ToTable("AppPlatformUserMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Authors") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Authors"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Description"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("ForceUpdate") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Name"); + + b.Property("Note") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Note"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Version"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Version"); + + b.ToTable("AppPlatformPackages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.PackageBlob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Authors") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Authors"); + + b.Property("ContentType") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("ContentType"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DownloadCount") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("License") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("License"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Name"); + + b.Property("PackageId") + .HasColumnType("char(36)"); + + b.Property("SHA256") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("SHA256"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Summary") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Summary"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Url") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.HasIndex("PackageId", "Name"); + + b.ToTable("AppPlatformPackageBlobs", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Portal.Enterprise", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Address"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("EnglishName") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("EnglishName"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LegalMan") + .HasMaxLength(60) + .HasColumnType("varchar(60)") + .HasColumnName("LegalMan"); + + b.Property("Logo") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Logo"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Name"); + + b.Property("OrganizationCode") + .HasMaxLength(16) + .HasColumnType("varchar(16)") + .HasColumnName("OrganizationCode"); + + b.Property("RegistrationCode") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("RegistrationCode"); + + b.Property("RegistrationDate") + .HasColumnType("datetime(6)"); + + b.Property("TaxCode") + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("TaxCode"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformEnterprises", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("varchar(96)") + .HasColumnName("ApplicationName"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("BrowserInfo"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ClientId"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ClientIpAddress"); + + b.Property("ClientName") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("ClientName"); + + b.Property("Comments") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("Comments"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("CorrelationId"); + + b.Property("Exceptions") + .HasColumnType("longtext"); + + b.Property("ExecutionDuration") + .HasColumnType("int") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("HttpMethod") + .HasMaxLength(16) + .HasColumnType("varchar(16)") + .HasColumnName("HttpMethod"); + + b.Property("HttpStatusCode") + .HasColumnType("int") + .HasColumnName("HttpStatusCode"); + + b.Property("ImpersonatorTenantId") + .HasColumnType("char(36)") + .HasColumnName("ImpersonatorTenantId"); + + b.Property("ImpersonatorTenantName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ImpersonatorTenantName"); + + b.Property("ImpersonatorUserId") + .HasColumnType("char(36)") + .HasColumnName("ImpersonatorUserId"); + + b.Property("ImpersonatorUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("ImpersonatorUserName"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("TenantName"); + + b.Property("Url") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("Url"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("UserId"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ExecutionTime"); + + b.HasIndex("TenantId", "UserId", "ExecutionTime"); + + b.ToTable("AbpAuditLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuditLogId") + .HasColumnType("char(36)") + .HasColumnName("AuditLogId"); + + b.Property("ExecutionDuration") + .HasColumnType("int") + .HasColumnName("ExecutionDuration"); + + b.Property("ExecutionTime") + .HasColumnType("datetime(6)") + .HasColumnName("ExecutionTime"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("MethodName") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("MethodName"); + + b.Property("Parameters") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("Parameters"); + + b.Property("ServiceName") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("ServiceName"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "ServiceName", "MethodName", "ExecutionTime"); + + b.ToTable("AbpAuditLogActions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuditLogId") + .HasColumnType("char(36)") + .HasColumnName("AuditLogId"); + + b.Property("ChangeTime") + .HasColumnType("datetime(6)") + .HasColumnName("ChangeTime"); + + b.Property("ChangeType") + .HasColumnType("tinyint unsigned") + .HasColumnName("ChangeType"); + + b.Property("EntityId") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("EntityId"); + + b.Property("EntityTenantId") + .HasColumnType("char(36)"); + + b.Property("EntityTypeFullName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("EntityTypeFullName"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AuditLogId"); + + b.HasIndex("TenantId", "EntityTypeFullName", "EntityId"); + + b.ToTable("AbpEntityChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EntityChangeId") + .HasColumnType("char(36)"); + + b.Property("NewValue") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("NewValue"); + + b.Property("OriginalValue") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("OriginalValue"); + + b.Property("PropertyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("PropertyName"); + + b.Property("PropertyTypeFullName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("PropertyTypeFullName"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("EntityChangeId"); + + b.ToTable("AbpEntityPropertyChanges", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowedProviders") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("DefaultValue") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("IsAvailableToHost") + .HasColumnType("tinyint(1)"); + + b.Property("IsVisibleToClients") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ValueType") + .HasMaxLength(2048) + .HasColumnType("varchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpFeatures", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpFeatureGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.FeatureManagement.FeatureValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("AbpFeatureValues", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Required") + .HasColumnType("tinyint(1)"); + + b.Property("ValueType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AbpClaimTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("SourceTenantId") + .HasColumnType("char(36)"); + + b.Property("SourceUserId") + .HasColumnType("char(36)"); + + b.Property("TargetTenantId") + .HasColumnType("char(36)"); + + b.Property("TargetUserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique(); + + b.ToTable("AbpLinkUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("tinyint(1)") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("AbpRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AbpRoleClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("varchar(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("varchar(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("varchar(96)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpSecurityLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("DeviceInfo") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("IpAddresses") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastAccessed") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("SignedIn") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Device"); + + b.HasIndex("SessionId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpSessions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("IsActive"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LastPasswordChangeTime") + .HasColumnType("datetime(6)"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("varchar(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("SecurityStamp"); + + b.Property("ShouldChangePasswordOnNextLogin") + .HasColumnType("tinyint(1)"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("AbpUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("SourceUserId") + .HasColumnType("char(36)"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("TargetUserId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AbpUserDelegations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("varchar(196)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("AbpUserLogins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("AbpUserOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("AbpUserRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AbpUserTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("varchar(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("AbpOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("char(36)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("AbpOrganizationUnitRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowedAccessTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("IdentityServerApiResources", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceClaim", b => + { + b.Property("ApiResourceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("ApiResourceId", "Type"); + + b.ToTable("IdentityServerApiResourceClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceProperty", b => + { + b.Property("ApiResourceId") + .HasColumnType("char(36)"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.HasKey("ApiResourceId", "Key", "Value"); + + b.ToTable("IdentityServerApiResourceProperties", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceScope", b => + { + b.Property("ApiResourceId") + .HasColumnType("char(36)"); + + b.Property("Scope") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("ApiResourceId", "Scope"); + + b.ToTable("IdentityServerApiResourceScopes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceSecret", b => + { + b.Property("ApiResourceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.HasKey("ApiResourceId", "Type", "Value"); + + b.ToTable("IdentityServerApiResourceSecrets", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Emphasize") + .HasColumnType("tinyint(1)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Required") + .HasColumnType("tinyint(1)"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("IdentityServerApiScopes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeClaim", b => + { + b.Property("ApiScopeId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("ApiScopeId", "Type"); + + b.ToTable("IdentityServerApiScopeClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeProperty", b => + { + b.Property("ApiScopeId") + .HasColumnType("char(36)"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.HasKey("ApiScopeId", "Key", "Value"); + + b.ToTable("IdentityServerApiScopeProperties", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AbsoluteRefreshTokenLifetime") + .HasColumnType("int"); + + b.Property("AccessTokenLifetime") + .HasColumnType("int"); + + b.Property("AccessTokenType") + .HasColumnType("int"); + + b.Property("AllowAccessTokensViaBrowser") + .HasColumnType("tinyint(1)"); + + b.Property("AllowOfflineAccess") + .HasColumnType("tinyint(1)"); + + b.Property("AllowPlainTextPkce") + .HasColumnType("tinyint(1)"); + + b.Property("AllowRememberConsent") + .HasColumnType("tinyint(1)"); + + b.Property("AllowedIdentityTokenSigningAlgorithms") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("AlwaysIncludeUserClaimsInIdToken") + .HasColumnType("tinyint(1)"); + + b.Property("AlwaysSendClientClaims") + .HasColumnType("tinyint(1)"); + + b.Property("AuthorizationCodeLifetime") + .HasColumnType("int"); + + b.Property("BackChannelLogoutSessionRequired") + .HasColumnType("tinyint(1)"); + + b.Property("BackChannelLogoutUri") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("ClientClaimsPrefix") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientUri") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConsentLifetime") + .HasColumnType("int"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("DeviceCodeLifetime") + .HasColumnType("int"); + + b.Property("EnableLocalLogin") + .HasColumnType("tinyint(1)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("FrontChannelLogoutSessionRequired") + .HasColumnType("tinyint(1)"); + + b.Property("FrontChannelLogoutUri") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("IdentityTokenLifetime") + .HasColumnType("int"); + + b.Property("IncludeJwtId") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LogoUri") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("PairWiseSubjectSalt") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ProtocolType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RefreshTokenExpiration") + .HasColumnType("int"); + + b.Property("RefreshTokenUsage") + .HasColumnType("int"); + + b.Property("RequireClientSecret") + .HasColumnType("tinyint(1)"); + + b.Property("RequireConsent") + .HasColumnType("tinyint(1)"); + + b.Property("RequirePkce") + .HasColumnType("tinyint(1)"); + + b.Property("RequireRequestObject") + .HasColumnType("tinyint(1)"); + + b.Property("SlidingRefreshTokenLifetime") + .HasColumnType("int"); + + b.Property("UpdateAccessTokenClaimsOnRefresh") + .HasColumnType("tinyint(1)"); + + b.Property("UserCodeType") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UserSsoLifetime") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("IdentityServerClients", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientClaim", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.HasKey("ClientId", "Type", "Value"); + + b.ToTable("IdentityServerClientClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientCorsOrigin", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("Origin") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("ClientId", "Origin"); + + b.ToTable("IdentityServerClientCorsOrigins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientGrantType", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("GrantType") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.HasKey("ClientId", "GrantType"); + + b.ToTable("IdentityServerClientGrantTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientIdPRestriction", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("Provider") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("ClientId", "Provider"); + + b.ToTable("IdentityServerClientIdPRestrictions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientPostLogoutRedirectUri", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("PostLogoutRedirectUri") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.HasKey("ClientId", "PostLogoutRedirectUri"); + + b.ToTable("IdentityServerClientPostLogoutRedirectUris", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientProperty", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.HasKey("ClientId", "Key", "Value"); + + b.ToTable("IdentityServerClientProperties", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientRedirectUri", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("RedirectUri") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.HasKey("ClientId", "RedirectUri"); + + b.ToTable("IdentityServerClientRedirectUris", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientScope", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("Scope") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("ClientId", "Scope"); + + b.ToTable("IdentityServerClientScopes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientSecret", b => + { + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientId", "Type", "Value"); + + b.ToTable("IdentityServerClientSecrets", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Devices.DeviceFlowCodes", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("varchar(10000)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UserCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.HasIndex("UserCode"); + + b.ToTable("IdentityServerDeviceFlowCodes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Grants.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConsumedTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("varchar(10000)"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Key"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("IdentityServerPersistedGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Emphasize") + .HasColumnType("tinyint(1)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Required") + .HasColumnType("tinyint(1)"); + + b.Property("ShowInDiscoveryDocument") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("IdentityServerIdentityResources", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceClaim", b => + { + b.Property("IdentityResourceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("IdentityResourceId", "Type"); + + b.ToTable("IdentityServerIdentityResourceClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceProperty", b => + { + b.Property("IdentityResourceId") + .HasColumnType("char(36)"); + + b.Property("Key") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("Value") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.HasKey("IdentityResourceId", "Key", "Value"); + + b.ToTable("IdentityServerIdentityResourceProperties", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.OpenIddict.Applications.OpenIddictApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("longtext"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ClientUri") + .HasColumnType("longtext"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .HasColumnType("longtext"); + + b.Property("DisplayNames") + .HasColumnType("longtext"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("JsonWebKeySet") + .HasColumnType("longtext"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LogoUri") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("longtext"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("RedirectUris") + .HasColumnType("longtext"); + + b.Property("Requirements") + .HasColumnType("longtext"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.OpenIddict.Authorizations.OpenIddictAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicationId") + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("Scopes") + .HasColumnType("longtext"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.OpenIddict.Scopes.OpenIddictScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Descriptions") + .HasColumnType("longtext"); + + b.Property("DisplayName") + .HasColumnType("longtext"); + + b.Property("DisplayNames") + .HasColumnType("longtext"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("Resources") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.OpenIddict.Tokens.OpenIddictToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApplicationId") + .HasColumnType("char(36)"); + + b.Property("AuthorizationId") + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Payload") + .HasColumnType("longtext"); + + b.Property("Properties") + .HasColumnType("longtext"); + + b.Property("RedemptionDate") + .HasColumnType("datetime(6)"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("varchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("MultiTenancySide") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Providers") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpPermissions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("AbpPermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpPermissionGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique(); + + b.ToTable("AbpSettings", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.SettingDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DefaultValue") + .HasMaxLength(2048) + .HasColumnType("varchar(2048)"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsEncrypted") + .HasColumnType("tinyint(1)"); + + b.Property("IsInherited") + .HasColumnType("tinyint(1)"); + + b.Property("IsVisibleToClients") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Providers") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpSettingDefinitions", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Abp.Demo.Books.Book", b => + { + b.HasOne("LINGYUN.Abp.Demo.Authors.Author", null) + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Abp.Saas.Tenants.Tenant", b => + { + b.HasOne("LINGYUN.Abp.Saas.Editions.Edition", "Edition") + .WithMany() + .HasForeignKey("EditionId"); + + b.Navigation("Edition"); + }); + + modelBuilder.Entity("LINGYUN.Abp.Saas.Tenants.TenantConnectionString", b => + { + b.HasOne("LINGYUN.Abp.Saas.Tenants.Tenant", null) + .WithMany("ConnectionStrings") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Abp.WebhooksManagement.WebhookSendRecord", b => + { + b.HasOne("LINGYUN.Abp.WebhooksManagement.WebhookEventRecord", "WebhookEvent") + .WithOne() + .HasForeignKey("LINGYUN.Abp.WebhooksManagement.WebhookSendRecord", "WebhookEventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WebhookEvent"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.DataItem", b => + { + b.HasOne("LINGYUN.Platform.Datas.Data", null) + .WithMany("Items") + .HasForeignKey("DataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Attachments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Comments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.PackageBlob", b => + { + b.HasOne("LINGYUN.Platform.Packages.Package", "Package") + .WithMany("Blobs") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("Actions") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) + .WithMany("EntityChanges") + .HasForeignKey("AuditLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityPropertyChange", b => + { + b.HasOne("Volo.Abp.AuditLogging.EntityChange", null) + .WithMany("PropertyChanges") + .HasForeignKey("EntityChangeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("Properties") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceScope", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResourceSecret", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiResources.ApiResource", null) + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiScopes.ApiScope", null) + .WithMany("UserClaims") + .HasForeignKey("ApiScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScopeProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.ApiScopes.ApiScope", null) + .WithMany("Properties") + .HasForeignKey("ApiScopeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientCorsOrigin", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientGrantType", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientIdPRestriction", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientPostLogoutRedirectUri", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("Properties") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientRedirectUri", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientScope", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.ClientSecret", b => + { + b.HasOne("Volo.Abp.IdentityServer.Clients.Client", null) + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceClaim", b => + { + b.HasOne("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", null) + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResourceProperty", b => + { + b.HasOne("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", null) + .WithMany("Properties") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.OpenIddict.Authorizations.OpenIddictAuthorization", b => + { + b.HasOne("Volo.Abp.OpenIddict.Applications.OpenIddictApplication", null) + .WithMany() + .HasForeignKey("ApplicationId"); + }); + + modelBuilder.Entity("Volo.Abp.OpenIddict.Tokens.OpenIddictToken", b => + { + b.HasOne("Volo.Abp.OpenIddict.Applications.OpenIddictApplication", null) + .WithMany() + .HasForeignKey("ApplicationId"); + + b.HasOne("Volo.Abp.OpenIddict.Authorizations.OpenIddictAuthorization", null) + .WithMany() + .HasForeignKey("AuthorizationId"); + }); + + modelBuilder.Entity("LINGYUN.Abp.Saas.Tenants.Tenant", b => + { + b.Navigation("ConnectionStrings"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.Data", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Navigation("Attachments"); + + b.Navigation("Comments"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.Package", b => + { + b.Navigation("Blobs"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => + { + b.Navigation("Actions"); + + b.Navigation("EntityChanges"); + }); + + modelBuilder.Entity("Volo.Abp.AuditLogging.EntityChange", b => + { + b.Navigation("PropertyChanges"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiResources.ApiResource", b => + { + b.Navigation("Properties"); + + b.Navigation("Scopes"); + + b.Navigation("Secrets"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.ApiScopes.ApiScope", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.Clients.Client", b => + { + b.Navigation("AllowedCorsOrigins"); + + b.Navigation("AllowedGrantTypes"); + + b.Navigation("AllowedScopes"); + + b.Navigation("Claims"); + + b.Navigation("ClientSecrets"); + + b.Navigation("IdentityProviderRestrictions"); + + b.Navigation("PostLogoutRedirectUris"); + + b.Navigation("Properties"); + + b.Navigation("RedirectUris"); + }); + + modelBuilder.Entity("Volo.Abp.IdentityServer.IdentityResources.IdentityResource", b => + { + b.Navigation("Properties"); + + b.Navigation("UserClaims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.cs b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.cs new file mode 100644 index 000000000..f453341b2 --- /dev/null +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/20241114072301_Add-Feedback.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LY.MicroService.Applications.Single.EntityFrameworkCore.Migrations +{ + /// + public partial class AddFeedback : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppPlatformFeedbacks", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TenantId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Content = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + ExtraProperties = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ConcurrencyStamp = table.Column(type: "varchar(40)", maxLength: 40, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + LastModificationTime = table.Column(type: "datetime(6)", nullable: true), + LastModifierId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + DeletionTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppPlatformFeedbacks", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AppPlatformFeedbackAttachments", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TenantId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Name = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Url = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Size = table.Column(type: "bigint", nullable: false), + FeedbackId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_AppPlatformFeedbackAttachments", x => x.Id); + table.ForeignKey( + name: "FK_AppPlatformFeedbackAttachments_AppPlatformFeedbacks_Feedback~", + column: x => x.FeedbackId, + principalTable: "AppPlatformFeedbacks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AppPlatformFeedbackComments", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TenantId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Capacity = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Content = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + FeedbackId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + LastModificationTime = table.Column(type: "datetime(6)", nullable: true), + LastModifierId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_AppPlatformFeedbackComments", x => x.Id); + table.ForeignKey( + name: "FK_AppPlatformFeedbackComments_AppPlatformFeedbacks_FeedbackId", + column: x => x.FeedbackId, + principalTable: "AppPlatformFeedbacks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AppPlatformFeedbackAttachments_FeedbackId", + table: "AppPlatformFeedbackAttachments", + column: "FeedbackId"); + + migrationBuilder.CreateIndex( + name: "IX_AppPlatformFeedbackComments_FeedbackId", + table: "AppPlatformFeedbackComments", + column: "FeedbackId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppPlatformFeedbackAttachments"); + + migrationBuilder.DropTable( + name: "AppPlatformFeedbackComments"); + + migrationBuilder.DropTable( + name: "AppPlatformFeedbacks"); + } + } +} diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/SingleMigrationsDbContextModelSnapshot.cs b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/SingleMigrationsDbContextModelSnapshot.cs index b7be23f59..639366b6b 100644 --- a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/SingleMigrationsDbContextModelSnapshot.cs +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/Migrations/SingleMigrationsDbContextModelSnapshot.cs @@ -1964,6 +1964,169 @@ namespace LY.MicroService.Applications.Single.EntityFrameworkCore.Migrations b.ToTable("AppPlatformDataItems", (string)null); }); + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Category"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformFeedbacks", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackAttachments", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Capacity"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackComments", (string)null); + }); + modelBuilder.Entity("LINGYUN.Platform.Layouts.Layout", b => { b.Property("Id") @@ -5138,6 +5301,24 @@ namespace LY.MicroService.Applications.Single.EntityFrameworkCore.Migrations .IsRequired(); }); + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Attachments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Comments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("LINGYUN.Platform.Packages.PackageBlob", b => { b.HasOne("LINGYUN.Platform.Packages.Package", "Package") @@ -5445,6 +5626,13 @@ namespace LY.MicroService.Applications.Single.EntityFrameworkCore.Migrations b.Navigation("Items"); }); + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Navigation("Attachments"); + + b.Navigation("Comments"); + }); + modelBuilder.Entity("LINGYUN.Platform.Packages.Package", b => { b.Navigation("Blobs"); diff --git a/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.Designer.cs b/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.Designer.cs new file mode 100644 index 000000000..2c54ed3cb --- /dev/null +++ b/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.Designer.cs @@ -0,0 +1,1070 @@ +// +using System; +using LY.MicroService.Platform.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace LY.MicroService.Platform.EntityFrameworkCore.Migrations +{ + [DbContext(typeof(PlatformMigrationsDbContext))] + [Migration("20241114072749_Add-Feedback")] + partial class AddFeedback + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.MySql) + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LINGYUN.Platform.Datas.Data", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Description"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Name"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("AppPlatformDatas", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.DataItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowBeNull") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DataId") + .HasColumnType("char(36)"); + + b.Property("DefaultValue") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DefaultValue"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Description"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsStatic") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Name"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("ValueType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DataId"); + + b.HasIndex("Name"); + + b.ToTable("AppPlatformDataItems", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Category"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformFeedbacks", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackAttachments", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Capacity"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackComments", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Layouts.Layout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DataId") + .HasColumnType("char(36)"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Framework") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Framework"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Path") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Path"); + + b.Property("Redirect") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Redirect"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformLayouts", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.Menu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(23) + .HasColumnType("varchar(23)") + .HasColumnName("Code"); + + b.Property("Component") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Component"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("Framework") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Framework"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsPublic") + .HasColumnType("tinyint(1)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LayoutId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("ParentId") + .HasColumnType("char(36)"); + + b.Property("Path") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Path"); + + b.Property("Redirect") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Redirect"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.RoleMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MenuId") + .HasColumnType("char(36)"); + + b.Property("RoleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("RoleName"); + + b.Property("Startup") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleName", "MenuId"); + + b.ToTable("AppPlatformRoleMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.UserFavoriteMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AliasName") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("AliasName"); + + b.Property("Color") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Color"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("DisplayName"); + + b.Property("Framework") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Framework"); + + b.Property("Icon") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Icon"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MenuId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Path"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "MenuId"); + + b.ToTable("AppPlatformUserFavoriteMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Menus.UserMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("MenuId") + .HasColumnType("char(36)"); + + b.Property("Startup") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "MenuId"); + + b.ToTable("AppPlatformUserMenus", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.Package", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Authors") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Authors"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Description"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("ForceUpdate") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Name"); + + b.Property("Note") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Note"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("Version"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Version"); + + b.ToTable("AppPlatformPackages", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.PackageBlob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Authors") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("Authors"); + + b.Property("ContentType") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("ContentType"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DownloadCount") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("License") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("License"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Name"); + + b.Property("PackageId") + .HasColumnType("char(36)"); + + b.Property("SHA256") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("SHA256"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Summary") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("Summary"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Url") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.HasIndex("PackageId", "Name"); + + b.ToTable("AppPlatformPackageBlobs", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Portal.Enterprise", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Address"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("EnglishName") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("EnglishName"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("LegalMan") + .HasMaxLength(60) + .HasColumnType("varchar(60)") + .HasColumnName("LegalMan"); + + b.Property("Logo") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("Logo"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Name"); + + b.Property("OrganizationCode") + .HasMaxLength(16) + .HasColumnType("varchar(16)") + .HasColumnName("OrganizationCode"); + + b.Property("RegistrationCode") + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("RegistrationCode"); + + b.Property("RegistrationDate") + .HasColumnType("datetime(6)"); + + b.Property("TaxCode") + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("TaxCode"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformEnterprises", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.DataItem", b => + { + b.HasOne("LINGYUN.Platform.Datas.Data", null) + .WithMany("Items") + .HasForeignKey("DataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Attachments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Comments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.PackageBlob", b => + { + b.HasOne("LINGYUN.Platform.Packages.Package", "Package") + .WithMany("Blobs") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Datas.Data", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Navigation("Attachments"); + + b.Navigation("Comments"); + }); + + modelBuilder.Entity("LINGYUN.Platform.Packages.Package", b => + { + b.Navigation("Blobs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.cs b/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.cs new file mode 100644 index 000000000..650774174 --- /dev/null +++ b/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/20241114072749_Add-Feedback.cs @@ -0,0 +1,122 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LY.MicroService.Platform.EntityFrameworkCore.Migrations +{ + /// + public partial class AddFeedback : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppPlatformFeedbacks", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TenantId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Content = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + ExtraProperties = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ConcurrencyStamp = table.Column(type: "varchar(40)", maxLength: 40, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + LastModificationTime = table.Column(type: "datetime(6)", nullable: true), + LastModifierId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + DeletionTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppPlatformFeedbacks", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AppPlatformFeedbackAttachments", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TenantId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Name = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Url = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Size = table.Column(type: "bigint", nullable: false), + FeedbackId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_AppPlatformFeedbackAttachments", x => x.Id); + table.ForeignKey( + name: "FK_AppPlatformFeedbackAttachments_AppPlatformFeedbacks_Feedback~", + column: x => x.FeedbackId, + principalTable: "AppPlatformFeedbacks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AppPlatformFeedbackComments", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + TenantId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Capacity = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Content = table.Column(type: "varchar(255)", maxLength: 255, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + FeedbackId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreationTime = table.Column(type: "datetime(6)", nullable: false), + CreatorId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + LastModificationTime = table.Column(type: "datetime(6)", nullable: true), + LastModifierId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_AppPlatformFeedbackComments", x => x.Id); + table.ForeignKey( + name: "FK_AppPlatformFeedbackComments_AppPlatformFeedbacks_FeedbackId", + column: x => x.FeedbackId, + principalTable: "AppPlatformFeedbacks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AppPlatformFeedbackAttachments_FeedbackId", + table: "AppPlatformFeedbackAttachments", + column: "FeedbackId"); + + migrationBuilder.CreateIndex( + name: "IX_AppPlatformFeedbackComments_FeedbackId", + table: "AppPlatformFeedbackComments", + column: "FeedbackId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppPlatformFeedbackAttachments"); + + migrationBuilder.DropTable( + name: "AppPlatformFeedbackComments"); + + migrationBuilder.DropTable( + name: "AppPlatformFeedbacks"); + } + } +} diff --git a/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/PlatformMigrationsDbContextModelSnapshot.cs b/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/PlatformMigrationsDbContextModelSnapshot.cs index 5f5e78da1..6bbc8b5ef 100644 --- a/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/PlatformMigrationsDbContextModelSnapshot.cs +++ b/aspnet-core/migrations/LY.MicroService.Platform.EntityFrameworkCore/Migrations/PlatformMigrationsDbContextModelSnapshot.cs @@ -209,6 +209,169 @@ namespace LY.MicroService.Platform.EntityFrameworkCore.Migrations b.ToTable("AppPlatformDataItems", (string)null); }); + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Category"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("char(36)") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime(6)") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AppPlatformFeedbacks", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Name"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Url"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackAttachments", (string)null); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Capacity") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("Capacity"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasColumnName("Content"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("char(36)") + .HasColumnName("CreatorId"); + + b.Property("FeedbackId") + .HasColumnType("char(36)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime(6)") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("char(36)") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("char(36)") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackId"); + + b.ToTable("AppPlatformFeedbackComments", (string)null); + }); + modelBuilder.Entity("LINGYUN.Platform.Layouts.Layout", b => { b.Property("Id") @@ -853,6 +1016,24 @@ namespace LY.MicroService.Platform.EntityFrameworkCore.Migrations .IsRequired(); }); + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackAttachment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Attachments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.FeedbackComment", b => + { + b.HasOne("LINGYUN.Platform.Feedbacks.Feedback", null) + .WithMany("Comments") + .HasForeignKey("FeedbackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("LINGYUN.Platform.Packages.PackageBlob", b => { b.HasOne("LINGYUN.Platform.Packages.Package", "Package") @@ -869,6 +1050,13 @@ namespace LY.MicroService.Platform.EntityFrameworkCore.Migrations b.Navigation("Items"); }); + modelBuilder.Entity("LINGYUN.Platform.Feedbacks.Feedback", b => + { + b.Navigation("Attachments"); + + b.Navigation("Comments"); + }); + modelBuilder.Entity("LINGYUN.Platform.Packages.Package", b => { b.Navigation("Blobs"); diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentDto.cs new file mode 100644 index 000000000..6303bd3eb --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentDto.cs @@ -0,0 +1,10 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentDto : CreationAuditedEntityDto +{ + public string Name { get; set; } + public string Url { get; set; } + public long Size { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentGetInput.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentGetInput.cs new file mode 100644 index 000000000..5592d7756 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentGetInput.cs @@ -0,0 +1,14 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentGetInput +{ + [Required] + public Guid FeedbackId { get; set; } + + [Required] + [DynamicStringLength(typeof(FeedbackAttachmentConsts), nameof(FeedbackAttachmentConsts.MaxNameLength))] + public string Name { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileCreateDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileCreateDto.cs new file mode 100644 index 000000000..5b92490a9 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileCreateDto.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentTempFileCreateDto +{ + public string Path { get; set; } + public string Id { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileDto.cs new file mode 100644 index 000000000..2ebae2d07 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentTempFileDto.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentTempFileDto +{ + public string Path { get; set; } + public string Id { get; set; } + public long Size { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentUploadInput.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentUploadInput.cs new file mode 100644 index 000000000..d0a223c73 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackAttachmentUploadInput.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Auditing; +using Volo.Abp.Content; +using Volo.Abp.Validation; + +namespace LINGYUN.Platform.Feedbacks; + +public class FeedbackAttachmentUploadInput +{ + [DisableAuditing] + [DisableValidation] + public IRemoteStreamContent File { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateDto.cs new file mode 100644 index 000000000..6e97fbad4 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackCommentCreateDto : FeedbackCommentCreateOrUpdateDto +{ + [Required] + [DynamicStringLength(typeof(FeedbackCommentConsts), nameof(FeedbackCommentConsts.MaxCapacityLength))] + public string Capacity { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateOrUpdateDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateOrUpdateDto.cs new file mode 100644 index 000000000..ab771a86c --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentCreateOrUpdateDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Platform.Feedbacks; +public abstract class FeedbackCommentCreateOrUpdateDto +{ + [Required] + [DynamicStringLength(typeof(FeedbackCommentConsts), nameof(FeedbackCommentConsts.MaxContentLength))] + public string Content { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentDto.cs new file mode 100644 index 000000000..bb3390bea --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentDto.cs @@ -0,0 +1,8 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackCommentDto : AuditedEntityDto +{ + public string Content { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentUpdateDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentUpdateDto.cs new file mode 100644 index 000000000..f05c19e33 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCommentUpdateDto.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Domain.Entities; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackCommentUpdateDto : FeedbackCommentCreateOrUpdateDto, IHasConcurrencyStamp +{ + public string ConcurrencyStamp { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCreateDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCreateDto.cs new file mode 100644 index 000000000..7c657e5c9 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackCreateDto.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackCreateDto +{ + [Required] + [DynamicStringLength(typeof(FeedbackConsts), nameof(FeedbackConsts.MaxContentLength))] + public string Content { get; set; } + + [Required] + [DynamicStringLength(typeof(FeedbackConsts), nameof(FeedbackConsts.MaxCategoryLength))] + public string Category { get; set; } + + public List Attachments { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackDto.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackDto.cs new file mode 100644 index 000000000..bfe1646fa --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackDto : ExtensibleAuditedEntityDto +{ + public string Content { get; set; } + public string Category { get; set; } + public FeedbackStatus Status { get; set; } + public List Comments { get; set; } + public List Attachments { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackGetListInput.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackGetListInput.cs new file mode 100644 index 000000000..b6c2a0e0b --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/Dto/FeedbackGetListInput.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackGetListInput : PagedAndSortedResultRequestDto +{ + public string Filter { get; set; } + public string Category { get; set; } + public FeedbackStatus? Status { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAppService.cs new file mode 100644 index 000000000..a5172de6b --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAppService.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Platform.Feedbacks; +public interface IFeedbackAppService : IApplicationService +{ + Task GetAsync(Guid id); + + Task CreateAsync(FeedbackCreateDto input); + + Task DeleteAsync(Guid id); + + Task> GetListAsync(FeedbackGetListInput input); +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAttachmentAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAttachmentAppService.cs new file mode 100644 index 000000000..1e2bc1acb --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackAttachmentAppService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Content; + +namespace LINGYUN.Platform.Feedbacks; +public interface IFeedbackAttachmentAppService : IApplicationService +{ + Task UploadAsync(FeedbackAttachmentUploadInput input); + + Task GetAsync(FeedbackAttachmentGetInput input); + + Task DeleteAsync(FeedbackAttachmentGetInput input); +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackCommentAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackCommentAppService.cs new file mode 100644 index 000000000..d6a8fceab --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IFeedbackCommentAppService.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Platform.Feedbacks; +public interface IFeedbackCommentAppService : IApplicationService +{ + Task ProgressAsync(FeedbackCommentCreateDto input); + + Task CloseAsync(FeedbackCommentCreateDto input); + + Task ResolveAsync(FeedbackCommentCreateDto input); + + Task UpdateAsync(Guid id, FeedbackCommentUpdateDto input); + + Task DeleteAsync(Guid id); +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IMyFeedbackAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IMyFeedbackAppService.cs new file mode 100644 index 000000000..e4fcbfd58 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Feedbacks/IMyFeedbackAppService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Platform.Feedbacks; +public interface IMyFeedbackAppService : IApplicationService +{ + Task> GetMyFeedbacksAsync(FeedbackGetListInput input); +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Menus/IMenuAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Menus/IMenuAppService.cs index df5850c47..c4e6d909a 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Menus/IMenuAppService.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Menus/IMenuAppService.cs @@ -1,31 +1,31 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace LINGYUN.Platform.Menus; - -public interface IMenuAppService : - ICrudAppService< - MenuDto, - Guid, - MenuGetListInput, - MenuCreateDto, - MenuUpdateDto> -{ - Task> GetAllAsync(MenuGetAllInput input); - - Task> GetUserMenuListAsync(MenuGetByUserInput input); - - Task> GetRoleMenuListAsync(MenuGetByRoleInput input); - - Task SetUserMenusAsync(UserMenuInput input); - - Task SetUserStartupAsync(Guid id, UserMenuStartupInput input); - +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace LINGYUN.Platform.Menus; + +public interface IMenuAppService : + ICrudAppService< + MenuDto, + Guid, + MenuGetListInput, + MenuCreateDto, + MenuUpdateDto> +{ + Task> GetAllAsync(MenuGetAllInput input); + + Task> GetUserMenuListAsync(MenuGetByUserInput input); + + Task> GetRoleMenuListAsync(MenuGetByRoleInput input); + + Task SetUserMenusAsync(UserMenuInput input); + + Task SetUserStartupAsync(Guid id, UserMenuStartupInput input); + Task SetRoleMenusAsync(RoleMenuInput input); - Task SetRoleStartupAsync(Guid id, RoleMenuStartupInput input); - - Task> GetCurrentUserMenuListAsync(GetMenuInput input); -} + Task SetRoleStartupAsync(Guid id, RoleMenuStartupInput input); + + Task> GetCurrentUserMenuListAsync(GetMenuInput input); +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissionDefinitionProvider.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissionDefinitionProvider.cs index dabb728f0..24e0854e2 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissionDefinitionProvider.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissionDefinitionProvider.cs @@ -1,44 +1,51 @@ -using LINGYUN.Platform.Localization; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Localization; - -namespace LINGYUN.Platform.Permissions; - -public class PlatformPermissionDefinitionProvider : PermissionDefinitionProvider -{ - public override void Define(IPermissionDefinitionContext context) - { - var platform = context.AddGroup(PlatformPermissions.GroupName, L("Permission:Platform")); - - var dataDictionary = platform.AddPermission(PlatformPermissions.DataDictionary.Default, L("Permission:DataDictionary")); - dataDictionary.AddChild(PlatformPermissions.DataDictionary.Create, L("Permission:Create")); - dataDictionary.AddChild(PlatformPermissions.DataDictionary.Update, L("Permission:Update")); - dataDictionary.AddChild(PlatformPermissions.DataDictionary.Move, L("Permission:Move")); - dataDictionary.AddChild(PlatformPermissions.DataDictionary.Delete, L("Permission:Delete")); - dataDictionary.AddChild(PlatformPermissions.DataDictionary.ManageItems, L("Permission:ManageItems")); - - var layout = platform.AddPermission(PlatformPermissions.Layout.Default, L("Permission:Layout")); - layout.AddChild(PlatformPermissions.Layout.Create, L("Permission:Create")); - layout.AddChild(PlatformPermissions.Layout.Update, L("Permission:Update")); - layout.AddChild(PlatformPermissions.Layout.Delete, L("Permission:Delete")); - - var menu = platform.AddPermission(PlatformPermissions.Menu.Default, L("Permission:Menu")); - menu.AddChild(PlatformPermissions.Menu.Create, L("Permission:Create")); - menu.AddChild(PlatformPermissions.Menu.Update, L("Permission:Update")); - menu.AddChild(PlatformPermissions.Menu.Delete, L("Permission:Delete")); - menu.AddChild(PlatformPermissions.Menu.ManageRoles, L("Permission:ManageRoleMenus")); +using LINGYUN.Platform.Localization; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace LINGYUN.Platform.Permissions; + +public class PlatformPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var platform = context.AddGroup(PlatformPermissions.GroupName, L("Permission:Platform")); + + var dataDictionary = platform.AddPermission(PlatformPermissions.DataDictionary.Default, L("Permission:DataDictionary")); + dataDictionary.AddChild(PlatformPermissions.DataDictionary.Create, L("Permission:Create")); + dataDictionary.AddChild(PlatformPermissions.DataDictionary.Update, L("Permission:Update")); + dataDictionary.AddChild(PlatformPermissions.DataDictionary.Move, L("Permission:Move")); + dataDictionary.AddChild(PlatformPermissions.DataDictionary.Delete, L("Permission:Delete")); + dataDictionary.AddChild(PlatformPermissions.DataDictionary.ManageItems, L("Permission:ManageItems")); + + var layout = platform.AddPermission(PlatformPermissions.Layout.Default, L("Permission:Layout")); + layout.AddChild(PlatformPermissions.Layout.Create, L("Permission:Create")); + layout.AddChild(PlatformPermissions.Layout.Update, L("Permission:Update")); + layout.AddChild(PlatformPermissions.Layout.Delete, L("Permission:Delete")); + + var menu = platform.AddPermission(PlatformPermissions.Menu.Default, L("Permission:Menu")); + menu.AddChild(PlatformPermissions.Menu.Create, L("Permission:Create")); + menu.AddChild(PlatformPermissions.Menu.Update, L("Permission:Update")); + menu.AddChild(PlatformPermissions.Menu.Delete, L("Permission:Delete")); + menu.AddChild(PlatformPermissions.Menu.ManageRoles, L("Permission:ManageRoleMenus")); menu.AddChild(PlatformPermissions.Menu.ManageUsers, L("Permission:ManageUserMenus")); - menu.AddChild(PlatformPermissions.Menu.ManageUserFavorites, L("Permission:ManageUserFavoriteMenus")); - - var package = platform.AddPermission(PlatformPermissions.Package.Default, L("Permission:Package")); - package.AddChild(PlatformPermissions.Package.Create, L("Permission:Create")); - package.AddChild(PlatformPermissions.Package.Update, L("Permission:Update")); + menu.AddChild(PlatformPermissions.Menu.ManageUserFavorites, L("Permission:ManageUserFavoriteMenus")); + + var package = platform.AddPermission(PlatformPermissions.Package.Default, L("Permission:Package")); + package.AddChild(PlatformPermissions.Package.Create, L("Permission:Create")); + package.AddChild(PlatformPermissions.Package.Update, L("Permission:Update")); package.AddChild(PlatformPermissions.Package.Delete, L("Permission:Delete")); - package.AddChild(PlatformPermissions.Package.ManageBlobs, L("Permission:ManageBlobs")); - } - - private static LocalizableString L(string name) - { - return LocalizableString.Create(name); - } -} + package.AddChild(PlatformPermissions.Package.ManageBlobs, L("Permission:ManageBlobs")); + + var feedback = platform.AddPermission(PlatformPermissions.Feedback.Default, L("Permission:Feedback")); + feedback.AddChild(PlatformPermissions.Feedback.Create, L("Permission:Create")); + feedback.AddChild(PlatformPermissions.Feedback.Update, L("Permission:Update")); + feedback.AddChild(PlatformPermissions.Feedback.Delete, L("Permission:Delete")); + feedback.AddChild(PlatformPermissions.Feedback.ManageAttachments, L("Permission:ManageAttachments")); + feedback.AddChild(PlatformPermissions.Feedback.ManageComments, L("Permission:ManageComments")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissions.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissions.cs index c98f0c487..ba324b78f 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissions.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/Permissions/PlatformPermissions.cs @@ -98,6 +98,21 @@ public static class PlatformPermissions public const string ManageBlobs = Default + ".ManageBlobs"; } + public class Feedback + { + public const string Default = GroupName + ".Feedback"; + + public const string Create = Default + ".Create"; + + public const string Update = Default + ".Update"; + + public const string Delete = Default + ".Delete"; + + public const string ManageComments = Default + ".ManageComments"; + + public const string ManageAttachments = Default + ".ManageAttachments"; + } + public static string[] GetAll() { return ReflectionHelper.GetPublicConstantsRecursively(typeof(PlatformPermissions)); diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/PlatformRemoteServiceConsts.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/PlatformRemoteServiceConsts.cs index 91ba4b5bb..1d65edcc4 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/PlatformRemoteServiceConsts.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application.Contracts/LINGYUN/Platform/PlatformRemoteServiceConsts.cs @@ -3,4 +3,5 @@ public static class PlatformRemoteServiceConsts { public const string RemoteServiceName = "Platform"; + public const string ModuleName = "platform"; } diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAppService.cs new file mode 100644 index 000000000..17e42ffea --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAppService.cs @@ -0,0 +1,107 @@ +using LINGYUN.Platform.Permissions; +using Microsoft.AspNetCore.Authorization; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; + +namespace LINGYUN.Platform.Feedbacks; + +[Authorize(PlatformPermissions.Feedback.Default)] +public class FeedbackAppService : PlatformApplicationServiceBase, IFeedbackAppService +{ + private readonly FeedbackAttachmentManager _attachmentManager; + private readonly IFeedbackRepository _feedbackRepository; + + public FeedbackAppService( + FeedbackAttachmentManager attachmentManager, + IFeedbackRepository feedbackRepository) + { + _attachmentManager = attachmentManager; + _feedbackRepository = feedbackRepository; + } + + [Authorize(PlatformPermissions.Feedback.Create)] + public async virtual Task CreateAsync(FeedbackCreateDto input) + { + var feedback = new Feedback( + GuidGenerator.Create(), + input.Category, + input.Content, + FeedbackStatus.Created, + CurrentTenant.Id); + + if (input.Attachments != null) + { + foreach (var attachment in input.Attachments) + { + var attachmentFile = await _attachmentManager.CopyFromTempAsync( + feedback, + attachment.Path, + attachment.Id); + + feedback.AddAttachment( + GuidGenerator, + attachmentFile.Name, + $"/api/platform/feedbacks/{feedback.Id}/attachments/{attachmentFile.Name}", + attachmentFile.Size); + } + } + + feedback = await _feedbackRepository.InsertAsync(feedback); + + await CurrentUnitOfWork.SaveChangesAsync(); + + return ObjectMapper.Map(feedback); + } + + [Authorize(PlatformPermissions.Feedback.Delete)] + public async Task DeleteAsync(Guid id) + { + var feedback = await _feedbackRepository.GetAsync(id); + + await _feedbackRepository.DeleteAsync(feedback); + + await CurrentUnitOfWork.SaveChangesAsync(); + } + + [Authorize(PlatformPermissions.Feedback.Default)] + public async virtual Task GetAsync(Guid id) + { + var feedback = await _feedbackRepository.GetAsync(id); + + return ObjectMapper.Map(feedback); + } + + public async virtual Task> GetListAsync(FeedbackGetListInput input) + { + var specification = new FeedbackGetListSpecification(input); + + var totalCount = await _feedbackRepository.GetCountAsync(specification); + var feedbacks = await _feedbackRepository.GetListAsync(specification, + input.Sorting, input.MaxResultCount, input.SkipCount); + + return new PagedResultDto(totalCount, + ObjectMapper.Map, List>(feedbacks)); + } + + internal class FeedbackGetListSpecification : Volo.Abp.Specifications.Specification + { + protected FeedbackGetListInput Input { get; } + public FeedbackGetListSpecification(FeedbackGetListInput input) + { + Input = input; + } + public override Expression> ToExpression() + { + Expression> expression = _ => true; + + return expression + .AndIf(Input.Status.HasValue, x => x.Status == Input.Status) + .AndIf(!Input.Category.IsNullOrWhiteSpace(), x => x.Category == Input.Category) + .AndIf(!Input.Filter.IsNullOrWhiteSpace(), x => x.Category.Contains(Input.Filter) || + x.Content.Contains(Input.Filter)); + } + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAttachmentAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAttachmentAppService.cs new file mode 100644 index 000000000..ab7e0042c --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackAttachmentAppService.cs @@ -0,0 +1,69 @@ +using LINGYUN.Platform.Permissions; +using Microsoft.AspNetCore.Authorization; +using System; +using System.Threading.Tasks; +using Volo.Abp.Content; + +namespace LINGYUN.Platform.Feedbacks; + +[Authorize(PlatformPermissions.Feedback.Default)] +public class FeedbackAttachmentAppService : PlatformApplicationServiceBase, IFeedbackAttachmentAppService +{ + private readonly IFeedbackRepository _feedbackRepository; + private readonly FeedbackAttachmentManager _feedbackAttachmentManager; + + public FeedbackAttachmentAppService( + IFeedbackRepository feedbackRepository, + FeedbackAttachmentManager feedbackAttachmentManager) + { + _feedbackRepository = feedbackRepository; + _feedbackAttachmentManager = feedbackAttachmentManager; + } + + public async virtual Task UploadAsync(FeedbackAttachmentUploadInput input) + { + var stream = input.File.GetStream(); + + var tmpFile = await _feedbackAttachmentManager.SaveToTempAsync(stream); + + return new FeedbackAttachmentTempFileDto + { + Size = tmpFile.Size, + Path = tmpFile.Path, + Id = tmpFile.Id, + }; + } + + public async virtual Task GetAsync(FeedbackAttachmentGetInput input) + { + var attachment = await GetFeedbackAttachmentAsync(input); + + var stream = await _feedbackAttachmentManager.DownloadAsync(attachment); + + return new RemoteStreamContent(stream, attachment.Name); + } + + public async virtual Task DeleteAsync(FeedbackAttachmentGetInput input) + { + var feedback = await _feedbackRepository.GetAsync(input.FeedbackId); + if (feedback.CreatorId != CurrentUser.Id) + { + await AuthorizationService.CheckAsync(PlatformPermissions.Feedback.ManageAttachments); + } + + var attachment = feedback.FindAttachment(input.Name); + + feedback.RemoveAttachment(attachment.Name); + + await CurrentUnitOfWork.SaveChangesAsync(); + + await _feedbackAttachmentManager.DeleteAsync(attachment); + } + + protected async virtual Task GetFeedbackAttachmentAsync(FeedbackAttachmentGetInput input) + { + var feedback = await _feedbackRepository.GetAsync(input.FeedbackId); + var attachment = feedback.FindAttachment(input.Name); + return attachment ?? throw new FeedbackAttachmentNotFoundException(input.Name); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackCommentAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackCommentAppService.cs new file mode 100644 index 000000000..be4251f2c --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/FeedbackCommentAppService.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackCommentAppService +{ +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/MyFeedbackAppService.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/MyFeedbackAppService.cs new file mode 100644 index 000000000..ae66ca903 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/Feedbacks/MyFeedbackAppService.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Users; +using static LINGYUN.Platform.Feedbacks.FeedbackAppService; + +namespace LINGYUN.Platform.Feedbacks; + +[Authorize] +public class MyFeedbackAppService : PlatformApplicationServiceBase, IMyFeedbackAppService +{ + private readonly IFeedbackRepository _feedbackRepository; + public MyFeedbackAppService(IFeedbackRepository feedbackRepository) + { + _feedbackRepository = feedbackRepository; + } + + public async virtual Task> GetMyFeedbacksAsync(FeedbackGetListInput input) + { + var specification = new FeedbackGetListByUserSpecification(CurrentUser.GetId(), input); + + var totalCount = await _feedbackRepository.GetCountAsync(specification); + var feedbacks = await _feedbackRepository.GetListAsync(specification, + input.Sorting, input.MaxResultCount, input.SkipCount); + + return new PagedResultDto(totalCount, + ObjectMapper.Map, List>(feedbacks)); + } + + private class FeedbackGetListByUserSpecification : FeedbackGetListSpecification + { + protected Guid UserId { get; } + public FeedbackGetListByUserSpecification( + Guid userId, + FeedbackGetListInput input) + : base(input) + { + UserId = userId; + } + + public override Expression> ToExpression() + { + var expression = base.ToExpression(); + + return expression.And(x => x.CreatorId == UserId); + } + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/PlatformApplicationMappingProfile.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/PlatformApplicationMappingProfile.cs index 73e2a0c5b..94e86c136 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/PlatformApplicationMappingProfile.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application/LINGYUN/Platform/PlatformApplicationMappingProfile.cs @@ -1,25 +1,30 @@ -using AutoMapper; -using LINGYUN.Platform.Datas; -using LINGYUN.Platform.Layouts; -using LINGYUN.Platform.Menus; +using AutoMapper; +using LINGYUN.Platform.Datas; +using LINGYUN.Platform.Feedbacks; +using LINGYUN.Platform.Layouts; +using LINGYUN.Platform.Menus; using LINGYUN.Platform.Packages; - -namespace LINGYUN.Platform; - -public class PlatformApplicationMappingProfile : Profile -{ - public PlatformApplicationMappingProfile() - { - CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - CreateMap() - .ForMember(dto => dto.Meta, map => map.MapFrom(src => src.ExtraProperties)) - .ForMember(dto => dto.Startup, map => map.Ignore()); - CreateMap() - .ForMember(dto => dto.Meta, map => map.MapFrom(src => src.ExtraProperties)); - CreateMap(); - } -} + +namespace LINGYUN.Platform; + +public class PlatformApplicationMappingProfile : Profile +{ + public PlatformApplicationMappingProfile() + { + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dto => dto.Meta, map => map.MapFrom(src => src.ExtraProperties)) + .ForMember(dto => dto.Startup, map => map.Ignore()); + CreateMap() + .ForMember(dto => dto.Meta, map => map.MapFrom(src => src.ExtraProperties)); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Application/System/Linq/Expressions/ExpressionFuncExtensions.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Application/System/Linq/Expressions/ExpressionFuncExtensions.cs new file mode 100644 index 000000000..fd7a017ea --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Application/System/Linq/Expressions/ExpressionFuncExtensions.cs @@ -0,0 +1,32 @@ +using Volo.Abp.Specifications; + +namespace System.Linq.Expressions; + +internal static class ExpressionFuncExtensions +{ + public static Expression> AndIf( + this Expression> first, + bool condition, + Expression> second) + { + if (condition) + { + return ExpressionFuncExtender.And(first, second); + } + + return first; + } + + public static Expression> OrIf( + this Expression> first, + bool condition, + Expression> second) + { + if (condition) + { + return ExpressionFuncExtender.Or(first, second); + } + + return first; + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentConsts.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentConsts.cs new file mode 100644 index 000000000..ef3ae3df2 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentConsts.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Platform.Feedbacks; +public static class FeedbackAttachmentConsts +{ + public static int MaxNameLength { get; set; } = 64; + public static int MaxUrlLength { get; set; } = 255; +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentFile.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentFile.cs new file mode 100644 index 000000000..1cfe25bb4 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentFile.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentFile(string name, long size) +{ + public string Name { get; } = name; + public long Size { get; } = size; +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentTempFile.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentTempFile.cs new file mode 100644 index 000000000..1f85480a5 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackAttachmentTempFile.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentTempFile +{ + public string Path { get; set; } + public string Id { get; set; } + public long Size { get; set; } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackCommentConsts.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackCommentConsts.cs new file mode 100644 index 000000000..6f17e1352 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackCommentConsts.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Platform.Feedbacks; +public static class FeedbackCommentConsts +{ + public static int MaxContentLength { get; set; } = 255; + public static int MaxCapacityLength { get; set; } = 64; +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackConsts.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackConsts.cs new file mode 100644 index 000000000..f2db8fbe0 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackConsts.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Platform.Feedbacks; +public static class FeedbackConsts +{ + public static int MaxCategoryLength { get; set; } = 64; + public static int MaxContentLength { get; set; } = 255; +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackStatus.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackStatus.cs new file mode 100644 index 000000000..3884b2a44 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Feedbacks/FeedbackStatus.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; + +namespace LINGYUN.Platform.Feedbacks; +public enum FeedbackStatus +{ + /// + /// 新增 + /// + [Description("FeedbackStatus:Created")] + Created = 1, + /// + /// 处理中 + /// + [Description("FeedbackStatus:InProgress")] + InProgress = 2, + /// + /// 已关闭 + /// + [Description("FeedbackStatus:Closed")] + Closed = 3, + /// + /// 已解决 + /// + [Description("FeedbackStatus:Resolved")] + Resolved = 4 +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/en.json b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/en.json index 4c1f3a8c2..71961220c 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/en.json +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/en.json @@ -10,6 +10,10 @@ "Platform:03001": "The metadata format does not match!", "Platform:04400": "The user favorites the menu repeatedly!", "Platform:04404": "User Favorites menu not found!", + "Platform:05101": "Unable to comment on issues in {Status} status!", + "Platform:05102": "Cannot add duplicate attachments {Name}!", + "Platform:05103": "User feedback: Attachment named {Name} not found!", + "Platform:05104": "Attachment {Name} is invalid, please upload again!", "UploadFileSizeBeyondLimit": "Upload file size cannot exceed {0} MB!", "NotAllowedFileExtensionName": "Not allowed file extension: {0}!", "DisplayName:VersionFileLimitLength": "File limit size", @@ -75,6 +79,10 @@ "DataItemNotFound": "There is no data dictionary entry named {0}!", "UnableRemoveHasChildNode": "Current data dictionary exists child node, cannot delete!", "DuplicateLayout": "A layout named {0} already exists!", - "Packages": "Packages" + "Packages": "Packages", + "FeedbackStatus:Created": "Created", + "FeedbackStatus:InProgress": "Pending", + "FeedbackStatus:Closed": "Closed", + "FeedbackStatus:Resolved": "Resolved" } } \ No newline at end of file diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/zh-Hans.json b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/zh-Hans.json index f9c91c6f2..93a813331 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/Localization/Resources/zh-Hans.json @@ -10,6 +10,10 @@ "Platform:03001": "元数据格式不匹配!", "Platform:04400": "用户重复收藏菜单!", "Platform:04404": "用户收藏菜单未找到!", + "Platform:05101": "无法对处于{Status}状态的问题进行评论!", + "Platform:05102": "不能添加重复的附件 {Name}!", + "Platform:05103": "用户反馈未找到名为 {Name} 的附件!", + "Platform:05104": "附件 {Name} 已失效, 请重新上传!", "UploadFileSizeBeyondLimit": "上传文件大小不能超过 {0} MB!", "NotAllowedFileExtensionName": "不被允许的文件扩展名: {0}!", "DisplayName:VersionFileLimitLength": "文件限制大小", @@ -75,6 +79,10 @@ "DataItemNotFound": "不存在名为 {0} 的数据字典项!", "UnableRemoveHasChildNode": "当前数据字典存在子节点,无法删除!", "DuplicateLayout": "已经存在名为 {0} 的布局!", - "Packages": "包列表" + "Packages": "包列表", + "FeedbackStatus:Created": "新增", + "FeedbackStatus:InProgress": "处理中", + "FeedbackStatus:Closed": "已关闭", + "FeedbackStatus:Resolved": "已解决" } } \ No newline at end of file diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/PlatformErrorCodes.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/PlatformErrorCodes.cs index d6ce475e1..09bec3a89 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/PlatformErrorCodes.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain.Shared/LINGYUN/Platform/PlatformErrorCodes.cs @@ -1,40 +1,56 @@ -namespace LINGYUN.Platform; - -public static class PlatformErrorCodes -{ - private const string Namespace = "Platform"; - - public const string VersionFileNotFound = Namespace + ":01404"; +namespace LINGYUN.Platform; + +public static class PlatformErrorCodes +{ + private const string Namespace = "Platform"; + + public const string VersionFileNotFound = Namespace + ":01404"; /// /// 包版本不能降级 - /// - public const string PackageVersionDegraded = Namespace + ":01403"; - /// - /// 同级菜单已经存在 - /// - public const string DuplicateMenu = Namespace + ":02001"; - /// - /// 不能删除拥有子菜单的节点 - /// - public const string DeleteMenuHaveChildren = Namespace + ":02002"; - /// - /// 菜单层级已达到最大值 - /// - public const string MenuAchieveMaxDepth = Namespace + ":02003"; - /// - /// 菜单元数据缺少必要的元素 - /// - public const string MenuMissingMetadata = Namespace + ":02101"; - /// - /// 元数据格式不匹配 - /// - public const string MetaFormatMissMatch = Namespace + ":03001"; + /// + public const string PackageVersionDegraded = Namespace + ":01403"; + /// + /// 同级菜单已经存在 + /// + public const string DuplicateMenu = Namespace + ":02001"; + /// + /// 不能删除拥有子菜单的节点 + /// + public const string DeleteMenuHaveChildren = Namespace + ":02002"; + /// + /// 菜单层级已达到最大值 + /// + public const string MenuAchieveMaxDepth = Namespace + ":02003"; + /// + /// 菜单元数据缺少必要的元素 + /// + public const string MenuMissingMetadata = Namespace + ":02101"; + /// + /// 元数据格式不匹配 + /// + public const string MetaFormatMissMatch = Namespace + ":03001"; /// /// 用户重复收藏菜单 - /// - public const string UserDuplicateFavoriteMenu = Namespace + ":04400"; + /// + public const string UserDuplicateFavoriteMenu = Namespace + ":04400"; /// /// 用户收藏菜单未找到 - /// - public const string UserFavoriteMenuNotFound = Namespace + ":04404"; -} + /// + public const string UserFavoriteMenuNotFound = Namespace + ":04404"; + /// + /// 无法对处于{Status}状态的问题进行评论 + /// + public const string UnableFeedbackCommentInStatus = Namespace + ":05101"; + /// + /// 不能添加重复的附件 {Name}! + /// + public const string DuplicateFeedbackAttachment = Namespace + ":05102"; + /// + /// 用户反馈未找到名为 {Name} 的附件! + /// + public const string FeedackAttachmentNotFound = Namespace + ":05103"; + /// + /// 附件 {Name} 已失效, 请重新上传! + /// + public const string FeedackAttachmentLoseEffectiveness = Namespace + ":05104"; +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/Feedback.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/Feedback.cs new file mode 100644 index 000000000..70893b1ae --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/Feedback.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Platform.Feedbacks; +public class Feedback : FullAuditedAggregateRoot, IMultiTenant +{ + public virtual Guid? TenantId { get; protected set; } + public virtual string Content { get; set; } + public virtual string Category { get; protected set; } + public virtual FeedbackStatus Status { get; protected set; } + public virtual ICollection Comments { get; protected set; } + public virtual ICollection Attachments { get; protected set; } + protected Feedback() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + + Comments = new Collection(); + Attachments = new Collection(); + } + + public Feedback( + Guid id, + string category, + string content, + FeedbackStatus status, + Guid? tenantId = null) + : base(id) + { + Category = Check.NotNullOrWhiteSpace(category, nameof(category), FeedbackConsts.MaxCategoryLength); + Content = Check.NotNullOrWhiteSpace(content, nameof(content), FeedbackConsts.MaxContentLength); + Status = status; + TenantId = tenantId; + + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + + Comments = new Collection(); + Attachments = new Collection(); + } + + public FeedbackAttachment AddAttachment( + IGuidGenerator guidGenerator, + string name, + string url, + long size) + { + if (FindAttachment(name) != null) + { + throw new BusinessException(PlatformErrorCodes.DuplicateFeedbackAttachment) + .WithData("Name", name); + } + + var attachment = new FeedbackAttachment( + guidGenerator.Create(), + Id, + name, + url, + size, + TenantId); + + Attachments.Add(attachment); + + return attachment; + } + + public FeedbackAttachment FindAttachment(string name) + { + return Attachments.FirstOrDefault(x => x.Name == name); + } + + public Feedback RemoveAttachment(string name) + { + Attachments.RemoveAll(x => x.Name == name); + + return this; + } + + public FeedbackComment Progress( + IGuidGenerator generator, + string capacity, + string content) + { + ValidateStatus(); + + var comment = new FeedbackComment( + generator.Create(), + Id, + capacity, + content, + TenantId); + + Comments.Add(comment); + + Status = FeedbackStatus.InProgress; + + return comment; + } + + public FeedbackComment Close( + IGuidGenerator generator, + string capacity, + string content) + { + ValidateStatus(); + + var comment = new FeedbackComment( + generator.Create(), + Id, + capacity, + content, + TenantId); + + Comments.Add(comment); + + Status = FeedbackStatus.Closed; + + return comment; + } + + public FeedbackComment Resolve( + IGuidGenerator generator, + string capacity, + string content) + { + ValidateStatus(); + + var comment = new FeedbackComment( + generator.Create(), + Id, + capacity, + content, + TenantId); + + Comments.Add(comment); + + Status = FeedbackStatus.Resolved; + + return comment; + } + + public Feedback ValidateStatus() + { + if (Status == FeedbackStatus.Closed) + { + throw new FeedbackStatusException(PlatformErrorCodes.UnableFeedbackCommentInStatus, FeedbackStatus.Closed); + } + if (Status == FeedbackStatus.Resolved) + { + throw new FeedbackStatusException(PlatformErrorCodes.UnableFeedbackCommentInStatus, FeedbackStatus.Resolved); + } + return this; + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs new file mode 100644 index 000000000..1f0cb7fe1 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs @@ -0,0 +1,34 @@ +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachment : CreationAuditedEntity, IMultiTenant +{ + public virtual Guid? TenantId { get; protected set; } + public virtual string Name { get; protected set; } + public virtual string Url { get; protected set; } + public virtual long Size { get; protected set; } + public virtual Guid FeedbackId { get; protected set; } + protected FeedbackAttachment() + { + + } + + public FeedbackAttachment( + Guid id, + Guid feedbackId, + string name, + string url, + long size, + Guid? tenantId) + :base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), FeedbackAttachmentConsts.MaxNameLength); + Url = Check.NotNullOrWhiteSpace(url, nameof(url), FeedbackAttachmentConsts.MaxUrlLength); + Size = size; + FeedbackId = feedbackId; + TenantId = tenantId; + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentManager.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentManager.cs new file mode 100644 index 000000000..dcdc81c39 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentManager.cs @@ -0,0 +1,114 @@ +using System.IO; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.BlobStoring; +using Volo.Abp.Domain.Services; +using Volo.Abp.IO; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentManager : DomainService +{ + private const string DefaultSaveToApplication = "abp-application"; + private const string DefaultSaveToTempPath = "feedback-upload-tmp"; + + private readonly IBlobContainer _blobContainer; + private readonly IApplicationInfoAccessor _applicationInfoAccessor; + + public FeedbackAttachmentManager( + IBlobContainer blobContainer, + IApplicationInfoAccessor applicationInfoAccessor) + { + _blobContainer = blobContainer; + _applicationInfoAccessor = applicationInfoAccessor; + } + + /// + /// 将文件流写入临时存储 + /// + /// 文件流 + /// 返回临时存储的文件标识 + public async Task SaveToTempAsync(Stream stream) + { + var blobSize = stream.Length; + var blobId = GuidGenerator.Create().ToString("N"); + var timeStampPath = Clock.Now.ToString("yyyy-MM-dd"); + + var tempFilePath = Path.Combine( + Path.GetTempPath(), + _applicationInfoAccessor.ApplicationName ?? DefaultSaveToApplication, + DefaultSaveToTempPath, + timeStampPath); + var tempSavedFile = Path.Combine(tempFilePath, $"{blobId}.png"); + + DirectoryHelper.CreateIfNotExists(tempFilePath); + + using (var fs = new FileStream(tempSavedFile, FileMode.Create, FileAccess.Write)) + { + await stream.CopyToAsync(fs); + } + + return new FeedbackAttachmentTempFile + { + Path = timeStampPath, + Size = blobSize, + Id = blobId, + }; + } + /// + /// 从临时存储附件拷贝到blob存储 + /// + /// 用户反馈实体 + /// 附件临时存储文件 + /// 已保存的附件 + public async Task CopyFromTempAsync(Feedback feedback, string filePtah, string fileId) + { + var fileName = $"{filePtah}/{fileId}.png"; + var tempFilePath = Path.Combine( + Path.GetTempPath(), + _applicationInfoAccessor.ApplicationName ?? DefaultSaveToApplication, + DefaultSaveToTempPath); + var tempFromFile = Path.Combine(tempFilePath, fileName); + var blobFile = $"{fileId}.png"; + var saveToBlobFile = $"{feedback.Id}/{blobFile}"; + + if (!File.Exists(tempFromFile)) + { + throw new BusinessException(PlatformErrorCodes.FeedackAttachmentLoseEffectiveness) + .WithData("Name", fileId); + } + FeedbackAttachmentFile attachmentFile; + + using (var fs = new FileStream(tempFromFile, FileMode.Open, FileAccess.Read)) + { + await _blobContainer.SaveAsync(saveToBlobFile, fs, true); + + attachmentFile = new FeedbackAttachmentFile(blobFile, fs.Length); + } + + FileHelper.DeleteIfExists(tempFromFile); + + return attachmentFile; + } + /// + /// 下载附件 + /// + /// 附件实体 + /// 附件文件流 + public async Task DownloadAsync(FeedbackAttachment attachment) + { + var blobFile = $"{attachment.FeedbackId}/{attachment.Name}"; + + return await _blobContainer.GetAsync(blobFile); + } + /// + /// 删除附件 + /// + /// 附件实体 + /// 附件文件流 + public async Task DeleteAsync(FeedbackAttachment attachment) + { + var blobFile = $"{attachment.FeedbackId}/{attachment.Name}"; + + await _blobContainer.DeleteAsync(blobFile); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentNotFoundException.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentNotFoundException.cs new file mode 100644 index 000000000..d43eedf2f --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachmentNotFoundException.cs @@ -0,0 +1,16 @@ +using Volo.Abp; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackAttachmentNotFoundException : BusinessException +{ + public string Name { get; protected set; } + public FeedbackAttachmentNotFoundException(string name) + : base( + PlatformErrorCodes.FeedackAttachmentNotFound, + $"User feedback: Attachment named {name} not found!") + { + Name = name; + + WithData("Name", name); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs new file mode 100644 index 000000000..05e965c76 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs @@ -0,0 +1,34 @@ +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Platform.Feedbacks; +/// +/// 评论 +/// +public class FeedbackComment : AuditedEntity, IMultiTenant +{ + public virtual Guid? TenantId { get; protected set; } + public virtual string Capacity { get; protected set; } + public virtual string Content { get; set; } + public virtual Guid FeedbackId { get; protected set; } + protected FeedbackComment() + { + + } + + public FeedbackComment( + Guid id, + Guid feedbackId, + string capacity, + string content, + Guid? tenantId = null) + : base(id) + { + Capacity = Check.NotNullOrWhiteSpace(capacity, nameof(capacity), FeedbackCommentConsts.MaxCapacityLength); + Content = Check.NotNullOrWhiteSpace(content, nameof(content), FeedbackCommentConsts.MaxContentLength); + FeedbackId = feedbackId; + TenantId = tenantId; + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackContainer.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackContainer.cs new file mode 100644 index 000000000..b09436ec1 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackContainer.cs @@ -0,0 +1,8 @@ +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Platform.Feedbacks; + +[BlobContainerName("feedbacks")] +public class FeedbackContainer +{ +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackStatusException.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackStatusException.cs new file mode 100644 index 000000000..906a1f702 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackStatusException.cs @@ -0,0 +1,25 @@ +using LINGYUN.Platform.Localization; +using Microsoft.Extensions.Localization; +using Volo.Abp; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.Localization; + +namespace LINGYUN.Platform.Feedbacks; +public class FeedbackStatusException : BusinessException, ILocalizeErrorMessage +{ + public FeedbackStatus Status { get; protected set; } + public FeedbackStatusException(string code, FeedbackStatus status) + : base(code, $"Unable to comment on issues in {status} status") + { + Status = status; + + WithData("Status", status.ToString()); + } + + public string LocalizeMessage(LocalizationContext context) + { + var localizer = context.LocalizerFactory.Create(); + + return localizer[PlatformErrorCodes.UnableFeedbackCommentInStatus, localizer[$"FeedbackStatus:{Status}"]]; + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/IFeedbackRepository.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/IFeedbackRepository.cs new file mode 100644 index 000000000..8646a960f --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/IFeedbackRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Specifications; + +namespace LINGYUN.Platform.Feedbacks; +public interface IFeedbackRepository : IBasicRepository +{ + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(Feedback.CreationTime)} DESC", + int maxResultCount = 25, + int skipCount = 0, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/PlatformDomainModule.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/PlatformDomainModule.cs index c0933cf5d..f3f21746e 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/PlatformDomainModule.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/PlatformDomainModule.cs @@ -1,69 +1,69 @@ -using LINGYUN.Platform.Datas; +using LINGYUN.Platform.Datas; using LINGYUN.Platform.Layouts; using LINGYUN.Platform.Menus; -using LINGYUN.Platform.ObjectExtending; +using LINGYUN.Platform.ObjectExtending; using LINGYUN.Platform.Packages; -using LINGYUN.Platform.Routes; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; -using Volo.Abp.BlobStoring; -using Volo.Abp.Domain.Entities.Events.Distributed; -using Volo.Abp.EventBus; -using Volo.Abp.Modularity; -using Volo.Abp.ObjectExtending.Modularity; - -namespace LINGYUN.Platform; - -[DependsOn( - typeof(PlatformDomainSharedModule), - typeof(AbpBlobStoringModule), - typeof(AbpEventBusModule))] -public class PlatformDomainModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.SetDefaultMapping(); - }); - - Configure(options => - { - options.AddProfile(validate: true); - }); - - Configure(options => - { - options.Containers.Configure(containerConfiguration => - { - containerConfiguration.IsMultiTenant = true; - }); - }); - - Configure(options => - { - options.EtoMappings.Add(typeof(PlatformDomainModule)); - - options.EtoMappings.Add(typeof(PlatformDomainModule)); - options.EtoMappings.Add(typeof(PlatformDomainModule)); +using LINGYUN.Platform.Routes; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AutoMapper; +using Volo.Abp.BlobStoring; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending.Modularity; + +namespace LINGYUN.Platform; + +[DependsOn( + typeof(PlatformDomainSharedModule), + typeof(AbpBlobStoringModule), + typeof(AbpEventBusModule))] +public class PlatformDomainModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAutoMapperObjectMapper(); + + Configure(options => + { + options.SetDefaultMapping(); + }); + + Configure(options => + { + options.AddProfile(validate: true); + }); + + Configure(options => + { + options.Containers.Configure(containerConfiguration => + { + containerConfiguration.IsMultiTenant = true; + }); + }); + + Configure(options => + { + options.EtoMappings.Add(typeof(PlatformDomainModule)); + + options.EtoMappings.Add(typeof(PlatformDomainModule)); + options.EtoMappings.Add(typeof(PlatformDomainModule)); options.EtoMappings.Add(typeof(PlatformDomainModule)); - options.EtoMappings.Add(typeof(PlatformDomainModule)); - }); - } - public override void PostConfigureServices(ServiceConfigurationContext context) - { - ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity( - PlatformModuleExtensionConsts.ModuleName, - PlatformModuleExtensionConsts.EntityNames.Route, - typeof(Route) - ); - ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity( - PlatformModuleExtensionConsts.ModuleName, - PlatformModuleExtensionConsts.EntityNames.Package, - typeof(Package) - ); - } -} + options.EtoMappings.Add(typeof(PlatformDomainModule)); + }); + } + public override void PostConfigureServices(ServiceConfigurationContext context) + { + ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity( + PlatformModuleExtensionConsts.ModuleName, + PlatformModuleExtensionConsts.EntityNames.Route, + typeof(Route) + ); + ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity( + PlatformModuleExtensionConsts.ModuleName, + PlatformModuleExtensionConsts.EntityNames.Package, + typeof(Package) + ); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformDbContextModelBuilderExtensions.cs b/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformDbContextModelBuilderExtensions.cs index 681fbd44a..54dd3419f 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformDbContextModelBuilderExtensions.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformDbContextModelBuilderExtensions.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using LINGYUN.Platform.Datas; +using LINGYUN.Platform.Feedbacks; using LINGYUN.Platform.Layouts; using LINGYUN.Platform.Menus; using LINGYUN.Platform.Packages; @@ -276,6 +277,61 @@ public static class PlatformDbContextModelBuilderExtensions x.ConfigureByConvention(); }); + + builder.Entity(x => + { + x.ToTable(options.TablePrefix + "Feedbacks", options.Schema); + + x.Property(p => p.Category) + .IsRequired() + .HasColumnName(nameof(Feedback.Category)) + .HasMaxLength(FeedbackConsts.MaxCategoryLength); + x.Property(p => p.Content) + .IsRequired() + .HasColumnName(nameof(Feedback.Content)) + .HasMaxLength(FeedbackConsts.MaxContentLength); + + x.HasMany(p => p.Attachments) + .WithOne() + .HasForeignKey(fk => fk.FeedbackId) + .IsRequired(); + x.HasMany(p => p.Comments) + .WithOne() + .HasForeignKey(fk => fk.FeedbackId) + .IsRequired(); + + x.ConfigureByConvention(); + }); + builder.Entity(x => + { + x.ToTable(options.TablePrefix + "FeedbackComments", options.Schema); + + x.Property(p => p.Capacity) + .IsRequired() + .HasColumnName(nameof(FeedbackComment.Capacity)) + .HasMaxLength(FeedbackCommentConsts.MaxCapacityLength); + x.Property(p => p.Content) + .IsRequired() + .HasColumnName(nameof(FeedbackComment.Content)) + .HasMaxLength(FeedbackCommentConsts.MaxContentLength); + + x.ConfigureByConvention(); + }); + builder.Entity(x => + { + x.ToTable(options.TablePrefix + "FeedbackAttachments", options.Schema); + + x.Property(p => p.Name) + .IsRequired() + .HasColumnName(nameof(FeedbackAttachment.Name)) + .HasMaxLength(FeedbackAttachmentConsts.MaxNameLength); + x.Property(p => p.Url) + .IsRequired() + .HasColumnName(nameof(FeedbackAttachment.Url)) + .HasMaxLength(FeedbackAttachmentConsts.MaxUrlLength); + + x.ConfigureByConvention(); + }); } public static EntityTypeBuilder ConfigureRoute( diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs b/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs index 7d0b4269f..2e301ac22 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs @@ -1,4 +1,5 @@ using LINGYUN.Platform.Datas; +using LINGYUN.Platform.Feedbacks; using LINGYUN.Platform.Layouts; using LINGYUN.Platform.Menus; using LINGYUN.Platform.Packages; @@ -27,6 +28,8 @@ public class PlatformEntityFrameworkCoreModule : AbpModule options.AddRepository(); options.AddRepository(); + options.AddRepository(); + options.AddDefaultRepositories(includeAllEntities: true); }); } diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/Feedbacks/EfCoreFeedbackRepository.cs b/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/Feedbacks/EfCoreFeedbackRepository.cs new file mode 100644 index 000000000..d8d328b01 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.EntityFrameworkCore/LINGYUN/Platform/Feedbacks/EfCoreFeedbackRepository.cs @@ -0,0 +1,51 @@ +using LINGYUN.Platform.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Specifications; + +namespace LINGYUN.Platform.Feedbacks; +public class EfCoreFeedbackRepository : EfCoreRepository, IFeedbackRepository +{ + public EfCoreFeedbackRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .CountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string sorting = $"{nameof(Feedback.CreationTime)} DESC", + int maxResultCount = 25, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .OrderBy(sorting.IsNullOrWhiteSpace() ? $"{nameof(Feedback.CreationTime)} DESC" : sorting) + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async override Task> WithDetailsAsync() + { + return (await base.WithDetailsAsync()) + .Include(x => x.Comments) + .Include(x => x.Attachments); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackAttachmentController.cs b/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackAttachmentController.cs new file mode 100644 index 000000000..7438e6c43 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackAttachmentController.cs @@ -0,0 +1,43 @@ +using LINGYUN.Platform.Permissions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Content; + +namespace LINGYUN.Platform.Feedbacks; + +[Area(PlatformRemoteServiceConsts.ModuleName)] +[RemoteService(Name = PlatformRemoteServiceConsts.RemoteServiceName)] +[Route($"api/{PlatformRemoteServiceConsts.ModuleName}/feedbacks")] +[Authorize(PlatformPermissions.Feedback.Default)] +public class FeedbackAttachmentController : AbpControllerBase, IFeedbackAttachmentAppService +{ + private readonly IFeedbackAttachmentAppService _service; + public FeedbackAttachmentController(IFeedbackAttachmentAppService service) + { + _service = service; + } + + [HttpGet] + [Route("{FeedbackId}/attachments/{Name}")] + public virtual Task GetAsync(FeedbackAttachmentGetInput input) + { + return _service.GetAsync(input); + } + + [HttpPost] + [Route("attachments/upload")] + public virtual Task UploadAsync([FromForm] FeedbackAttachmentUploadInput input) + { + return _service.UploadAsync(input); + } + + [HttpDelete] + [Route("{FeedbackId}/attachments/{Name}")] + public virtual Task DeleteAsync(FeedbackAttachmentGetInput input) + { + return _service.DeleteAsync(input); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackController.cs b/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackController.cs new file mode 100644 index 000000000..381a00c15 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/FeedbackController.cs @@ -0,0 +1,51 @@ +using LINGYUN.Platform.Permissions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Platform.Feedbacks; + +[Area(PlatformRemoteServiceConsts.ModuleName)] +[RemoteService(Name = PlatformRemoteServiceConsts.RemoteServiceName)] +[Route($"api/{PlatformRemoteServiceConsts.ModuleName}/feedbacks")] +[Authorize(PlatformPermissions.Feedback.Default)] +public class FeedbackController : AbpControllerBase, IFeedbackAppService +{ + private readonly IFeedbackAppService _service; + public FeedbackController(IFeedbackAppService service) + { + _service = service; + } + + [HttpPost] + [Authorize(PlatformPermissions.Feedback.Create)] + public virtual Task CreateAsync(FeedbackCreateDto input) + { + return _service.CreateAsync(input); + } + + [HttpDelete] + [Route("{id}")] + [Authorize(PlatformPermissions.Feedback.Delete)] + public virtual Task DeleteAsync(Guid id) + { + return _service.DeleteAsync(id); + } + + [HttpGet] + [Route("{id}")] + public virtual Task GetAsync(Guid id) + { + return _service.GetAsync(id); + } + + [HttpGet] + public virtual Task> GetListAsync(FeedbackGetListInput input) + { + return _service.GetListAsync(input); + } +} diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/MyFeedbackController.cs b/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/MyFeedbackController.cs new file mode 100644 index 000000000..72e729832 --- /dev/null +++ b/aspnet-core/modules/platform/LINGYUN.Platform.HttpApi/LINGYUN/Platform/Feedbacks/MyFeedbackController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Platform.Feedbacks; + +[Authorize] +[Area(PlatformRemoteServiceConsts.ModuleName)] +[RemoteService(Name = PlatformRemoteServiceConsts.RemoteServiceName)] +[Route($"api/{PlatformRemoteServiceConsts.ModuleName}/my-feedbacks")] +public class MyFeedbackController : AbpControllerBase, IMyFeedbackAppService +{ + private readonly IMyFeedbackAppService _service; + public MyFeedbackController(IMyFeedbackAppService service) + { + _service = service; + } + + [HttpGet] + public virtual Task> GetMyFeedbacksAsync(FeedbackGetListInput input) + { + return _service.GetMyFeedbacksAsync(input); + } +} From 0a3b3ac112d69022532f4abe5a96f448d26a7765 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 10:13:43 +0800 Subject: [PATCH 08/81] feat(platform): protected attachment and comment entity --- .../LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs | 2 +- .../LINGYUN/Platform/Feedbacks/FeedbackComment.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs index 1f0cb7fe1..2a244bcb1 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackAttachment.cs @@ -16,7 +16,7 @@ public class FeedbackAttachment : CreationAuditedEntity, IMultiTenant } - public FeedbackAttachment( + internal FeedbackAttachment( Guid id, Guid feedbackId, string name, diff --git a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs index 05e965c76..a53e42b72 100644 --- a/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs +++ b/aspnet-core/modules/platform/LINGYUN.Platform.Domain/LINGYUN/Platform/Feedbacks/FeedbackComment.cs @@ -18,7 +18,7 @@ public class FeedbackComment : AuditedEntity, IMultiTenant } - public FeedbackComment( + internal FeedbackComment( Guid id, Guid feedbackId, string capacity, From cf2a27b48e3836d1f6f7c78315699fc136598f28 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 22 Nov 2024 10:17:26 +0800 Subject: [PATCH 09/81] feat: add more changes --- Directory.Packages.props | 2 + .../api/webhooks/subscriptions/model/index.ts | 2 + .../components/SubscriptionModal.vue | 43 ++++++++++------ .../appsettings.Development.json | 50 ++++++++++++------- aspnet-core/services/.gitignore | 3 +- .../CustomIdentityResources.cs | 7 +-- .../appsettings.Development.json | 48 +++++++++++------- 7 files changed, 98 insertions(+), 57 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6375b66d9..98bf9ad30 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,6 +2,7 @@ 8.2.0 2.14.1 + 3.2.3 8.3.0 8.3.0 8.0.0 @@ -95,6 +96,7 @@ + diff --git a/apps/vue/src/api/webhooks/subscriptions/model/index.ts b/apps/vue/src/api/webhooks/subscriptions/model/index.ts index 15ef8d5fb..1ab14ea69 100644 --- a/apps/vue/src/api/webhooks/subscriptions/model/index.ts +++ b/apps/vue/src/api/webhooks/subscriptions/model/index.ts @@ -5,6 +5,7 @@ export interface WebhookSubscription extends CreationAuditedEntityDto, I secret: string; isActive: boolean; webhooks: string[]; + timeoutDuration?: number; headers: Dictionary; } @@ -14,6 +15,7 @@ export interface WebhookSubscriptionCreateOrUpdate { secret: string; isActive: boolean; webhooks: string[]; + timeoutDuration?: number; headers: Dictionary; } diff --git a/apps/vue/src/views/webhooks/subscriptions/components/SubscriptionModal.vue b/apps/vue/src/views/webhooks/subscriptions/components/SubscriptionModal.vue index 853d47095..c7eadf227 100644 --- a/apps/vue/src/views/webhooks/subscriptions/components/SubscriptionModal.vue +++ b/apps/vue/src/views/webhooks/subscriptions/components/SubscriptionModal.vue @@ -7,31 +7,34 @@ :mask-closable="false" @ok="handleSubmit" > -
- + + {{ L('DisplayName:IsActive') }} - + - - + + - - + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/textarea/index.ts b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/textarea/index.ts new file mode 100644 index 000000000..e2d47739b --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/textarea/index.ts @@ -0,0 +1 @@ +export { default as Textarea } from './Textarea.vue'; diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroup.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroup.vue new file mode 100644 index 000000000..66b3562c2 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroup.vue @@ -0,0 +1,48 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroupItem.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroupItem.vue new file mode 100644 index 000000000..abf918f3a --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroupItem.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/index.ts b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/index.ts new file mode 100644 index 000000000..ac3e34bb9 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/index.ts @@ -0,0 +1,2 @@ +export { default as ToggleGroup } from './ToggleGroup.vue'; +export { default as ToggleGroupItem } from './ToggleGroupItem.vue'; diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/Toggle.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/Toggle.vue new file mode 100644 index 000000000..4517b9c61 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/Toggle.vue @@ -0,0 +1,48 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/index.ts b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/index.ts new file mode 100644 index 000000000..ecc1e3f05 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/index.ts @@ -0,0 +1,2 @@ +export * from './toggle'; +export { default as Toggle } from './Toggle.vue'; diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/toggle.ts b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/toggle.ts new file mode 100644 index 000000000..150eaa524 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/toggle.ts @@ -0,0 +1,25 @@ +import { cva, type VariantProps } from 'class-variance-authority'; + +export const toggleVariants = cva( + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground', + { + defaultVariants: { + size: 'default', + variant: 'default', + }, + variants: { + size: { + default: 'h-9 px-3', + lg: 'h-10 px-3', + sm: 'h-8 px-2', + }, + variant: { + default: 'bg-transparent', + outline: + 'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground', + }, + }, + }, +); + +export type ToggleVariants = VariantProps; diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/Tooltip.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/Tooltip.vue new file mode 100644 index 000000000..13cca0419 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/Tooltip.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipContent.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipContent.vue new file mode 100644 index 000000000..81b951e89 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipContent.vue @@ -0,0 +1,52 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipProvider.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipProvider.vue new file mode 100644 index 000000000..b13a457ae --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipProvider.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipTrigger.vue b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipTrigger.vue new file mode 100644 index 000000000..2aa3794f9 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/index.ts b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/index.ts new file mode 100644 index 000000000..94e681c81 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/index.ts @@ -0,0 +1,4 @@ +export { default as Tooltip } from './Tooltip.vue'; +export { default as TooltipContent } from './TooltipContent.vue'; +export { default as TooltipProvider } from './TooltipProvider.vue'; +export { default as TooltipTrigger } from './TooltipTrigger.vue'; diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/tailwind.config.mjs b/apps/vben5/packages/@core/ui-kit/shadcn-ui/tailwind.config.mjs new file mode 100644 index 000000000..f17f556fa --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/apps/vben5/packages/@core/ui-kit/shadcn-ui/tsconfig.json b/apps/vben5/packages/@core/ui-kit/shadcn-ui/tsconfig.json new file mode 100644 index 000000000..0a46bc578 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/shadcn-ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@vben-core/shadcn-ui/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/build.config.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/build.config.ts new file mode 100644 index 000000000..18eaa604c --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/build.config.ts @@ -0,0 +1,21 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: [ + { + builder: 'mkdist', + input: './src', + loaders: ['vue'], + pattern: ['**/*.vue'], + }, + { + builder: 'mkdist', + format: 'esm', + input: './src', + loaders: ['js'], + pattern: ['**/*.ts'], + }, + ], +}); diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/package.json b/apps/vben5/packages/@core/ui-kit/tabs-ui/package.json new file mode 100644 index 000000000..41fb23748 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/package.json @@ -0,0 +1,47 @@ +{ + "name": "@vben-core/tabs-ui", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/uikit/tabs-ui" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist" + ], + "sideEffects": [ + "**/*.css" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben-core/icons": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/typings": "workspace:*", + "@vueuse/core": "catalog:", + "vue": "catalog:" + } +} diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/postcss.config.mjs b/apps/vben5/packages/@core/ui-kit/tabs-ui/postcss.config.mjs new file mode 100644 index 000000000..3d8070455 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/index.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/index.ts new file mode 100644 index 000000000..3c28c946c --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as Tabs } from './tabs/tabs.vue'; +export { default as TabsChrome } from './tabs-chrome/tabs.vue'; diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue new file mode 100644 index 000000000..7ab875a78 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue new file mode 100644 index 000000000..ba77aabe6 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue @@ -0,0 +1,133 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/index.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/index.ts new file mode 100644 index 000000000..a26899ef5 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/index.ts @@ -0,0 +1,2 @@ +export { default as TabsToolMore } from './tool-more.vue'; +export { default as TabsToolScreen } from './tool-screen.vue'; diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-more.vue b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-more.vue new file mode 100644 index 000000000..0cbfc73ae --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-more.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-screen.vue b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-screen.vue new file mode 100644 index 000000000..018371901 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-screen.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/index.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/index.ts new file mode 100644 index 000000000..6d562db88 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/index.ts @@ -0,0 +1,3 @@ +export * from './components/widgets'; +export { default as TabsView } from './tabs-view.vue'; +export type { IContextMenuItem } from '@vben-core/shadcn-ui'; diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/tabs-view.vue b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/tabs-view.vue new file mode 100644 index 000000000..7656e489c --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/tabs-view.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/types.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/types.ts new file mode 100644 index 000000000..ce0ac474b --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/types.ts @@ -0,0 +1,64 @@ +import type { IContextMenuItem } from '@vben-core/shadcn-ui'; +import type { TabDefinition, TabsStyleType } from '@vben-core/typings'; + +export type TabsEmits = { + close: [string]; + sortTabs: [number, number]; + unpin: [TabDefinition]; +}; + +export interface TabsProps { + active?: string; + /** + * @zh_CN content class + * @default tabs-chrome + */ + contentClass?: string; + /** + * @zh_CN 右键菜单 + */ + contextMenus?: (data: any) => IContextMenuItem[]; + /** + * @zh_CN 是否可以拖拽 + */ + draggable?: boolean; + /** + * @zh_CN 间隙 + * @default 7 + * 仅限 tabs-chrome + */ + gap?: number; + /** + * @zh_CN tab 最大宽度 + * 仅限 tabs-chrome + */ + maxWidth?: number; + + /** + * @zh_CN tab最小宽度 + * 仅限 tabs-chrome + */ + minWidth?: number; + + /** + * @zh_CN 是否显示图标 + */ + showIcon?: boolean; + /** + * @zh_CN 标签页风格 + */ + styleType?: TabsStyleType; + + /** + * @zh_CN 选项卡数据 + */ + tabs?: TabDefinition[]; +} + +export interface TabConfig extends TabDefinition { + affixTab: boolean; + closable: boolean; + icon: string; + key: string; + title: string; +} diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts new file mode 100644 index 000000000..db4b7ce84 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts @@ -0,0 +1,127 @@ +import type { EmitType } from '@vben-core/typings'; + +import type { TabsProps } from './types'; + +import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; + +import { + type Sortable, + useIsMobile, + useSortable, +} from '@vben-core/composables'; + +// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素 +function findParentElement(element: HTMLElement) { + const parentCls = 'group'; + return element.classList.contains(parentCls) + ? element + : element.closest(`.${parentCls}`); +} + +export function useTabsDrag(props: TabsProps, emit: EmitType) { + const sortableInstance = ref(null); + + async function initTabsSortable() { + await nextTick(); + + const el = document.querySelectorAll( + `.${props.contentClass}`, + )?.[0] as HTMLElement; + + if (!el) { + console.warn('Element not found for sortable initialization'); + return; + } + + const resetElState = async () => { + el.style.cursor = 'default'; + // el.classList.remove('dragging'); + el.querySelector('.draggable')?.classList.remove('dragging'); + }; + + const { initializeSortable } = useSortable(el, { + filter: (_evt, target: HTMLElement) => { + const parent = findParentElement(target); + const draggable = parent?.classList.contains('draggable'); + return !draggable || !props.draggable; + }, + onEnd(evt) { + const { newIndex, oldIndex } = evt; + // const fromElement = evt.item; + const { srcElement } = (evt as any).originalEvent; + + if (!srcElement) { + resetElState(); + return; + } + + const srcParent = findParentElement(srcElement); + + if (!srcParent) { + resetElState(); + return; + } + + if (!srcParent.classList.contains('draggable')) { + resetElState(); + + return; + } + + if ( + oldIndex !== undefined && + newIndex !== undefined && + !Number.isNaN(oldIndex) && + !Number.isNaN(newIndex) && + oldIndex !== newIndex + ) { + emit('sortTabs', oldIndex, newIndex); + } + resetElState(); + }, + onMove(evt) { + const parent = findParentElement(evt.related); + if (parent?.classList.contains('draggable') && props.draggable) { + const isCurrentAffix = evt.dragged.classList.contains('affix-tab'); + const isRelatedAffix = evt.related.classList.contains('affix-tab'); + // 不允许在固定的tab和非固定的tab之间互相拖拽 + return isCurrentAffix === isRelatedAffix; + } else { + return false; + } + }, + onStart: () => { + el.style.cursor = 'grabbing'; + el.querySelector('.draggable')?.classList.add('dragging'); + // el.classList.add('dragging'); + }, + }); + + sortableInstance.value = await initializeSortable(); + } + + async function init() { + const { isMobile } = useIsMobile(); + + // 移动端下tab不需要拖拽 + if (isMobile.value) { + return; + } + await nextTick(); + initTabsSortable(); + } + + onMounted(init); + + watch( + () => props.styleType, + () => { + sortableInstance.value?.destroy(); + init(); + }, + ); + + onUnmounted(() => { + sortableInstance.value?.destroy(); + }); +} diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts new file mode 100644 index 000000000..a56b38217 --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts @@ -0,0 +1,194 @@ +import type { TabsProps } from './types'; + +import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; + +import { VbenScrollbar } from '@vben-core/shadcn-ui'; + +import { useDebounceFn } from '@vueuse/core'; + +type DomElement = Element | null | undefined; + +export function useTabsViewScroll(props: TabsProps) { + let resizeObserver: null | ResizeObserver = null; + let mutationObserver: MutationObserver | null = null; + let tabItemCount = 0; + const scrollbarRef = ref | null>(null); + const scrollViewportEl = ref(null); + const showScrollButton = ref(false); + const scrollIsAtLeft = ref(true); + const scrollIsAtRight = ref(false); + + function getScrollClientWidth() { + const scrollbarEl = scrollbarRef.value?.$el; + if (!scrollbarEl || !scrollViewportEl.value) return {}; + + const scrollbarWidth = scrollbarEl.clientWidth; + const scrollViewWidth = scrollViewportEl.value.clientWidth; + + return { + scrollbarWidth, + scrollViewWidth, + }; + } + + function scrollDirection( + direction: 'left' | 'right', + distance: number = 150, + ) { + const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth(); + + if (!scrollbarWidth || !scrollViewWidth) return; + + if (scrollbarWidth > scrollViewWidth) return; + + scrollViewportEl.value?.scrollBy({ + behavior: 'smooth', + left: + direction === 'left' + ? -(scrollbarWidth - distance) + : +(scrollbarWidth - distance), + }); + } + + async function initScrollbar() { + await nextTick(); + + const scrollbarEl = scrollbarRef.value?.$el; + if (!scrollbarEl) { + return; + } + + const viewportEl = scrollbarEl?.querySelector( + 'div[data-radix-scroll-area-viewport]', + ); + + scrollViewportEl.value = viewportEl; + calcShowScrollbarButton(); + + await nextTick(); + scrollToActiveIntoView(); + + // 监听大小变化 + resizeObserver?.disconnect(); + resizeObserver = new ResizeObserver( + useDebounceFn((_entries: ResizeObserverEntry[]) => { + calcShowScrollbarButton(); + scrollToActiveIntoView(); + }, 100), + ); + resizeObserver.observe(viewportEl); + + tabItemCount = props.tabs?.length || 0; + mutationObserver?.disconnect(); + // 使用 MutationObserver 仅监听子节点数量变化 + mutationObserver = new MutationObserver(() => { + const count = viewportEl.querySelectorAll( + `div[data-tab-item="true"]`, + ).length; + + if (count > tabItemCount) { + scrollToActiveIntoView(); + } + + if (count !== tabItemCount) { + calcShowScrollbarButton(); + tabItemCount = count; + } + }); + + // 配置为仅监听子节点的添加和移除 + mutationObserver.observe(viewportEl, { + attributes: false, + childList: true, + subtree: true, + }); + } + + async function scrollToActiveIntoView() { + if (!scrollViewportEl.value) { + return; + } + await nextTick(); + const viewportEl = scrollViewportEl.value; + const { scrollbarWidth } = getScrollClientWidth(); + const { scrollWidth } = viewportEl; + + if (scrollbarWidth >= scrollWidth) { + return; + } + + requestAnimationFrame(() => { + const activeItem = viewportEl?.querySelector('.is-active'); + activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' }); + }); + } + + /** + * 计算tabs 宽度,用于判断是否显示左右滚动按钮 + */ + async function calcShowScrollbarButton() { + if (!scrollViewportEl.value) { + return; + } + + const { scrollbarWidth } = getScrollClientWidth(); + + showScrollButton.value = + scrollViewportEl.value.scrollWidth > scrollbarWidth; + } + + const handleScrollAt = useDebounceFn(({ left, right }) => { + scrollIsAtLeft.value = left; + scrollIsAtRight.value = right; + }, 100); + + watch( + () => props.active, + async () => { + // 200为了等待 tab 切换动画完成 + // setTimeout(() => { + scrollToActiveIntoView(); + // }, 300); + }, + { + flush: 'post', + }, + ); + + // watch( + // () => props.tabs?.length, + // async () => { + // await nextTick(); + // calcShowScrollbarButton(); + // }, + // { + // flush: 'post', + // }, + // ); + + watch( + () => props.styleType, + () => { + initScrollbar(); + }, + ); + + onMounted(initScrollbar); + + onUnmounted(() => { + resizeObserver?.disconnect(); + mutationObserver?.disconnect(); + resizeObserver = null; + mutationObserver = null; + }); + + return { + handleScrollAt, + initScrollbar, + scrollbarRef, + scrollDirection, + scrollIsAtLeft, + scrollIsAtRight, + showScrollButton, + }; +} diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/tailwind.config.mjs b/apps/vben5/packages/@core/ui-kit/tabs-ui/tailwind.config.mjs new file mode 100644 index 000000000..f17f556fa --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/apps/vben5/packages/@core/ui-kit/tabs-ui/tsconfig.json b/apps/vben5/packages/@core/ui-kit/tabs-ui/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/@core/ui-kit/tabs-ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/constants/README.md b/apps/vben5/packages/constants/README.md new file mode 100644 index 000000000..1fae490bf --- /dev/null +++ b/apps/vben5/packages/constants/README.md @@ -0,0 +1,19 @@ +# @vben/constants + +用于多个 `app` 公用的常量,继承了 `@vben-core/shared/constants` 的所有能力。业务上有通用常量可以放在这里。 + +## 用法 + +### 添加依赖 + +```bash +# 进入目标应用目录,例如 apps/xxxx-app +# cd apps/xxxx-app +pnpm add @vben/constants +``` + +### 使用 + +```ts +import { DEFAULT_HOME_PATH } from '@vben/constants'; +``` diff --git a/apps/vben5/packages/constants/package.json b/apps/vben5/packages/constants/package.json new file mode 100644 index 000000000..9ecc03f25 --- /dev/null +++ b/apps/vben5/packages/constants/package.json @@ -0,0 +1,25 @@ +{ + "name": "@vben/constants", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/constants" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/shared": "workspace:*" + } +} diff --git a/apps/vben5/packages/constants/src/core.ts b/apps/vben5/packages/constants/src/core.ts new file mode 100644 index 000000000..24db39dad --- /dev/null +++ b/apps/vben5/packages/constants/src/core.ts @@ -0,0 +1,28 @@ +/** + * @zh_CN 登录页面 url 地址 + */ +export const LOGIN_PATH = '/auth/login'; + +/** + * @zh_CN 默认首页地址 + */ +export const DEFAULT_HOME_PATH = '/analytics'; + +export interface LanguageOption { + label: string; + value: 'en-US' | 'zh-CN'; +} + +/** + * Supported languages + */ +export const SUPPORT_LANGUAGES: LanguageOption[] = [ + { + label: '简体中文', + value: 'zh-CN', + }, + { + label: 'English', + value: 'en-US', + }, +]; diff --git a/apps/vben5/packages/constants/src/index.ts b/apps/vben5/packages/constants/src/index.ts new file mode 100644 index 000000000..6502c6db9 --- /dev/null +++ b/apps/vben5/packages/constants/src/index.ts @@ -0,0 +1,2 @@ +export * from './core'; +export * from '@vben-core/shared/constants'; diff --git a/apps/vben5/packages/constants/tsconfig.json b/apps/vben5/packages/constants/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/constants/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/README.md b/apps/vben5/packages/effects/README.md new file mode 100644 index 000000000..4965b5125 --- /dev/null +++ b/apps/vben5/packages/effects/README.md @@ -0,0 +1,10 @@ +## Effects 目录 + +`effects` 目录专门用于存放与轻微耦合相关的代码和逻辑。如果你的包具有以下特点,建议将其放置在 `effects` 目录下: + +- **状态管理**:使用状态管理框架 `pinia`,并包含处理副作用(如异步操作、API 调用)的部分。 +- **用户偏好设置**:使用 `@vben-core/preferences` 处理用户偏好设置,涉及本地存储或浏览器缓存逻辑(如使用 `localStorage`)。 +- **导航和路由**:处理导航、页面跳转等场景,需要管理路由变化的逻辑。 +- **组件库依赖**:包含与特定组件库紧密耦合或依赖大型仓库的部分。 + +通过将相关代码归类到 `effects` 目录,可以使项目结构更加清晰,便于维护和扩展。 diff --git a/apps/vben5/packages/effects/access/package.json b/apps/vben5/packages/effects/access/package.json new file mode 100644 index 000000000..e5bdabd10 --- /dev/null +++ b/apps/vben5/packages/effects/access/package.json @@ -0,0 +1,29 @@ +{ + "name": "@vben/access", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/effects/permissions" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben/preferences": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/types": "workspace:*", + "@vben/utils": "workspace:*", + "vue": "catalog:" + } +} diff --git a/apps/vben5/packages/effects/access/src/access-control.vue b/apps/vben5/packages/effects/access/src/access-control.vue new file mode 100644 index 000000000..219608e55 --- /dev/null +++ b/apps/vben5/packages/effects/access/src/access-control.vue @@ -0,0 +1,47 @@ + + + + diff --git a/apps/vben5/packages/effects/access/src/accessible.ts b/apps/vben5/packages/effects/access/src/accessible.ts new file mode 100644 index 000000000..c951d92a5 --- /dev/null +++ b/apps/vben5/packages/effects/access/src/accessible.ts @@ -0,0 +1,86 @@ +import type { + AccessModeType, + GenerateMenuAndRoutesOptions, + RouteRecordRaw, +} from '@vben/types'; + +import { + cloneDeep, + generateMenus, + generateRoutesByBackend, + generateRoutesByFrontend, + mapTree, +} from '@vben/utils'; + +async function generateAccessible( + mode: AccessModeType, + options: GenerateMenuAndRoutesOptions, +) { + const { router } = options; + + options.routes = cloneDeep(options.routes); + // 生成路由 + const accessibleRoutes = await generateRoutes(mode, options); + + // 动态添加到router实例内 + accessibleRoutes.forEach((route) => { + router.addRoute(route); + }); + + // 生成菜单 + const accessibleMenus = await generateMenus(accessibleRoutes, options.router); + + return { accessibleMenus, accessibleRoutes }; +} + +/** + * Generate routes + * @param mode + * @param options + */ +async function generateRoutes( + mode: AccessModeType, + options: GenerateMenuAndRoutesOptions, +) { + const { forbiddenComponent, roles, routes } = options; + + let resultRoutes: RouteRecordRaw[] = routes; + switch (mode) { + case 'backend': { + resultRoutes = await generateRoutesByBackend(options); + break; + } + case 'frontend': { + resultRoutes = await generateRoutesByFrontend( + routes, + roles || [], + forbiddenComponent, + ); + break; + } + } + + /** + * 调整路由树,做以下处理: + * 1. 对未添加redirect的路由添加redirect + */ + resultRoutes = mapTree(resultRoutes, (route) => { + // 如果有redirect或者没有子路由,则直接返回 + if (route.redirect || !route.children || route.children.length === 0) { + return route; + } + const firstChild = route.children[0]; + + // 如果子路由不是以/开头,则直接返回,这种情况需要计算全部父级的path才能得出正确的path,这里不做处理 + if (!firstChild?.path || !firstChild.path.startsWith('/')) { + return route; + } + + route.redirect = firstChild.path; + return route; + }); + + return resultRoutes; +} + +export { generateAccessible }; diff --git a/apps/vben5/packages/effects/access/src/directive.ts b/apps/vben5/packages/effects/access/src/directive.ts new file mode 100644 index 000000000..35d9d5170 --- /dev/null +++ b/apps/vben5/packages/effects/access/src/directive.ts @@ -0,0 +1,42 @@ +/** + * Global authority directive + * Used for fine-grained control of component permissions + * @Example v-access:role="[ROLE_NAME]" or v-access:role="ROLE_NAME" + * @Example v-access:code="[ROLE_CODE]" or v-access:code="ROLE_CODE" + */ +import type { App, Directive, DirectiveBinding } from 'vue'; + +import { useAccess } from './use-access'; + +function isAccessible( + el: Element, + binding: DirectiveBinding, +) { + const { accessMode, hasAccessByCodes, hasAccessByRoles } = useAccess(); + + const value = binding.value; + + if (!value) return; + const authMethod = + accessMode.value === 'frontend' && binding.arg === 'role' + ? hasAccessByRoles + : hasAccessByCodes; + + const values = Array.isArray(value) ? value : [value]; + + if (!authMethod(values)) { + el?.remove(); + } +} + +const mounted = (el: Element, binding: DirectiveBinding) => { + isAccessible(el, binding); +}; + +const authDirective: Directive = { + mounted, +}; + +export function registerAccessDirective(app: App) { + app.directive('access', authDirective); +} diff --git a/apps/vben5/packages/effects/access/src/index.ts b/apps/vben5/packages/effects/access/src/index.ts new file mode 100644 index 000000000..392aa53bb --- /dev/null +++ b/apps/vben5/packages/effects/access/src/index.ts @@ -0,0 +1,4 @@ +export { default as AccessControl } from './access-control.vue'; +export * from './accessible'; +export * from './directive'; +export * from './use-access'; diff --git a/apps/vben5/packages/effects/access/src/use-access.ts b/apps/vben5/packages/effects/access/src/use-access.ts new file mode 100644 index 000000000..939cdbedf --- /dev/null +++ b/apps/vben5/packages/effects/access/src/use-access.ts @@ -0,0 +1,53 @@ +import { computed } from 'vue'; + +import { preferences, updatePreferences } from '@vben/preferences'; +import { useAccessStore, useUserStore } from '@vben/stores'; + +function useAccess() { + const accessStore = useAccessStore(); + const userStore = useUserStore(); + const accessMode = computed(() => { + return preferences.app.accessMode; + }); + + /** + * 基于角色判断是否有权限 + * @description: Determine whether there is permission,The role is judged by the user's role + * @param roles + */ + function hasAccessByRoles(roles: string[]) { + const userRoleSet = new Set(userStore.userRoles); + const intersection = roles.filter((item) => userRoleSet.has(item)); + return intersection.length > 0; + } + + /** + * 基于权限码判断是否有权限 + * @description: Determine whether there is permission,The permission code is judged by the user's permission code + * @param codes + */ + function hasAccessByCodes(codes: string[]) { + const userCodesSet = new Set(accessStore.accessCodes); + + const intersection = codes.filter((item) => userCodesSet.has(item)); + return intersection.length > 0; + } + + async function toggleAccessMode() { + updatePreferences({ + app: { + accessMode: + preferences.app.accessMode === 'frontend' ? 'backend' : 'frontend', + }, + }); + } + + return { + accessMode, + hasAccessByCodes, + hasAccessByRoles, + toggleAccessMode, + }; +} + +export { useAccess }; diff --git a/apps/vben5/packages/effects/access/tsconfig.json b/apps/vben5/packages/effects/access/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/effects/access/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/common-ui/package.json b/apps/vben5/packages/effects/common-ui/package.json new file mode 100644 index 000000000..2ff5fdf3b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vben/common-ui", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/effects/common-ui" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/form-ui": "workspace:*", + "@vben-core/popup-ui": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben/constants": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/icons": "workspace:*", + "@vben/locales": "workspace:*", + "@vben/types": "workspace:*", + "@vueuse/core": "catalog:", + "@vueuse/integrations": "catalog:", + "qrcode": "catalog:", + "vue": "catalog:", + "vue-router": "catalog:" + }, + "devDependencies": { + "@types/qrcode": "catalog:" + } +} diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/hooks/useCaptchaPoints.ts b/apps/vben5/packages/effects/common-ui/src/components/captcha/hooks/useCaptchaPoints.ts new file mode 100644 index 000000000..511fb3b75 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/hooks/useCaptchaPoints.ts @@ -0,0 +1,19 @@ +import type { CaptchaPoint } from '../types'; + +import { reactive } from 'vue'; + +export function useCaptchaPoints() { + const points = reactive([]); + function addPoint(point: CaptchaPoint) { + points.push(point); + } + + function clearPoints() { + points.splice(0, points.length); + } + return { + addPoint, + clearPoints, + points, + }; +} diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/index.ts b/apps/vben5/packages/effects/common-ui/src/components/captcha/index.ts new file mode 100644 index 000000000..6ad68c496 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/index.ts @@ -0,0 +1,6 @@ +export { default as PointSelectionCaptcha } from './point-selection-captcha/index.vue'; +export { default as PointSelectionCaptchaCard } from './point-selection-captcha/index.vue'; + +export { default as SliderCaptcha } from './slider-captcha/index.vue'; +export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue'; +export type * from './types'; diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/point-selection-captcha/index.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/point-selection-captcha/index.vue new file mode 100644 index 000000000..d4b136194 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/point-selection-captcha/index.vue @@ -0,0 +1,175 @@ + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/point-selection-captcha/point-selection-captcha-card.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/point-selection-captcha/point-selection-captcha-card.vue new file mode 100644 index 000000000..0ece2bf04 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/point-selection-captcha/point-selection-captcha-card.vue @@ -0,0 +1,83 @@ + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/index.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/index.vue new file mode 100644 index 000000000..fa43493e2 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/index.vue @@ -0,0 +1,243 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-action.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-action.vue new file mode 100644 index 000000000..fe0b5aaa8 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-action.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-bar.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-bar.vue new file mode 100644 index 000000000..eacf6842d --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-bar.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-content.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-content.vue new file mode 100644 index 000000000..5c481f2e6 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-content.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-rotate-captcha/index.vue b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-rotate-captcha/index.vue new file mode 100644 index 000000000..88945892b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/slider-rotate-captcha/index.vue @@ -0,0 +1,213 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/captcha/types.ts b/apps/vben5/packages/effects/common-ui/src/components/captcha/types.ts new file mode 100644 index 000000000..ba3adc7d9 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/captcha/types.ts @@ -0,0 +1,175 @@ +import type { ClassType } from '@vben/types'; + +import type { CSSProperties } from 'vue'; + +export interface CaptchaData { + /** + * x + */ + x: number; + /** + * y + */ + y: number; + /** + * 时间戳 + */ + t: number; +} +export interface CaptchaPoint extends CaptchaData { + /** + * 数据索引 + */ + i: number; +} +export interface PointSelectionCaptchaCardProps { + /** + * 验证码图片 + */ + captchaImage: string; + /** + * 验证码图片高度 + * @default '220px' + */ + height?: number | string; + /** + * 水平内边距 + * @default '12px' + */ + paddingX?: number | string; + /** + * 垂直内边距 + * @default '16px' + */ + paddingY?: number | string; + /** + * 标题 + * @default '请按图依次点击' + */ + title?: string; + /** + * 验证码图片宽度 + * @default '300px' + */ + width?: number | string; +} + +export interface PointSelectionCaptchaProps + extends PointSelectionCaptchaCardProps { + /** + * 是否展示确定按钮 + * @default false + */ + showConfirm?: boolean; + /** + * 提示图片 + * @default '' + */ + hintImage?: string; + /** + * 提示文本 + * @default '' + */ + hintText?: string; +} + +export interface SliderCaptchaProps { + class?: ClassType; + /** + * @description 滑块的样式 + * @default {} + */ + actionStyle?: CSSProperties; + + /** + * @description 滑块条的样式 + * @default {} + */ + barStyle?: CSSProperties; + + /** + * @description 内容的样式 + * @default {} + */ + contentStyle?: CSSProperties; + + /** + * @description 组件的样式 + * @default {} + */ + wrapperStyle?: CSSProperties; + + /** + * @description 是否作为插槽使用,用于联动组件,可参考旋转校验组件 + * @default false + */ + isSlot?: boolean; + + /** + * @description 验证成功的提示 + * @default '验证通过' + */ + successText?: string; + + /** + * @description 提示文字 + * @default '请按住滑块拖动' + */ + text?: string; +} + +export interface SliderRotateCaptchaProps { + /** + * @description 旋转的角度 + * @default 20 + */ + diffDegree?: number; + + /** + * @description 图片的宽度 + * @default 260 + */ + imageSize?: number; + + /** + * @description 图片的样式 + * @default {} + */ + imageWrapperStyle?: CSSProperties; + + /** + * @description 最大旋转角度 + * @default 270 + */ + maxDegree?: number; + + /** + * @description 最小旋转角度 + * @default 90 + */ + minDegree?: number; + + /** + * @description 图片的地址 + */ + src?: string; + /** + * @description 默认提示文本 + */ + defaultTip?: string; +} + +export interface CaptchaVerifyPassingData { + isPassing: boolean; + time: number | string; +} + +export interface SliderCaptchaActionType { + resume: () => void; +} + +export interface SliderRotateVerifyPassingData { + event: MouseEvent | TouchEvent; + moveDistance: number; + moveX: number; +} diff --git a/apps/vben5/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue b/apps/vben5/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue new file mode 100644 index 000000000..bd4f1fd38 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue @@ -0,0 +1,142 @@ + + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/ellipsis-text/index.ts b/apps/vben5/packages/effects/common-ui/src/components/ellipsis-text/index.ts new file mode 100644 index 000000000..67a236c98 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/ellipsis-text/index.ts @@ -0,0 +1 @@ +export { default as EllipsisText } from './ellipsis-text.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue b/apps/vben5/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue new file mode 100644 index 000000000..131eb8ea2 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue @@ -0,0 +1,167 @@ + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/icon-picker/index.ts b/apps/vben5/packages/effects/common-ui/src/components/icon-picker/index.ts new file mode 100644 index 000000000..3dabc86a9 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/icon-picker/index.ts @@ -0,0 +1 @@ +export { default as IconPicker } from './icon-picker.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/components/index.ts b/apps/vben5/packages/effects/common-ui/src/components/index.ts new file mode 100644 index 000000000..b7ae8548b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/index.ts @@ -0,0 +1,19 @@ +export * from './captcha'; +export * from './ellipsis-text'; +export * from './icon-picker'; +export * from './page'; +export * from './resize'; +export * from '@vben-core/form-ui'; +export * from '@vben-core/popup-ui'; + +// 给文档用 +export { + VbenButton, + VbenCountToAnimator, + VbenInputPassword, + VbenLoading, + VbenPinInput, + VbenSpinner, +} from '@vben-core/shadcn-ui'; + +export { globalShareState } from '@vben-core/shared/global-state'; diff --git a/apps/vben5/packages/effects/common-ui/src/components/page/__tests__/page.test.ts b/apps/vben5/packages/effects/common-ui/src/components/page/__tests__/page.test.ts new file mode 100644 index 000000000..776fe6af4 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/page/__tests__/page.test.ts @@ -0,0 +1,88 @@ +import { mount } from '@vue/test-utils'; +import { describe, expect, it } from 'vitest'; + +import { Page } from '..'; + +describe('page.vue', () => { + it('renders title when passed', () => { + const wrapper = mount(Page, { + props: { + title: 'Test Title', + }, + }); + + expect(wrapper.text()).toContain('Test Title'); + }); + + it('renders description when passed', () => { + const wrapper = mount(Page, { + props: { + description: 'Test Description', + }, + }); + + expect(wrapper.text()).toContain('Test Description'); + }); + + it('renders default slot content', () => { + const wrapper = mount(Page, { + slots: { + default: '

Default Slot Content

', + }, + }); + + expect(wrapper.html()).toContain('

Default Slot Content

'); + }); + + it('renders footer slot when showFooter is true', () => { + const wrapper = mount(Page, { + props: { + showFooter: true, + }, + slots: { + footer: '

Footer Slot Content

', + }, + }); + + expect(wrapper.html()).toContain('

Footer Slot Content

'); + }); + + it('applies the custom contentClass', () => { + const wrapper = mount(Page, { + props: { + contentClass: 'custom-class', + }, + }); + + const contentDiv = wrapper.find('.p-4'); + expect(contentDiv.classes()).toContain('custom-class'); + }); + + it('does not render title slot if title prop is provided', () => { + const wrapper = mount(Page, { + props: { + title: 'Test Title', + }, + slots: { + title: '

Title Slot Content

', + }, + }); + + expect(wrapper.text()).toContain('Title Slot Content'); + expect(wrapper.html()).not.toContain('Test Title'); + }); + + it('does not render description slot if description prop is provided', () => { + const wrapper = mount(Page, { + props: { + description: 'Test Description', + }, + slots: { + description: '

Description Slot Content

', + }, + }); + + expect(wrapper.text()).toContain('Description Slot Content'); + expect(wrapper.html()).not.toContain('Test Description'); + }); +}); diff --git a/apps/vben5/packages/effects/common-ui/src/components/page/index.ts b/apps/vben5/packages/effects/common-ui/src/components/page/index.ts new file mode 100644 index 000000000..65bf3c697 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/page/index.ts @@ -0,0 +1 @@ +export { default as Page } from './page.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/components/page/page.vue b/apps/vben5/packages/effects/common-ui/src/components/page/page.vue new file mode 100644 index 000000000..e8c486ebe --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/page/page.vue @@ -0,0 +1,103 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/components/resize/index.ts b/apps/vben5/packages/effects/common-ui/src/components/resize/index.ts new file mode 100644 index 000000000..f9375221d --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/resize/index.ts @@ -0,0 +1 @@ +export { default as VResize } from './resize.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/components/resize/resize.vue b/apps/vben5/packages/effects/common-ui/src/components/resize/resize.vue new file mode 100644 index 000000000..aaf89eaf2 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/components/resize/resize.vue @@ -0,0 +1,1122 @@ + + + + + diff --git a/apps/vben5/packages/effects/common-ui/src/index.ts b/apps/vben5/packages/effects/common-ui/src/index.ts new file mode 100644 index 000000000..27bd7186e --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/index.ts @@ -0,0 +1,2 @@ +export * from './components'; +export * from './ui'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/about/about.ts b/apps/vben5/packages/effects/common-ui/src/ui/about/about.ts new file mode 100644 index 000000000..c4cf95797 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/about/about.ts @@ -0,0 +1,14 @@ +import type { Component } from 'vue'; + +interface AboutProps { + description?: string; + name?: string; + title?: string; +} + +interface DescriptionItem { + content: Component | string; + title: string; +} + +export type { AboutProps, DescriptionItem }; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/about/about.vue b/apps/vben5/packages/effects/common-ui/src/ui/about/about.vue new file mode 100644 index 000000000..9395d704b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/about/about.vue @@ -0,0 +1,182 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/about/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/about/index.ts new file mode 100644 index 000000000..5860f5b92 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/about/index.ts @@ -0,0 +1 @@ +export { default as About } from './about.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/auth-title.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/auth-title.vue new file mode 100644 index 000000000..3ed9a6bef --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/auth-title.vue @@ -0,0 +1,13 @@ + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/code-login.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/code-login.vue new file mode 100644 index 000000000..7de5dd3b4 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/code-login.vue @@ -0,0 +1,118 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/forget-password.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/forget-password.vue new file mode 100644 index 000000000..7955ded67 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/forget-password.vue @@ -0,0 +1,115 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/authentication/index.ts new file mode 100644 index 000000000..95a3890fd --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/index.ts @@ -0,0 +1,7 @@ +export { default as AuthenticationCodeLogin } from './code-login.vue'; +export { default as AuthenticationForgetPassword } from './forget-password.vue'; +export { default as AuthenticationLogin } from './login.vue'; +export { default as AuthenticationLoginExpiredModal } from './login-expired-modal.vue'; +export { default as AuthenticationQrCodeLogin } from './qrcode-login.vue'; +export { default as AuthenticationRegister } from './register.vue'; +export type { AuthenticationProps } from './types'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue new file mode 100644 index 000000000..0b705ad3b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/login.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/login.vue new file mode 100644 index 000000000..22b0a45e7 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/login.vue @@ -0,0 +1,184 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/qrcode-login.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/qrcode-login.vue new file mode 100644 index 000000000..6e7a3320c --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/qrcode-login.vue @@ -0,0 +1,94 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/register.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/register.vue new file mode 100644 index 000000000..f603a8996 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/register.vue @@ -0,0 +1,119 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/third-party-login.vue b/apps/vben5/packages/effects/common-ui/src/ui/authentication/third-party-login.vue new file mode 100644 index 000000000..47326796c --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/third-party-login.vue @@ -0,0 +1,36 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/authentication/types.ts b/apps/vben5/packages/effects/common-ui/src/ui/authentication/types.ts new file mode 100644 index 000000000..cb297e2e1 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/authentication/types.ts @@ -0,0 +1,70 @@ +interface AuthenticationProps { + /** + * @zh_CN 验证码登录路径 + */ + codeLoginPath?: string; + /** + * @zh_CN 忘记密码路径 + */ + forgetPasswordPath?: string; + + /** + * @zh_CN 是否处于加载处理状态 + */ + loading?: boolean; + + /** + * @zh_CN 二维码登录路径 + */ + qrCodeLoginPath?: string; + + /** + * @zh_CN 注册路径 + */ + registerPath?: string; + + /** + * @zh_CN 是否显示验证码登录 + */ + showCodeLogin?: boolean; + /** + * @zh_CN 是否显示忘记密码 + */ + showForgetPassword?: boolean; + + /** + * @zh_CN 是否显示二维码登录 + */ + showQrcodeLogin?: boolean; + + /** + * @zh_CN 是否显示注册按钮 + */ + showRegister?: boolean; + + /** + * @zh_CN 是否显示记住账号 + */ + showRememberMe?: boolean; + + /** + * @zh_CN 是否显示第三方登录 + */ + showThirdPartyLogin?: boolean; + + /** + * @zh_CN 登录框子标题 + */ + subTitle?: string; + + /** + * @zh_CN 登录框标题 + */ + title?: string; + /** + * @zh_CN 提交按钮文本 + */ + submitButtonText?: string; +} + +export type { AuthenticationProps }; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-chart-card.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-chart-card.vue new file mode 100644 index 000000000..294d5173e --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-chart-card.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-charts-tabs.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-charts-tabs.vue new file mode 100644 index 000000000..9c8cf203d --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-charts-tabs.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue new file mode 100644 index 000000000..ea4d7bca8 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/index.ts new file mode 100644 index 000000000..7c67b8721 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/analysis/index.ts @@ -0,0 +1,3 @@ +export { default as AnalysisChartCard } from './analysis-chart-card.vue'; +export { default as AnalysisChartsTabs } from './analysis-charts-tabs.vue'; +export { default as AnalysisOverview } from './analysis-overview.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/index.ts new file mode 100644 index 000000000..bc74424bc --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/index.ts @@ -0,0 +1,3 @@ +export * from './analysis'; +export type * from './typing'; +export * from './workbench'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/typing.ts b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/typing.ts new file mode 100644 index 000000000..48fc9c7bf --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/typing.ts @@ -0,0 +1,48 @@ +import type { Component } from 'vue'; + +interface AnalysisOverviewItem { + icon: Component | string; + title: string; + totalTitle: string; + totalValue: number; + value: number; +} + +interface WorkbenchProjectItem { + color?: string; + content: string; + date: string; + group: string; + icon: Component | string; + title: string; + url?: string; +} + +interface WorkbenchTrendItem { + avatar: string; + content: string; + date: string; + title: string; +} + +interface WorkbenchTodoItem { + completed: boolean; + content: string; + date: string; + title: string; +} + +interface WorkbenchQuickNavItem { + color?: string; + icon: Component | string; + title: string; + url?: string; +} + +export type { + AnalysisOverviewItem, + WorkbenchProjectItem, + WorkbenchQuickNavItem, + WorkbenchTodoItem, + WorkbenchTrendItem, +}; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/index.ts new file mode 100644 index 000000000..76b7af17b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/index.ts @@ -0,0 +1,5 @@ +export { default as WorkbenchHeader } from './workbench-header.vue'; +export { default as WorkbenchProject } from './workbench-project.vue'; +export { default as WorkbenchQuickNav } from './workbench-quick-nav.vue'; +export { default as WorkbenchTodo } from './workbench-todo.vue'; +export { default as WorkbenchTrends } from './workbench-trends.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue new file mode 100644 index 000000000..a3ea3eb02 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue @@ -0,0 +1,46 @@ + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue new file mode 100644 index 000000000..b70629295 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-quick-nav.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-quick-nav.vue new file mode 100644 index 000000000..78846099e --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-quick-nav.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-todo.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-todo.vue new file mode 100644 index 000000000..8aae81fef --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-todo.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-trends.vue b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-trends.vue new file mode 100644 index 000000000..358b210fa --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-trends.vue @@ -0,0 +1,64 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/fallback.ts b/apps/vben5/packages/effects/common-ui/src/ui/fallback/fallback.ts new file mode 100644 index 000000000..5f8e19370 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/fallback.ts @@ -0,0 +1,25 @@ +interface FallbackProps { + /** + * 描述 + */ + description?: string; + /** + * @zh_CN 首页路由地址 + * @default / + */ + homePath?: string; + /** + * @zh_CN 默认显示的图片 + * @default pageNotFoundSvg + */ + image?: string; + /** + * @zh_CN 内置类型 + */ + status?: '403' | '404' | '500' | 'coming-soon' | 'offline'; + /** + * @zh_CN 页面提示语 + */ + title?: string; +} +export type { FallbackProps }; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/fallback.vue b/apps/vben5/packages/effects/common-ui/src/ui/fallback/fallback.vue new file mode 100644 index 000000000..dc46ef426 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/fallback.vue @@ -0,0 +1,163 @@ + + + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-403.vue b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-403.vue new file mode 100644 index 000000000..e3908673b --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-403.vue @@ -0,0 +1,151 @@ + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-404.vue b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-404.vue new file mode 100644 index 000000000..385cf6a5e --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-404.vue @@ -0,0 +1,154 @@ + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-500.vue b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-500.vue new file mode 100644 index 000000000..97326b7c6 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-500.vue @@ -0,0 +1,215 @@ + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-coming-soon.vue b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-coming-soon.vue new file mode 100644 index 000000000..e511c43c0 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-coming-soon.vue @@ -0,0 +1,262 @@ + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-offline.vue b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-offline.vue new file mode 100644 index 000000000..9e50b19b4 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/icon-offline.vue @@ -0,0 +1,112 @@ + diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/warning.svg b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/warning.svg new file mode 100644 index 000000000..951a905c3 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/vben5/packages/effects/common-ui/src/ui/fallback/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/fallback/index.ts new file mode 100644 index 000000000..f56f66517 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/fallback/index.ts @@ -0,0 +1,2 @@ +export type * from './fallback'; +export { default as Fallback } from './fallback.vue'; diff --git a/apps/vben5/packages/effects/common-ui/src/ui/index.ts b/apps/vben5/packages/effects/common-ui/src/ui/index.ts new file mode 100644 index 000000000..fb99fdec1 --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/src/ui/index.ts @@ -0,0 +1,4 @@ +export * from './about'; +export * from './authentication'; +export * from './dashboard'; +export * from './fallback'; diff --git a/apps/vben5/packages/effects/common-ui/tsconfig.json b/apps/vben5/packages/effects/common-ui/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/effects/common-ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/hooks/README.md b/apps/vben5/packages/effects/hooks/README.md new file mode 100644 index 000000000..54d9ff757 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/README.md @@ -0,0 +1,19 @@ +# @vben/hooks + +用于多个 `app` 公用的 hook,继承了 `@vben/hooks` 的所有能力。业务上有通用 hooks 可以放在这里。 + +## 用法 + +### 添加依赖 + +```bash +# 进入目标应用目录,例如 apps/xxxx-app +# cd apps/xxxx-app +pnpm add @vben/hooks +``` + +### 使用 + +```ts +import { useNamespace } from '@vben/hooks'; +``` diff --git a/apps/vben5/packages/effects/hooks/package.json b/apps/vben5/packages/effects/hooks/package.json new file mode 100644 index 000000000..8c87d3087 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/package.json @@ -0,0 +1,32 @@ +{ + "name": "@vben/hooks", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/effects/hooks" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben/preferences": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/types": "workspace:*", + "@vben/utils": "workspace:*", + "vue": "catalog:", + "vue-router": "catalog:", + "watermark-js-plus": "catalog:" + } +} diff --git a/apps/vben5/packages/effects/hooks/src/index.ts b/apps/vben5/packages/effects/hooks/src/index.ts new file mode 100644 index 000000000..51f640602 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/index.ts @@ -0,0 +1,8 @@ +export * from './use-app-config'; +export * from './use-content-maximize'; +export * from './use-design-tokens'; +export * from './use-pagination'; +export * from './use-refresh'; +export * from './use-tabs'; +export * from './use-watermark'; +export * from '@vben-core/composables'; diff --git a/apps/vben5/packages/effects/hooks/src/use-app-config.ts b/apps/vben5/packages/effects/hooks/src/use-app-config.ts new file mode 100644 index 000000000..dfe1e28d6 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-app-config.ts @@ -0,0 +1,26 @@ +import type { + ApplicationConfig, + VbenAdminProAppConfigRaw, +} from '@vben/types/global'; + +/** + * 由 vite-inject-app-config 注入的全局配置 + */ +export function useAppConfig( + env: Record, + isProduction: boolean, +): ApplicationConfig { + // 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量 + const config = isProduction + ? window._VBEN_ADMIN_PRO_APP_CONF_ + : (env as VbenAdminProAppConfigRaw); + + const { VITE_GLOB_API_URL, VITE_GLOB_CLIENT_ID, VITE_GLOB_CLIENT_SECRET } = + config; + + return { + apiURL: VITE_GLOB_API_URL, + clientId: VITE_GLOB_CLIENT_ID, + clientSecret: VITE_GLOB_CLIENT_SECRET, + }; +} diff --git a/apps/vben5/packages/effects/hooks/src/use-content-maximize.ts b/apps/vben5/packages/effects/hooks/src/use-content-maximize.ts new file mode 100644 index 000000000..77d1fab35 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-content-maximize.ts @@ -0,0 +1,24 @@ +import { updatePreferences, usePreferences } from '@vben/preferences'; +/** + * 主体区域最大化 + */ +export function useContentMaximize() { + const { contentIsMaximize } = usePreferences(); + + function toggleMaximize() { + const isMaximize = contentIsMaximize.value; + + updatePreferences({ + header: { + hidden: !isMaximize, + }, + sidebar: { + hidden: !isMaximize, + }, + }); + } + return { + contentIsMaximize, + toggleMaximize, + }; +} diff --git a/apps/vben5/packages/effects/hooks/src/use-design-tokens.ts b/apps/vben5/packages/effects/hooks/src/use-design-tokens.ts new file mode 100644 index 000000000..8ee744013 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-design-tokens.ts @@ -0,0 +1,280 @@ +import { reactive, watch } from 'vue'; + +import { preferences, usePreferences } from '@vben/preferences'; +import { convertToRgb, updateCSSVariables } from '@vben/utils'; + +/** + * 用于适配各个框架的设计系统 + */ + +export function useAntdDesignTokens() { + const rootStyles = getComputedStyle(document.documentElement); + + const tokens = reactive({ + borderRadius: '' as any, + colorBgBase: '', + colorBgContainer: '', + colorBgElevated: '', + colorBgLayout: '', + colorBgMask: '', + colorBorder: '', + colorBorderSecondary: '', + colorError: '', + colorInfo: '', + colorPrimary: '', + colorSuccess: '', + colorTextBase: '', + colorWarning: '', + }); + + const getCssVariableValue = (variable: string, isColor: boolean = true) => { + const value = rootStyles.getPropertyValue(variable); + return isColor ? `hsl(${value})` : value; + }; + + watch( + () => preferences.theme, + () => { + tokens.colorPrimary = getCssVariableValue('--primary'); + + tokens.colorInfo = getCssVariableValue('--primary'); + + tokens.colorError = getCssVariableValue('--destructive'); + + tokens.colorWarning = getCssVariableValue('--warning'); + + tokens.colorSuccess = getCssVariableValue('--success'); + + tokens.colorTextBase = getCssVariableValue('--foreground'); + + getCssVariableValue('--primary-foreground'); + + tokens.colorBorderSecondary = tokens.colorBorder = + getCssVariableValue('--border'); + + tokens.colorBgElevated = getCssVariableValue('--popover'); + + tokens.colorBgContainer = getCssVariableValue('--card'); + + tokens.colorBgBase = getCssVariableValue('--background'); + + const radius = Number.parseFloat(getCssVariableValue('--radius', false)); + // 1rem = 16px + tokens.borderRadius = radius * 16; + + tokens.colorBgLayout = getCssVariableValue('--background-deep'); + tokens.colorBgMask = getCssVariableValue('--overlay'); + }, + { immediate: true }, + ); + + return { + tokens, + }; +} + +export function useNaiveDesignTokens() { + const rootStyles = getComputedStyle(document.documentElement); + + const commonTokens = reactive({ + baseColor: '', + bodyColor: '', + borderColor: '', + borderRadius: '', + cardColor: '', + dividerColor: '', + errorColor: '', + errorColorHover: '', + errorColorPressed: '', + errorColorSuppl: '', + invertedColor: '', + modalColor: '', + popoverColor: '', + primaryColor: '', + primaryColorHover: '', + primaryColorPressed: '', + primaryColorSuppl: '', + successColor: '', + successColorHover: '', + successColorPressed: '', + successColorSuppl: '', + tableColor: '', + textColorBase: '', + warningColor: '', + warningColorHover: '', + warningColorPressed: '', + warningColorSuppl: '', + }); + + const getCssVariableValue = (variable: string, isColor: boolean = true) => { + const value = rootStyles.getPropertyValue(variable); + return isColor ? convertToRgb(`hsl(${value})`) : value; + }; + + watch( + () => preferences.theme, + () => { + commonTokens.primaryColor = getCssVariableValue('--primary'); + commonTokens.primaryColorHover = getCssVariableValue('--primary-600'); + commonTokens.primaryColorPressed = getCssVariableValue('--primary-700'); + commonTokens.primaryColorSuppl = getCssVariableValue('--primary-800'); + + commonTokens.errorColor = getCssVariableValue('--destructive'); + commonTokens.errorColorHover = getCssVariableValue('--destructive-600'); + commonTokens.errorColorPressed = getCssVariableValue('--destructive-700'); + commonTokens.errorColorSuppl = getCssVariableValue('--destructive-800'); + + commonTokens.warningColor = getCssVariableValue('--warning'); + commonTokens.warningColorHover = getCssVariableValue('--warning-600'); + commonTokens.warningColorPressed = getCssVariableValue('--warning-700'); + commonTokens.warningColorSuppl = getCssVariableValue('--warning-800'); + + commonTokens.successColor = getCssVariableValue('--success'); + commonTokens.successColorHover = getCssVariableValue('--success-600'); + commonTokens.successColorPressed = getCssVariableValue('--success-700'); + commonTokens.successColorSuppl = getCssVariableValue('--success-800'); + + commonTokens.textColorBase = getCssVariableValue('--foreground'); + + commonTokens.baseColor = getCssVariableValue('--primary-foreground'); + + commonTokens.dividerColor = commonTokens.borderColor = + getCssVariableValue('--border'); + + commonTokens.modalColor = commonTokens.popoverColor = + getCssVariableValue('--popover'); + + commonTokens.tableColor = commonTokens.cardColor = + getCssVariableValue('--card'); + + commonTokens.bodyColor = getCssVariableValue('--background'); + commonTokens.invertedColor = getCssVariableValue('--background-deep'); + + commonTokens.borderRadius = getCssVariableValue('--radius', false); + }, + { immediate: true }, + ); + return { + commonTokens, + }; +} + +export function useElementPlusDesignTokens() { + const { isDark } = usePreferences(); + const rootStyles = getComputedStyle(document.documentElement); + + const getCssVariableValueRaw = (variable: string) => { + return rootStyles.getPropertyValue(variable); + }; + + const getCssVariableValue = (variable: string, isColor: boolean = true) => { + const value = getCssVariableValueRaw(variable); + return isColor ? convertToRgb(`hsl(${value})`) : value; + }; + + watch( + () => preferences.theme, + () => { + const background = getCssVariableValue('--background'); + const border = getCssVariableValue('--border'); + const accent = getCssVariableValue('--accent'); + + const variables: Record = { + '--el-bg-color': background, + '--el-bg-color-overlay': getCssVariableValue('--popover'), + '--el-bg-color-page': getCssVariableValue('--background-deep'), + '--el-border-color': border, + '--el-border-color-dark': border, + '--el-border-color-extra-light': border, + '--el-border-color-hover': accent, + '--el-border-color-light': border, + '--el-border-color-lighter': border, + + '--el-border-radius-base': getCssVariableValue('--radius', false), + '--el-color-danger': getCssVariableValue('--destructive-500'), + '--el-color-danger-dark-2': getCssVariableValue('--destructive'), + '--el-color-danger-light-3': getCssVariableValue('--destructive-400'), + '--el-color-danger-light-5': getCssVariableValue('--destructive-300'), + '--el-color-danger-light-7': getCssVariableValue('--destructive-200'), + '--el-color-danger-light-8': isDark.value + ? border + : getCssVariableValue('--destructive-100'), + '--el-color-danger-light-9': isDark.value + ? accent + : getCssVariableValue('--destructive-50'), + + '--el-color-error': getCssVariableValue('--destructive-500'), + '--el-color-error-dark-2': getCssVariableValue('--destructive'), + '--el-color-error-light-3': getCssVariableValue('--destructive-400'), + '--el-color-error-light-5': getCssVariableValue('--destructive-300'), + '--el-color-error-light-7': getCssVariableValue('--destructive-200'), + '--el-color-error-light-8': isDark.value + ? border + : getCssVariableValue('--destructive-100'), + '--el-color-error-light-9': isDark.value + ? accent + : getCssVariableValue('--destructive-50'), + + '--el-color-info-light-8': border, + '--el-color-info-light-9': getCssVariableValue('--info'), // getCssVariableValue('--secondary'), + '--el-color-primary': getCssVariableValue('--primary-500'), + '--el-color-primary-dark-2': getCssVariableValue('--primary'), + '--el-color-primary-light-3': getCssVariableValue('--primary-400'), + '--el-color-primary-light-5': getCssVariableValue('--primary-300'), + '--el-color-primary-light-7': isDark.value + ? border + : getCssVariableValue('--primary-200'), + '--el-color-primary-light-8': isDark.value + ? border + : getCssVariableValue('--primary-100'), + '--el-color-primary-light-9': isDark.value + ? accent + : getCssVariableValue('--primary-50'), + + '--el-color-success': getCssVariableValue('--success-500'), + '--el-color-success-dark-2': getCssVariableValue('--success'), + '--el-color-success-light-3': getCssVariableValue('--success-400'), + '--el-color-success-light-5': getCssVariableValue('--success-300'), + '--el-color-success-light-7': getCssVariableValue('--success-200'), + '--el-color-success-light-8': isDark.value + ? border + : getCssVariableValue('--success-100'), + '--el-color-success-light-9': isDark.value + ? accent + : getCssVariableValue('--success-50'), + + '--el-color-warning': getCssVariableValue('--warning-500'), + '--el-color-warning-dark-2': getCssVariableValue('--warning'), + '--el-color-warning-light-3': getCssVariableValue('--warning-400'), + '--el-color-warning-light-5': getCssVariableValue('--warning-300'), + '--el-color-warning-light-7': getCssVariableValue('--warning-200'), + '--el-color-warning-light-8': isDark.value + ? border + : getCssVariableValue('--warning-100'), + '--el-color-warning-light-9': isDark.value + ? accent + : getCssVariableValue('--warning-50'), + + '--el-fill-color': getCssVariableValue('--accent'), + '--el-fill-color-blank': background, + '--el-fill-color-light': getCssVariableValue('--accent'), + '--el-fill-color-lighter': getCssVariableValue('--accent-lighter'), + + '--el-fill-color-dark': getCssVariableValue('--accent-dark'), + '--el-fill-color-darker': getCssVariableValue('--accent-darker'), + + // 解决ElLoading背景色问题 + '--el-mask-color': isDark.value + ? 'rgba(0,0,0,.8)' + : 'rgba(255,255,255,.9)', + + '--el-text-color-primary': getCssVariableValue('--foreground'), + + '--el-text-color-regular': getCssVariableValue('--foreground'), + }; + + updateCSSVariables(variables, `__vben_design_styles__`); + }, + { immediate: true }, + ); +} diff --git a/apps/vben5/packages/effects/hooks/src/use-pagination.ts b/apps/vben5/packages/effects/hooks/src/use-pagination.ts new file mode 100644 index 000000000..23b36dcd3 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-pagination.ts @@ -0,0 +1,57 @@ +import type { Ref } from 'vue'; +import { computed, ref, unref } from 'vue'; + +/** + * Paginates an array of items + * @param list The array to paginate + * @param pageNo The current page number (1-based) + * @param pageSize Number of items per page + * @returns Paginated array slice + * @throws {Error} If pageNo or pageSize are invalid + */ +function pagination(list: T[], pageNo: number, pageSize: number): T[] { + if (pageNo < 1) throw new Error('Page number must be positive'); + if (pageSize < 1) throw new Error('Page size must be positive'); + + const offset = (pageNo - 1) * Number(pageSize); + const ret = + offset + pageSize >= list.length + ? list.slice(offset) + : list.slice(offset, offset + pageSize); + return ret; +} + +export function usePagination(list: Ref, pageSize: number) { + const currentPage = ref(1); + const pageSizeRef = ref(pageSize); + + const totalPages = computed(() => + Math.ceil(unref(list).length / unref(pageSizeRef)), + ); + + const paginationList = computed(() => { + return pagination(unref(list), unref(currentPage), unref(pageSizeRef)); + }); + + const total = computed(() => { + return unref(list).length; + }); + + function setCurrentPage(page: number) { + if (page < 1 || page > unref(totalPages)) { + throw new Error('Invalid page number'); + } + currentPage.value = page; + } + + function setPageSize(pageSize: number) { + if (pageSize < 1) { + throw new Error('Page size must be positive'); + } + pageSizeRef.value = pageSize; + // Reset to first page to prevent invalid state + currentPage.value = 1; + } + + return { setCurrentPage, total, setPageSize, paginationList }; +} diff --git a/apps/vben5/packages/effects/hooks/src/use-refresh.ts b/apps/vben5/packages/effects/hooks/src/use-refresh.ts new file mode 100644 index 000000000..b3a5caeb8 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-refresh.ts @@ -0,0 +1,16 @@ +import { useRouter } from 'vue-router'; + +import { useTabbarStore } from '@vben/stores'; + +export function useRefresh() { + const router = useRouter(); + const tabbarStore = useTabbarStore(); + + async function refresh() { + await tabbarStore.refresh(router); + } + + return { + refresh, + }; +} diff --git a/apps/vben5/packages/effects/hooks/src/use-tabs.ts b/apps/vben5/packages/effects/hooks/src/use-tabs.ts new file mode 100644 index 000000000..aa79d72f6 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-tabs.ts @@ -0,0 +1,113 @@ +import { type RouteLocationNormalized, useRoute, useRouter } from 'vue-router'; + +import { useTabbarStore } from '@vben/stores'; + +export function useTabs() { + const router = useRouter(); + const route = useRoute(); + const tabbarStore = useTabbarStore(); + + async function closeLeftTabs(tab?: RouteLocationNormalized) { + await tabbarStore.closeLeftTabs(tab || route); + } + + async function closeAllTabs() { + await tabbarStore.closeAllTabs(router); + } + + async function closeRightTabs(tab?: RouteLocationNormalized) { + await tabbarStore.closeRightTabs(tab || route); + } + + async function closeOtherTabs(tab?: RouteLocationNormalized) { + await tabbarStore.closeOtherTabs(tab || route); + } + + async function closeCurrentTab(tab?: RouteLocationNormalized) { + await tabbarStore.closeTab(tab || route, router); + } + + async function pinTab(tab?: RouteLocationNormalized) { + await tabbarStore.pinTab(tab || route); + } + + async function unpinTab(tab?: RouteLocationNormalized) { + await tabbarStore.unpinTab(tab || route); + } + + async function toggleTabPin(tab?: RouteLocationNormalized) { + await tabbarStore.toggleTabPin(tab || route); + } + + async function refreshTab() { + await tabbarStore.refresh(router); + } + + async function openTabInNewWindow(tab?: RouteLocationNormalized) { + await tabbarStore.openTabInNewWindow(tab || route); + } + + async function closeTabByKey(key: string) { + await tabbarStore.closeTabByKey(key, router); + } + + async function setTabTitle(title: string) { + tabbarStore.setUpdateTime(); + await tabbarStore.setTabTitle(route, title); + } + + async function resetTabTitle() { + tabbarStore.setUpdateTime(); + await tabbarStore.resetTabTitle(route); + } + + /** + * 获取操作是否禁用 + * @param tab + */ + function getTabDisableState(tab: RouteLocationNormalized = route) { + const tabs = tabbarStore.getTabs; + const affixTabs = tabbarStore.affixTabs; + const index = tabs.findIndex((item) => item.path === tab.path); + + const disabled = tabs.length <= 1; + + const { meta } = tab; + const affixTab = meta?.affixTab ?? false; + const isCurrentTab = route.path === tab.path; + + // 当前处于最左侧或者减去固定标签页的数量等于0 + const disabledCloseLeft = + index === 0 || index - affixTabs.length <= 0 || !isCurrentTab; + + const disabledCloseRight = !isCurrentTab || index === tabs.length - 1; + + const disabledCloseOther = + disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1; + return { + disabledCloseAll: disabled, + disabledCloseCurrent: !!affixTab || disabled, + disabledCloseLeft, + disabledCloseOther, + disabledCloseRight, + disabledRefresh: !isCurrentTab, + }; + } + + return { + closeAllTabs, + closeCurrentTab, + closeLeftTabs, + closeOtherTabs, + closeRightTabs, + closeTabByKey, + getTabDisableState, + openTabInNewWindow, + pinTab, + refreshTab, + resetTabTitle, + setTabTitle, + toggleTabPin, + unpinTab, + }; +} diff --git a/apps/vben5/packages/effects/hooks/src/use-watermark.ts b/apps/vben5/packages/effects/hooks/src/use-watermark.ts new file mode 100644 index 000000000..c3f56eaf7 --- /dev/null +++ b/apps/vben5/packages/effects/hooks/src/use-watermark.ts @@ -0,0 +1,88 @@ +import type { Watermark, WatermarkOptions } from 'watermark-js-plus'; + +import { nextTick, onUnmounted, readonly, ref } from 'vue'; + +import { updatePreferences } from '@vben/preferences'; + +const watermark = ref(); +const unmountedHooked = ref(false); +const cachedOptions = ref>({ + advancedStyle: { + colorStops: [ + { + color: 'gray', + offset: 0, + }, + { + color: 'gray', + offset: 1, + }, + ], + type: 'linear', + }, + // fontSize: '20px', + content: '', + contentType: 'multi-line-text', + globalAlpha: 0.25, + gridLayoutOptions: { + cols: 2, + gap: [20, 20], + matrix: [ + [1, 0], + [0, 1], + ], + rows: 2, + }, + height: 200, + layout: 'grid', + rotate: 30, + width: 160, +}); + +export function useWatermark() { + async function initWatermark(options: Partial) { + const { Watermark } = await import('watermark-js-plus'); + + cachedOptions.value = { + ...cachedOptions.value, + ...options, + }; + watermark.value = new Watermark(cachedOptions.value); + updatePreferences({ app: { watermark: true } }); + await watermark.value?.create(); + } + + async function updateWatermark(options: Partial) { + if (watermark.value) { + await nextTick(); + await watermark.value?.changeOptions({ + ...cachedOptions.value, + ...options, + }); + } else { + await initWatermark(options); + } + } + + function destroyWatermark() { + if (watermark.value) { + watermark.value.destroy(); + watermark.value = undefined; + } + updatePreferences({ app: { watermark: false } }); + } + + // 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印 + if (!unmountedHooked.value) { + unmountedHooked.value = true; + onUnmounted(() => { + destroyWatermark(); + }); + } + + return { + destroyWatermark, + updateWatermark, + watermark: readonly(watermark), + }; +} diff --git a/apps/vben5/packages/effects/hooks/tsconfig.json b/apps/vben5/packages/effects/hooks/tsconfig.json new file mode 100644 index 000000000..b13aa7aba --- /dev/null +++ b/apps/vben5/packages/effects/hooks/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "compilerOptions": { + "types": ["vite/client", "@vben/types/global"] + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/layouts/package.json b/apps/vben5/packages/effects/layouts/package.json new file mode 100644 index 000000000..863874b43 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/package.json @@ -0,0 +1,43 @@ +{ + "name": "@vben/layouts", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/effects/layouts" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben-core/form-ui": "workspace:*", + "@vben-core/layout-ui": "workspace:*", + "@vben-core/menu-ui": "workspace:*", + "@vben-core/popup-ui": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben-core/tabs-ui": "workspace:*", + "@vben/constants": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/icons": "workspace:*", + "@vben/locales": "workspace:*", + "@vben/preferences": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/types": "workspace:*", + "@vben/utils": "workspace:*", + "@vueuse/core": "catalog:", + "vue": "catalog:", + "vue-router": "catalog:" + } +} diff --git a/apps/vben5/packages/effects/layouts/src/authentication/authentication.vue b/apps/vben5/packages/effects/layouts/src/authentication/authentication.vue new file mode 100644 index 000000000..ceae8027f --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/authentication/authentication.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/apps/vben5/packages/effects/layouts/src/authentication/form.vue b/apps/vben5/packages/effects/layouts/src/authentication/form.vue new file mode 100644 index 000000000..80c907493 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/authentication/form.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/authentication/icons/slogan.vue b/apps/vben5/packages/effects/layouts/src/authentication/icons/slogan.vue new file mode 100644 index 000000000..82cc11983 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/authentication/icons/slogan.vue @@ -0,0 +1,4568 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/authentication/index.ts b/apps/vben5/packages/effects/layouts/src/authentication/index.ts new file mode 100644 index 000000000..6c684d117 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/authentication/index.ts @@ -0,0 +1 @@ +export { default as AuthPageLayout } from './authentication.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/authentication/toolbar.vue b/apps/vben5/packages/effects/layouts/src/authentication/toolbar.vue new file mode 100644 index 000000000..94d321ab0 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/authentication/toolbar.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/authentication/types.ts b/apps/vben5/packages/effects/layouts/src/authentication/types.ts new file mode 100644 index 000000000..c4c1c7f69 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/authentication/types.ts @@ -0,0 +1 @@ +export type ToolbarType = 'color' | 'language' | 'layout' | 'theme'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/README.md b/apps/vben5/packages/effects/layouts/src/basic/README.md new file mode 100644 index 000000000..b1266ea2b --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/README.md @@ -0,0 +1,7 @@ +## layout + +### header + +- 支持N个自定义插槽,命名方式:header-right-n,header-left-n +- header-left-n ,排序方式:0-19 ,breadcrumb 21-x +- header-right-n ,排序方式:0-49,global-search,51-59,theme-toggle,61-69,language-toggle,71-79,fullscreen,81-89,notification,91-149,user-dropdown,151-x diff --git a/apps/vben5/packages/effects/layouts/src/basic/content/content-spinner.vue b/apps/vben5/packages/effects/layouts/src/basic/content/content-spinner.vue new file mode 100644 index 000000000..e97e4b6c9 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/content/content-spinner.vue @@ -0,0 +1,12 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/content/content.vue b/apps/vben5/packages/effects/layouts/src/basic/content/content.vue new file mode 100644 index 000000000..4ce9e4dbb --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/content/content.vue @@ -0,0 +1,114 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/content/index.ts b/apps/vben5/packages/effects/layouts/src/basic/content/index.ts new file mode 100644 index 000000000..500cbccaf --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/content/index.ts @@ -0,0 +1,2 @@ +export { default as LayoutContent } from './content.vue'; +export { default as LayoutContentSpinner } from './content-spinner.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/content/use-content-spinner.ts b/apps/vben5/packages/effects/layouts/src/basic/content/use-content-spinner.ts new file mode 100644 index 000000000..dfe3c5366 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/content/use-content-spinner.ts @@ -0,0 +1,50 @@ +import { computed, ref } from 'vue'; +import { useRouter } from 'vue-router'; + +import { preferences } from '@vben/preferences'; + +function useContentSpinner() { + const spinning = ref(false); + const startTime = ref(0); + const router = useRouter(); + const minShowTime = 500; // 最小显示时间 + const enableLoading = computed(() => preferences.transition.loading); + + // 结束加载动画 + const onEnd = () => { + if (!enableLoading.value) { + return; + } + const processTime = performance.now() - startTime.value; + if (processTime < minShowTime) { + setTimeout(() => { + spinning.value = false; + }, minShowTime - processTime); + } else { + spinning.value = false; + } + }; + + // 路由前置守卫 + router.beforeEach((to) => { + if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) { + return true; + } + startTime.value = performance.now(); + spinning.value = true; + return true; + }); + + // 路由后置守卫 + router.afterEach((to) => { + if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) { + return true; + } + onEnd(); + return true; + }); + + return { spinning }; +} + +export { useContentSpinner }; diff --git a/apps/vben5/packages/effects/layouts/src/basic/copyright/copyright.vue b/apps/vben5/packages/effects/layouts/src/basic/copyright/copyright.vue new file mode 100644 index 000000000..8202076a1 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/copyright/copyright.vue @@ -0,0 +1,48 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/copyright/index.ts b/apps/vben5/packages/effects/layouts/src/basic/copyright/index.ts new file mode 100644 index 000000000..e0620daa7 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/copyright/index.ts @@ -0,0 +1 @@ +export { default as Copyright } from './copyright.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/footer/footer.vue b/apps/vben5/packages/effects/layouts/src/basic/footer/footer.vue new file mode 100644 index 000000000..6fc256d71 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/footer/footer.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/footer/index.ts b/apps/vben5/packages/effects/layouts/src/basic/footer/index.ts new file mode 100644 index 000000000..7e149a25b --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/footer/index.ts @@ -0,0 +1 @@ +export { default as LayoutFooter } from './footer.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/header/header.vue b/apps/vben5/packages/effects/layouts/src/basic/header/header.vue new file mode 100644 index 000000000..546342bf9 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/header/header.vue @@ -0,0 +1,168 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/header/index.ts b/apps/vben5/packages/effects/layouts/src/basic/header/index.ts new file mode 100644 index 000000000..44f0729f7 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/header/index.ts @@ -0,0 +1 @@ +export { default as LayoutHeader } from './header.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/index.ts b/apps/vben5/packages/effects/layouts/src/basic/index.ts new file mode 100644 index 000000000..b3a01cd11 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/index.ts @@ -0,0 +1 @@ +export { default as BasicLayout } from './layout.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/layout.vue b/apps/vben5/packages/effects/layouts/src/basic/layout.vue new file mode 100644 index 000000000..16c3551f2 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/layout.vue @@ -0,0 +1,333 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/extra-menu.vue b/apps/vben5/packages/effects/layouts/src/basic/menu/extra-menu.vue new file mode 100644 index 000000000..d8ccc1b48 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/extra-menu.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/index.ts b/apps/vben5/packages/effects/layouts/src/basic/menu/index.ts new file mode 100644 index 000000000..72b87ad88 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/index.ts @@ -0,0 +1,5 @@ +export { default as LayoutExtraMenu } from './extra-menu.vue'; +export { default as LayoutMenu } from './menu.vue'; +export { default as LayoutMixedMenu } from './mixed-menu.vue'; +export * from './use-extra-menu'; +export * from './use-mixed-menu'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/menu.vue b/apps/vben5/packages/effects/layouts/src/basic/menu/menu.vue new file mode 100644 index 000000000..11c34e198 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/menu.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/mixed-menu.vue b/apps/vben5/packages/effects/layouts/src/basic/menu/mixed-menu.vue new file mode 100644 index 000000000..b54bc48bb --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/mixed-menu.vue @@ -0,0 +1,44 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/use-extra-menu.ts b/apps/vben5/packages/effects/layouts/src/basic/menu/use-extra-menu.ts new file mode 100644 index 000000000..a1c6d3fe9 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/use-extra-menu.ts @@ -0,0 +1,109 @@ +import type { MenuRecordRaw } from '@vben/types'; + +import { computed, ref, watch } from 'vue'; +import { useRoute } from 'vue-router'; + +import { preferences } from '@vben/preferences'; +import { useAccessStore } from '@vben/stores'; +import { findRootMenuByPath } from '@vben/utils'; + +import { useNavigation } from './use-navigation'; + +function useExtraMenu() { + const accessStore = useAccessStore(); + const { navigation } = useNavigation(); + + const menus = computed(() => accessStore.accessMenus); + + const route = useRoute(); + const extraMenus = ref([]); + const sidebarExtraVisible = ref(false); + const extraActiveMenu = ref(''); + + /** + * 选择混合菜单事件 + * @param menu + */ + const handleMixedMenuSelect = async (menu: MenuRecordRaw) => { + extraMenus.value = menu?.children ?? []; + extraActiveMenu.value = menu.parents?.[0] ?? menu.path; + const hasChildren = extraMenus.value.length > 0; + + sidebarExtraVisible.value = hasChildren; + if (!hasChildren) { + await navigation(menu.path); + } + }; + + /** + * 选择默认菜单事件 + * @param menu + * @param rootMenu + */ + const handleDefaultSelect = ( + menu: MenuRecordRaw, + rootMenu?: MenuRecordRaw, + ) => { + extraMenus.value = rootMenu?.children ?? []; + extraActiveMenu.value = menu.parents?.[0] ?? menu.path; + + if (preferences.sidebar.expandOnHover) { + sidebarExtraVisible.value = extraMenus.value.length > 0; + } + }; + + /** + * 侧边菜单鼠标移出事件 + */ + const handleSideMouseLeave = () => { + if (preferences.sidebar.expandOnHover) { + return; + } + sidebarExtraVisible.value = false; + + const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath( + menus.value, + route.path, + ); + extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? ''; + extraMenus.value = rootMenu?.children ?? []; + }; + + const handleMenuMouseEnter = (menu: MenuRecordRaw) => { + if (!preferences.sidebar.expandOnHover) { + const { findMenu } = findRootMenuByPath(menus.value, menu.path); + extraMenus.value = findMenu?.children ?? []; + extraActiveMenu.value = menu.parents?.[0] ?? menu.path; + sidebarExtraVisible.value = extraMenus.value.length > 0; + } + }; + + watch( + () => route.path, + (path) => { + const currentPath = route.meta?.activePath || path; + // if (preferences.sidebar.expandOnHover) { + // return; + // } + const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath( + menus.value, + currentPath, + ); + extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? ''; + extraMenus.value = rootMenu?.children ?? []; + }, + { immediate: true }, + ); + + return { + extraActiveMenu, + extraMenus, + handleDefaultSelect, + handleMenuMouseEnter, + handleMixedMenuSelect, + handleSideMouseLeave, + sidebarExtraVisible, + }; +} + +export { useExtraMenu }; diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts b/apps/vben5/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts new file mode 100644 index 000000000..ca78fc0dc --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts @@ -0,0 +1,129 @@ +import type { MenuRecordRaw } from '@vben/types'; + +import { computed, onBeforeMount, ref, watch } from 'vue'; +import { useRoute } from 'vue-router'; + +import { preferences, usePreferences } from '@vben/preferences'; +import { useAccessStore } from '@vben/stores'; +import { findRootMenuByPath } from '@vben/utils'; + +import { useNavigation } from './use-navigation'; + +function useMixedMenu() { + const { navigation } = useNavigation(); + const accessStore = useAccessStore(); + const route = useRoute(); + const splitSideMenus = ref([]); + const rootMenuPath = ref(''); + + const { isMixedNav } = usePreferences(); + + const needSplit = computed( + () => preferences.navigation.split && isMixedNav.value, + ); + + const sidebarVisible = computed(() => { + const enableSidebar = preferences.sidebar.enable; + if (needSplit.value) { + return enableSidebar && splitSideMenus.value.length > 0; + } + return enableSidebar; + }); + const menus = computed(() => accessStore.accessMenus); + + /** + * 头部菜单 + */ + const headerMenus = computed(() => { + if (!needSplit.value) { + return menus.value; + } + return menus.value.map((item) => { + return { + ...item, + children: [], + }; + }); + }); + + /** + * 侧边菜单 + */ + const sidebarMenus = computed(() => { + return needSplit.value ? splitSideMenus.value : menus.value; + }); + + /** + * 侧边菜单激活路径 + */ + const sidebarActive = computed(() => { + return (route?.meta?.activePath as string) ?? route.path; + }); + + /** + * 头部菜单激活路径 + */ + const headerActive = computed(() => { + if (!needSplit.value) { + return route.path; + } + return rootMenuPath.value; + }); + + /** + * 菜单点击事件处理 + * @param key 菜单路径 + * @param mode 菜单模式 + */ + const handleMenuSelect = (key: string, mode?: string) => { + if (!needSplit.value || mode === 'vertical') { + navigation(key); + return; + } + + const rootMenu = menus.value.find((item) => item.path === key); + rootMenuPath.value = rootMenu?.path ?? ''; + splitSideMenus.value = rootMenu?.children ?? []; + if (splitSideMenus.value.length === 0) { + navigation(key); + } + }; + + /** + * 计算侧边菜单 + * @param path 路由路径 + */ + function calcSideMenus(path: string = route.path) { + let { rootMenu } = findRootMenuByPath(menus.value, path); + if (!rootMenu) { + rootMenu = menus.value.find((item) => item.path === path); + } + rootMenuPath.value = rootMenu?.path ?? ''; + splitSideMenus.value = rootMenu?.children ?? []; + } + + watch( + () => route.path, + (path) => { + const currentPath = (route?.meta?.activePath as string) ?? path; + calcSideMenus(currentPath); + }, + { immediate: true }, + ); + + // 初始化计算侧边菜单 + onBeforeMount(() => { + calcSideMenus(route.meta?.activePath || route.path); + }); + + return { + handleMenuSelect, + headerActive, + headerMenus, + sidebarActive, + sidebarMenus, + sidebarVisible, + }; +} + +export { useMixedMenu }; diff --git a/apps/vben5/packages/effects/layouts/src/basic/menu/use-navigation.ts b/apps/vben5/packages/effects/layouts/src/basic/menu/use-navigation.ts new file mode 100644 index 000000000..ace64452e --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/menu/use-navigation.ts @@ -0,0 +1,33 @@ +import { type RouteRecordNormalized, useRouter } from 'vue-router'; + +import { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils'; + +function useNavigation() { + const router = useRouter(); + const routes = router.getRoutes(); + + const routeMetaMap = new Map(); + + routes.forEach((route) => { + routeMetaMap.set(route.path, route); + }); + + const navigation = async (path: string) => { + const route = routeMetaMap.get(path); + const { openInNewWindow = false, query = {} } = route?.meta ?? {}; + if (isHttpUrl(path)) { + openWindow(path, { target: '_blank' }); + } else if (openInNewWindow) { + openRouteInNewWindow(path); + } else { + await router.push({ + path, + query, + }); + } + }; + + return { navigation }; +} + +export { useNavigation }; diff --git a/apps/vben5/packages/effects/layouts/src/basic/tabbar/index.ts b/apps/vben5/packages/effects/layouts/src/basic/tabbar/index.ts new file mode 100644 index 000000000..5cc247900 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/tabbar/index.ts @@ -0,0 +1,2 @@ +export { default as LayoutTabbar } from './tabbar.vue'; +export * from './use-tabbar'; diff --git a/apps/vben5/packages/effects/layouts/src/basic/tabbar/tabbar.vue b/apps/vben5/packages/effects/layouts/src/basic/tabbar/tabbar.vue new file mode 100644 index 000000000..f932364ed --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/tabbar/tabbar.vue @@ -0,0 +1,72 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/basic/tabbar/use-tabbar.ts b/apps/vben5/packages/effects/layouts/src/basic/tabbar/use-tabbar.ts new file mode 100644 index 000000000..52b32dc46 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/basic/tabbar/use-tabbar.ts @@ -0,0 +1,220 @@ +import type { TabDefinition } from '@vben/types'; +import type { IContextMenuItem } from '@vben-core/tabs-ui'; +import type { RouteLocationNormalizedGeneric } from 'vue-router'; + +import { computed, ref, watch } from 'vue'; +import { useRoute, useRouter } from 'vue-router'; + +import { useContentMaximize, useTabs } from '@vben/hooks'; +import { + ArrowLeftToLine, + ArrowRightLeft, + ArrowRightToLine, + ExternalLink, + FoldHorizontal, + Fullscreen, + Minimize2, + Pin, + PinOff, + RotateCw, + X, +} from '@vben/icons'; +import { $t, useI18n } from '@vben/locales'; +import { useAccessStore, useTabbarStore } from '@vben/stores'; +import { filterTree } from '@vben/utils'; + +export function useTabbar() { + const router = useRouter(); + const route = useRoute(); + const accessStore = useAccessStore(); + const tabbarStore = useTabbarStore(); + const { contentIsMaximize, toggleMaximize } = useContentMaximize(); + const { + closeAllTabs, + closeCurrentTab, + closeLeftTabs, + closeOtherTabs, + closeRightTabs, + closeTabByKey, + getTabDisableState, + openTabInNewWindow, + refreshTab, + toggleTabPin, + } = useTabs(); + + const currentActive = computed(() => { + return route.fullPath; + }); + + const { locale } = useI18n(); + const currentTabs = ref(); + watch( + [ + () => tabbarStore.getTabs, + () => tabbarStore.updateTime, + () => locale.value, + ], + ([tabs]) => { + currentTabs.value = tabs.map((item) => wrapperTabLocale(item)); + }, + ); + + /** + * 初始化固定标签页 + */ + const initAffixTabs = () => { + const affixTabs = filterTree(router.getRoutes(), (route) => { + return !!route.meta?.affixTab; + }); + tabbarStore.setAffixTabs(affixTabs); + }; + + // 点击tab,跳转路由 + const handleClick = (key: string) => { + router.push(key); + }; + + // 关闭tab + const handleClose = async (key: string) => { + await closeTabByKey(key); + }; + + function wrapperTabLocale(tab: RouteLocationNormalizedGeneric) { + return { + ...tab, + meta: { + ...tab?.meta, + title: $t(tab?.meta?.title as string), + }, + }; + } + + watch( + () => accessStore.accessMenus, + () => { + initAffixTabs(); + }, + { immediate: true }, + ); + + watch( + () => route.path, + () => { + const meta = route.matched?.[route.matched.length - 1]?.meta; + tabbarStore.addTab({ + ...route, + meta: meta || route.meta, + }); + }, + { immediate: true }, + ); + + const createContextMenus = (tab: TabDefinition) => { + const { + disabledCloseAll, + disabledCloseCurrent, + disabledCloseLeft, + disabledCloseOther, + disabledCloseRight, + disabledRefresh, + } = getTabDisableState(tab); + + const affixTab = tab?.meta?.affixTab ?? false; + + const menus: IContextMenuItem[] = [ + { + disabled: disabledCloseCurrent, + handler: async () => { + await closeCurrentTab(tab); + }, + icon: X, + key: 'close', + text: $t('preferences.tabbar.contextMenu.close'), + }, + { + handler: async () => { + await toggleTabPin(tab); + }, + icon: affixTab ? PinOff : Pin, + key: 'affix', + text: affixTab + ? $t('preferences.tabbar.contextMenu.unpin') + : $t('preferences.tabbar.contextMenu.pin'), + }, + { + handler: async () => { + if (!contentIsMaximize.value) { + await router.push(tab.fullPath); + } + toggleMaximize(); + }, + icon: contentIsMaximize.value ? Minimize2 : Fullscreen, + key: contentIsMaximize.value ? 'restore-maximize' : 'maximize', + text: contentIsMaximize.value + ? $t('preferences.tabbar.contextMenu.restoreMaximize') + : $t('preferences.tabbar.contextMenu.maximize'), + }, + { + disabled: disabledRefresh, + handler: refreshTab, + icon: RotateCw, + key: 'reload', + text: $t('preferences.tabbar.contextMenu.reload'), + }, + { + handler: async () => { + await openTabInNewWindow(tab); + }, + icon: ExternalLink, + key: 'open-in-new-window', + separator: true, + text: $t('preferences.tabbar.contextMenu.openInNewWindow'), + }, + + { + disabled: disabledCloseLeft, + handler: async () => { + await closeLeftTabs(tab); + }, + icon: ArrowLeftToLine, + key: 'close-left', + text: $t('preferences.tabbar.contextMenu.closeLeft'), + }, + { + disabled: disabledCloseRight, + handler: async () => { + await closeRightTabs(tab); + }, + icon: ArrowRightToLine, + key: 'close-right', + separator: true, + text: $t('preferences.tabbar.contextMenu.closeRight'), + }, + { + disabled: disabledCloseOther, + handler: async () => { + await closeOtherTabs(tab); + }, + icon: FoldHorizontal, + key: 'close-other', + text: $t('preferences.tabbar.contextMenu.closeOther'), + }, + { + disabled: disabledCloseAll, + handler: closeAllTabs, + icon: ArrowRightLeft, + key: 'close-all', + text: $t('preferences.tabbar.contextMenu.closeAll'), + }, + ]; + return menus; + }; + + return { + createContextMenus, + currentActive, + currentTabs, + handleClick, + handleClose, + }; +} diff --git a/apps/vben5/packages/effects/layouts/src/iframe/iframe-router-view.vue b/apps/vben5/packages/effects/layouts/src/iframe/iframe-router-view.vue new file mode 100644 index 000000000..0e45ed966 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/iframe/iframe-router-view.vue @@ -0,0 +1,85 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/iframe/iframe-view.vue b/apps/vben5/packages/effects/layouts/src/iframe/iframe-view.vue new file mode 100644 index 000000000..7b8b46cb0 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/iframe/iframe-view.vue @@ -0,0 +1,3 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/iframe/index.ts b/apps/vben5/packages/effects/layouts/src/iframe/index.ts new file mode 100644 index 000000000..1b8c1313c --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/iframe/index.ts @@ -0,0 +1,2 @@ +export { default as IFrameRouterView } from './iframe-router-view.vue'; +export { default as IFrameView } from './iframe-view.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/index.ts b/apps/vben5/packages/effects/layouts/src/index.ts new file mode 100644 index 000000000..124a44a9b --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/index.ts @@ -0,0 +1,4 @@ +export * from './authentication'; +export * from './basic'; +export * from './iframe'; +export * from './widgets'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/breadcrumb.vue b/apps/vben5/packages/effects/layouts/src/widgets/breadcrumb.vue new file mode 100644 index 000000000..744aed268 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/breadcrumb.vue @@ -0,0 +1,72 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/check-updates/check-updates.vue b/apps/vben5/packages/effects/layouts/src/widgets/check-updates/check-updates.vue new file mode 100644 index 000000000..a0c9adeaf --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/check-updates/check-updates.vue @@ -0,0 +1,134 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/check-updates/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/check-updates/index.ts new file mode 100644 index 000000000..fc20c66d9 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/check-updates/index.ts @@ -0,0 +1 @@ +export { default as CheckUpdates } from './check-updates.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/color-toggle.vue b/apps/vben5/packages/effects/layouts/src/widgets/color-toggle.vue new file mode 100644 index 000000000..67cad31f6 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/color-toggle.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/global-search/global-search.vue b/apps/vben5/packages/effects/layouts/src/widgets/global-search/global-search.vue new file mode 100644 index 000000000..197251b8e --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/global-search/global-search.vue @@ -0,0 +1,156 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/global-search/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/global-search/index.ts new file mode 100644 index 000000000..cd490aeef --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/global-search/index.ts @@ -0,0 +1 @@ +export { default as GlobalSearch } from './global-search.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/global-search/search-panel.vue b/apps/vben5/packages/effects/layouts/src/widgets/global-search/search-panel.vue new file mode 100644 index 000000000..acbc2f19a --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/global-search/search-panel.vue @@ -0,0 +1,287 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/index.ts new file mode 100644 index 000000000..f6a4a7ba5 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/index.ts @@ -0,0 +1,11 @@ +export { default as Breadcrumb } from './breadcrumb.vue'; +export * from './check-updates'; +export { default as AuthenticationColorToggle } from './color-toggle.vue'; +export * from './global-search'; +export { default as LanguageToggle } from './language-toggle.vue'; +export { default as AuthenticationLayoutToggle } from './layout-toggle.vue'; +export * from './lock-screen'; +export * from './notification'; +export * from './preferences'; +export * from './theme-toggle'; +export * from './user-dropdown'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/language-toggle.vue b/apps/vben5/packages/effects/layouts/src/widgets/language-toggle.vue new file mode 100644 index 000000000..c8b612bba --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/language-toggle.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/layout-toggle.vue b/apps/vben5/packages/effects/layouts/src/widgets/layout-toggle.vue new file mode 100644 index 000000000..21823c3de --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/layout-toggle.vue @@ -0,0 +1,61 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/index.ts new file mode 100644 index 000000000..1609becbe --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/index.ts @@ -0,0 +1,2 @@ +export { default as LockScreen } from './lock-screen.vue'; +export { default as LockScreenModal } from './lock-screen-modal.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue b/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue new file mode 100644 index 000000000..a346d82ac --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue b/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue new file mode 100644 index 000000000..cce417dae --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue @@ -0,0 +1,155 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/notification/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/notification/index.ts new file mode 100644 index 000000000..e219b7108 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/notification/index.ts @@ -0,0 +1,3 @@ +export { default as Notification } from './notification.vue'; + +export type * from './types'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/notification/notification.vue b/apps/vben5/packages/effects/layouts/src/widgets/notification/notification.vue new file mode 100644 index 000000000..946fdfdf8 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/notification/notification.vue @@ -0,0 +1,186 @@ + + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/notification/types.ts b/apps/vben5/packages/effects/layouts/src/widgets/notification/types.ts new file mode 100644 index 000000000..57e179433 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/notification/types.ts @@ -0,0 +1,9 @@ +interface NotificationItem { + avatar: string; + date: string; + isRead?: boolean; + message: string; + title: string; +} + +export type { NotificationItem }; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/block.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/block.vue new file mode 100644 index 000000000..c8cc9e6a6 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/block.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/general/animation.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/general/animation.vue new file mode 100644 index 000000000..b27d074a3 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/general/animation.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue new file mode 100644 index 000000000..630882f4a --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/index.ts new file mode 100644 index 000000000..59595dc48 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/index.ts @@ -0,0 +1,19 @@ +export { default as Block } from './block.vue'; +export { default as Animation } from './general/animation.vue'; +export { default as General } from './general/general.vue'; +export { default as Breadcrumb } from './layout/breadcrumb.vue'; +export { default as Content } from './layout/content.vue'; +export { default as Copyright } from './layout/copyright.vue'; +export { default as Footer } from './layout/footer.vue'; +export { default as Header } from './layout/header.vue'; +export { default as Layout } from './layout/layout.vue'; +export { default as Navigation } from './layout/navigation.vue'; +export { default as Sidebar } from './layout/sidebar.vue'; +export { default as Tabbar } from './layout/tabbar.vue'; +export { default as Widget } from './layout/widget.vue'; +export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue'; +export { default as SwitchItem } from './switch-item.vue'; +export { default as BuiltinTheme } from './theme/builtin.vue'; +export { default as ColorMode } from './theme/color-mode.vue'; +export { default as Radius } from './theme/radius.vue'; +export { default as Theme } from './theme/theme.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue new file mode 100644 index 000000000..c480db08f --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/breadcrumb.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/breadcrumb.vue new file mode 100644 index 000000000..51f1a4078 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/breadcrumb.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/content.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/content.vue new file mode 100644 index 000000000..cd773771b --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/content.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/copyright.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/copyright.vue new file mode 100644 index 000000000..938902973 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/copyright.vue @@ -0,0 +1,44 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/footer.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/footer.vue new file mode 100644 index 000000000..8a77920e2 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/footer.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/header.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/header.vue new file mode 100644 index 000000000..cdb236cf3 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/header.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/layout.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/layout.vue new file mode 100644 index 000000000..ec5d3cfbf --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/layout.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/navigation.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/navigation.vue new file mode 100644 index 000000000..23acb1f28 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/navigation.vue @@ -0,0 +1,45 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue new file mode 100644 index 000000000..3c9efd699 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue new file mode 100644 index 000000000..5a3824a64 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue @@ -0,0 +1,68 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue new file mode 100644 index 000000000..82addb332 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue @@ -0,0 +1,71 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/number-field-item.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/number-field-item.vue new file mode 100644 index 000000000..00a532810 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/number-field-item.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/select-item.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/select-item.vue new file mode 100644 index 000000000..4e310156d --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/select-item.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/shortcut-keys/global.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/shortcut-keys/global.vue new file mode 100644 index 000000000..f71a1f6c5 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/shortcut-keys/global.vue @@ -0,0 +1,50 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/switch-item.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/switch-item.vue new file mode 100644 index 000000000..8ca7dd654 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/switch-item.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue new file mode 100644 index 000000000..480bc8c9b --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue @@ -0,0 +1,141 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/color-mode.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/color-mode.vue new file mode 100644 index 000000000..9a41d4ec3 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/color-mode.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/radius.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/radius.vue new file mode 100644 index 000000000..4201ed60d --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/radius.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/theme.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/theme.vue new file mode 100644 index 000000000..e509d6676 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/theme/theme.vue @@ -0,0 +1,83 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/toggle-item.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/toggle-item.vue new file mode 100644 index 000000000..df2c39fa0 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/blocks/toggle-item.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/content-compact.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/content-compact.vue new file mode 100644 index 000000000..a1bcefdc0 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/content-compact.vue @@ -0,0 +1,119 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/full-content.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/full-content.vue new file mode 100644 index 000000000..5cbcaca8a --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/full-content.vue @@ -0,0 +1,50 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/header-nav.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/header-nav.vue new file mode 100644 index 000000000..18158b3a7 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/header-nav.vue @@ -0,0 +1,119 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/index.ts new file mode 100644 index 000000000..c0c5a28e7 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/index.ts @@ -0,0 +1,10 @@ +import HeaderNav from './header-nav.vue'; + +export { default as ContentCompact } from './content-compact.vue'; +export { default as FullContent } from './full-content.vue'; +export { default as MixedNav } from './mixed-nav.vue'; +export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue'; +export { default as SidebarNav } from './sidebar-nav.vue'; + +const ContentWide = HeaderNav; +export { ContentWide, HeaderNav }; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/mixed-nav.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/mixed-nav.vue new file mode 100644 index 000000000..d194383f0 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/mixed-nav.vue @@ -0,0 +1,161 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/setting.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/setting.vue new file mode 100644 index 000000000..d824e11f4 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/setting.vue @@ -0,0 +1,12 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/sidebar-mixed-nav.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/sidebar-mixed-nav.vue new file mode 100644 index 000000000..8fc0ba406 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/sidebar-mixed-nav.vue @@ -0,0 +1,173 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/sidebar-nav.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/sidebar-nav.vue new file mode 100644 index 000000000..83ff3998d --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/icons/sidebar-nav.vue @@ -0,0 +1,153 @@ + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/preferences/index.ts new file mode 100644 index 000000000..0a7d32cce --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/index.ts @@ -0,0 +1,3 @@ +export { default as Preferences } from './preferences.vue'; +export { default as PreferencesButton } from './preferences-button.vue'; +export * from './use-open-preferences'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences-button.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences-button.vue new file mode 100644 index 000000000..29e110d31 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences-button.vue @@ -0,0 +1,19 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue new file mode 100644 index 000000000..77bf4d703 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue @@ -0,0 +1,423 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences.vue b/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences.vue new file mode 100644 index 000000000..9479adb2f --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/preferences.vue @@ -0,0 +1,71 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/preferences/use-open-preferences.ts b/apps/vben5/packages/effects/layouts/src/widgets/preferences/use-open-preferences.ts new file mode 100644 index 000000000..eb9d847cc --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/preferences/use-open-preferences.ts @@ -0,0 +1,16 @@ +import { ref } from 'vue'; + +const openPreferences = ref(false); + +function useOpenPreferences() { + function handleOpenPreference() { + openPreferences.value = true; + } + + return { + handleOpenPreference, + openPreferences, + }; +} + +export { useOpenPreferences }; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/index.ts new file mode 100644 index 000000000..0673d2188 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/index.ts @@ -0,0 +1 @@ +export { default as ThemeToggle } from './theme-toggle.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue b/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue new file mode 100644 index 000000000..77f330c58 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/theme-toggle.vue b/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/theme-toggle.vue new file mode 100644 index 000000000..4ddc6b8f6 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/theme-toggle/theme-toggle.vue @@ -0,0 +1,82 @@ + + diff --git a/apps/vben5/packages/effects/layouts/src/widgets/user-dropdown/index.ts b/apps/vben5/packages/effects/layouts/src/widgets/user-dropdown/index.ts new file mode 100644 index 000000000..86429e9e8 --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/user-dropdown/index.ts @@ -0,0 +1 @@ +export { default as UserDropdown } from './user-dropdown.vue'; diff --git a/apps/vben5/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue b/apps/vben5/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue new file mode 100644 index 000000000..475ba6f6f --- /dev/null +++ b/apps/vben5/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue @@ -0,0 +1,227 @@ + + + diff --git a/apps/vben5/packages/effects/layouts/tsconfig.json b/apps/vben5/packages/effects/layouts/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/effects/layouts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/plugins/README.md b/apps/vben5/packages/effects/plugins/README.md new file mode 100644 index 000000000..c394c9ced --- /dev/null +++ b/apps/vben5/packages/effects/plugins/README.md @@ -0,0 +1,28 @@ +# @vben/plugins + +该目录用于存放项目中集成的第三方库及其相关插件。每个插件都包含了可重用的逻辑、配置和组件,方便在项目中进行统一管理和调用。 + +## 注意 + +所有的第三方插件都必须以 `subpath` 形式引入,例: + +以 `echarts` 为例,引入方式如下: + +**packages.json** + +```json +"exports": { + "./echarts": { + "types": "./src/echarts/index.ts", + "default": "./src/echarts/index.ts" + } + } +``` + +**使用方式** + +```ts +import { useEcharts } from '@vben/plugins/echarts'; +``` + +这样做的好处是,应用可以自行选择是否使用插件,而不会因为插件的引入及副作用而导致打包体积增大,只引入需要的插件即可。 diff --git a/apps/vben5/packages/effects/plugins/package.json b/apps/vben5/packages/effects/plugins/package.json new file mode 100644 index 000000000..c59f28c62 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/package.json @@ -0,0 +1,42 @@ +{ + "name": "@vben/plugins", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/effects/plugins" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + "./echarts": { + "types": "./src/echarts/index.ts", + "default": "./src/echarts/index.ts" + }, + "./vxe-table": { + "types": "./src/vxe-table/index.ts", + "default": "./src/vxe-table/index.ts" + } + }, + "dependencies": { + "@vben-core/form-ui": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/icons": "workspace:*", + "@vben/locales": "workspace:*", + "@vben/preferences": "workspace:*", + "@vben/types": "workspace:*", + "@vben/utils": "workspace:*", + "@vueuse/core": "catalog:", + "echarts": "catalog:", + "vue": "catalog:", + "vxe-pc-ui": "catalog:", + "vxe-table": "catalog:" + } +} diff --git a/apps/vben5/packages/effects/plugins/src/echarts/echarts-ui.vue b/apps/vben5/packages/effects/plugins/src/echarts/echarts-ui.vue new file mode 100644 index 000000000..70d1f20b6 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/echarts/echarts-ui.vue @@ -0,0 +1,15 @@ + + + diff --git a/apps/vben5/packages/effects/plugins/src/echarts/echarts.ts b/apps/vben5/packages/effects/plugins/src/echarts/echarts.ts new file mode 100644 index 000000000..85323a429 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/echarts/echarts.ts @@ -0,0 +1,59 @@ +import type { + // 系列类型的定义后缀都为 SeriesOption + BarSeriesOption, + LineSeriesOption, +} from 'echarts/charts'; +import type { + DatasetComponentOption, + GridComponentOption, + // 组件类型的定义后缀都为 ComponentOption + TitleComponentOption, + TooltipComponentOption, +} from 'echarts/components'; +import type { ComposeOption } from 'echarts/core'; + +import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'; +import { + // 数据集组件 + DatasetComponent, + GridComponent, + LegendComponent, + TitleComponent, + ToolboxComponent, + TooltipComponent, + // 内置数据转换器组件 (filter, sort) + TransformComponent, +} from 'echarts/components'; +import * as echarts from 'echarts/core'; +import { LabelLayout, UniversalTransition } from 'echarts/features'; +import { CanvasRenderer } from 'echarts/renderers'; + +// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型 +export type ECOption = ComposeOption< + | BarSeriesOption + | DatasetComponentOption + | GridComponentOption + | LineSeriesOption + | TitleComponentOption + | TooltipComponentOption +>; + +// 注册必须的组件 +echarts.use([ + TitleComponent, + PieChart, + RadarChart, + TooltipComponent, + GridComponent, + DatasetComponent, + TransformComponent, + BarChart, + LineChart, + LabelLayout, + UniversalTransition, + CanvasRenderer, + LegendComponent, + ToolboxComponent, +]); + +export default echarts; diff --git a/apps/vben5/packages/effects/plugins/src/echarts/index.ts b/apps/vben5/packages/effects/plugins/src/echarts/index.ts new file mode 100644 index 000000000..80f36a13a --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/echarts/index.ts @@ -0,0 +1,3 @@ +export * from './echarts'; +export { default as EchartsUI } from './echarts-ui.vue'; +export * from './use-echarts'; diff --git a/apps/vben5/packages/effects/plugins/src/echarts/use-echarts.ts b/apps/vben5/packages/effects/plugins/src/echarts/use-echarts.ts new file mode 100644 index 000000000..bbe315903 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/echarts/use-echarts.ts @@ -0,0 +1,116 @@ +import type { EChartsOption } from 'echarts'; + +import type EchartsUI from './echarts-ui.vue'; + +import type { Ref } from 'vue'; +import { computed, nextTick, watch } from 'vue'; + +import { usePreferences } from '@vben/preferences'; + +import { + tryOnUnmounted, + useDebounceFn, + useResizeObserver, + useTimeoutFn, + useWindowSize, +} from '@vueuse/core'; + +import echarts from './echarts'; + +type EchartsUIType = typeof EchartsUI | undefined; + +type EchartsThemeType = 'dark' | 'light' | null; + +function useEcharts(chartRef: Ref) { + let chartInstance: echarts.ECharts | null = null; + let cacheOptions: EChartsOption = {}; + + const { isDark } = usePreferences(); + const { height, width } = useWindowSize(); + const resizeHandler: () => void = useDebounceFn(resize, 200); + + const getOptions = computed((): EChartsOption => { + if (!isDark.value) { + return {}; + } + + return { + backgroundColor: 'transparent', + }; + }); + + const initCharts = (t?: EchartsThemeType) => { + const el = chartRef?.value?.$el; + if (!el) { + return; + } + chartInstance = echarts.init(el, t || isDark.value ? 'dark' : null); + + return chartInstance; + }; + + const renderEcharts = (options: EChartsOption, clear = true) => { + cacheOptions = options; + const currentOptions = { + ...options, + ...getOptions.value, + }; + return new Promise((resolve) => { + if (chartRef.value?.offsetHeight === 0) { + useTimeoutFn(() => { + renderEcharts(currentOptions); + resolve(null); + }, 30); + return; + } + nextTick(() => { + useTimeoutFn(() => { + if (!chartInstance) { + const instance = initCharts(); + if (!instance) return; + } + clear && chartInstance?.clear(); + chartInstance?.setOption(currentOptions); + resolve(null); + }, 30); + }); + }); + }; + + function resize() { + chartInstance?.resize({ + animation: { + duration: 300, + easing: 'quadraticIn', + }, + }); + } + + watch([width, height], () => { + resizeHandler?.(); + }); + + useResizeObserver(chartRef as never, resizeHandler); + + watch(isDark, () => { + if (chartInstance) { + chartInstance.dispose(); + initCharts(); + renderEcharts(cacheOptions); + resize(); + } + }); + + tryOnUnmounted(() => { + // 销毁实例,释放资源 + chartInstance?.dispose(); + }); + return { + renderEcharts, + resize, + }; +} + +export { useEcharts }; + +export type { EchartsUIType }; diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/api.ts b/apps/vben5/packages/effects/plugins/src/vxe-table/api.ts new file mode 100644 index 000000000..365c90b7e --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/api.ts @@ -0,0 +1,115 @@ +import type { ExtendedFormApi } from '@vben-core/form-ui'; +import type { VxeGridInstance } from 'vxe-table'; + +import type { VxeGridProps } from './types'; + +import { toRaw } from 'vue'; + +import { Store } from '@vben-core/shared/store'; +import { + bindMethods, + isFunction, + mergeWithArrayOverride, + StateHandler, +} from '@vben-core/shared/utils'; + +function getDefaultState(): VxeGridProps { + return { + class: '', + gridClass: '', + gridOptions: {}, + gridEvents: {}, + formOptions: undefined, + }; +} + +export class VxeGridApi { + private isMounted = false; + + private stateHandler: StateHandler; + public formApi = {} as ExtendedFormApi; + + // private prevState: null | VxeGridProps = null; + public grid = {} as VxeGridInstance; + + public state: null | VxeGridProps = null; + + public store: Store; + + constructor(options: VxeGridProps = {}) { + const storeState = { ...options }; + + const defaultState = getDefaultState(); + this.store = new Store( + mergeWithArrayOverride(storeState, defaultState), + { + onUpdate: () => { + // this.prevState = this.state; + this.state = this.store.state; + }, + }, + ); + + this.state = this.store.state; + this.stateHandler = new StateHandler(); + bindMethods(this); + } + + mount(instance: null | VxeGridInstance, formApi: ExtendedFormApi) { + if (!this.isMounted && instance) { + this.grid = instance; + this.formApi = formApi; + this.stateHandler.setConditionTrue(); + this.isMounted = true; + } + } + + async query(params: Record = {}) { + try { + await this.grid.commitProxy('query', toRaw(params)); + } catch (error) { + console.error('Error occurred while querying:', error); + } + } + + async reload(params: Record = {}) { + try { + await this.grid.commitProxy('reload', toRaw(params)); + } catch (error) { + console.error('Error occurred while reloading:', error); + } + } + + setGridOptions(options: Partial) { + this.setState({ + gridOptions: options, + }); + } + + setLoading(isLoading: boolean) { + this.setState({ + gridOptions: { + loading: isLoading, + }, + }); + } + + setState( + stateOrFn: + | ((prev: VxeGridProps) => Partial) + | Partial, + ) { + if (isFunction(stateOrFn)) { + this.store.setState((prev) => { + return mergeWithArrayOverride(stateOrFn(prev), prev); + }); + } else { + this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev)); + } + } + + unmount() { + this.isMounted = false; + this.stateHandler.reset(); + } +} diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/extends.ts b/apps/vben5/packages/effects/plugins/src/vxe-table/extends.ts new file mode 100644 index 000000000..d20ecfa71 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/extends.ts @@ -0,0 +1,80 @@ +import type { Recordable } from '@vben/types'; +import type { VxeGridProps, VxeUIExport } from 'vxe-table'; + +import type { VxeGridApi } from './api'; + +import { formatDate, formatDateTime, isFunction } from '@vben/utils'; + +export function extendProxyOptions( + api: VxeGridApi, + options: VxeGridProps, + getFormValues: () => Recordable, +) { + [ + 'query', + 'querySuccess', + 'queryError', + 'queryAll', + 'queryAllSuccess', + 'queryAllError', + ].forEach((key) => { + extendProxyOption(key, api, options, getFormValues); + }); +} + +function extendProxyOption( + key: string, + api: VxeGridApi, + options: VxeGridProps, + getFormValues: () => Recordable, +) { + const { proxyConfig } = options; + const configFn = (proxyConfig?.ajax as Recordable)?.[key]; + if (!isFunction(configFn)) { + return options; + } + + const wrapperFn = async ( + params: Recordable, + customValues: Recordable, + ...args: Recordable[] + ) => { + const formValues = getFormValues(); + const data = await configFn( + params, + { + /** + * 开启toolbarConfig.refresh功能 + * 点击刷新按钮 这里的值为PointerEvent 会携带错误参数 + */ + ...(customValues instanceof PointerEvent ? {} : customValues), + ...formValues, + }, + ...args, + ); + return data; + }; + api.setState({ + gridOptions: { + proxyConfig: { + ajax: { + [key]: wrapperFn, + }, + }, + }, + }); +} + +export function extendsDefaultFormatter(vxeUI: VxeUIExport) { + vxeUI.formats.add('formatDate', { + tableCellFormatMethod({ cellValue }) { + return formatDate(cellValue); + }, + }); + + vxeUI.formats.add('formatDateTime', { + tableCellFormatMethod({ cellValue }) { + return formatDateTime(cellValue); + }, + }); +} diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/index.ts b/apps/vben5/packages/effects/plugins/src/vxe-table/index.ts new file mode 100644 index 000000000..4ae1c2ef5 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/index.ts @@ -0,0 +1,5 @@ +export { setupVbenVxeTable } from './init'; +export * from './use-vxe-grid'; +export { default as VbenVxeGrid } from './use-vxe-grid.vue'; + +export type { VxeGridListeners, VxeGridProps } from 'vxe-table'; diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/init.ts b/apps/vben5/packages/effects/plugins/src/vxe-table/init.ts new file mode 100644 index 000000000..4d08f45f8 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/init.ts @@ -0,0 +1,129 @@ +import type { SetupVxeTable } from './types'; + +import { defineComponent, watch } from 'vue'; + +import { usePreferences } from '@vben/preferences'; +import { useVbenForm } from '@vben-core/form-ui'; + +import { + VxeButton, + VxeCheckbox, + + // VxeFormGather, + // VxeForm, + // VxeFormItem, + VxeIcon, + VxeInput, + VxeLoading, + VxeModal, + VxePager, + // VxeList, + // VxeModal, + // VxeOptgroup, + // VxeOption, + // VxePulldown, + // VxeRadio, + // VxeRadioButton, + VxeRadioGroup, + VxeSelect, + VxeTooltip, + VxeUI, + VxeUpload, + // VxeSwitch, + // VxeTextarea, +} from 'vxe-pc-ui'; +import enUS from 'vxe-pc-ui/lib/language/en-US'; + +// 导入默认的语言 +import zhCN from 'vxe-pc-ui/lib/language/zh-CN'; +import { + VxeColgroup, + VxeColumn, + VxeGrid, + VxeTable, + VxeToolbar, +} from 'vxe-table'; + +import { extendsDefaultFormatter } from './extends'; + +// 是否加载过 +let isInit = false; + +// eslint-disable-next-line import/no-mutable-exports +export let useTableForm: typeof useVbenForm; + +// 部分组件,如果没注册,vxe-table 会报错,这里实际没用组件,只是为了不报错,同时可以减少打包体积 +const createVirtualComponent = (name = '') => { + return defineComponent({ + name, + }); +}; + +export function initVxeTable() { + if (isInit) { + return; + } + + VxeUI.component(VxeTable); + VxeUI.component(VxeColumn); + VxeUI.component(VxeColgroup); + VxeUI.component(VxeGrid); + VxeUI.component(VxeToolbar); + + VxeUI.component(VxeButton); + // VxeUI.component(VxeButtonGroup); + VxeUI.component(VxeCheckbox); + // VxeUI.component(VxeCheckboxGroup); + VxeUI.component(createVirtualComponent('VxeForm')); + // VxeUI.component(VxeFormGather); + // VxeUI.component(VxeFormItem); + VxeUI.component(VxeIcon); + VxeUI.component(VxeInput); + // VxeUI.component(VxeList); + VxeUI.component(VxeLoading); + VxeUI.component(VxeModal); + // VxeUI.component(VxeOptgroup); + // VxeUI.component(VxeOption); + VxeUI.component(VxePager); + // VxeUI.component(VxePulldown); + // VxeUI.component(VxeRadio); + // VxeUI.component(VxeRadioButton); + VxeUI.component(VxeRadioGroup); + VxeUI.component(VxeSelect); + // VxeUI.component(VxeSwitch); + // VxeUI.component(VxeTextarea); + VxeUI.component(VxeTooltip); + VxeUI.component(VxeUpload); + + isInit = true; +} + +export function setupVbenVxeTable(setupOptions: SetupVxeTable) { + const { configVxeTable, useVbenForm } = setupOptions; + + initVxeTable(); + useTableForm = useVbenForm; + + const preference = usePreferences(); + + const localMap = { + 'zh-CN': zhCN, + 'en-US': enUS, + }; + + watch( + [() => preference.theme.value, () => preference.locale.value], + ([theme, locale]) => { + VxeUI.setTheme(theme === 'dark' ? 'dark' : 'light'); + VxeUI.setI18n(locale, localMap[locale]); + VxeUI.setLanguage(locale); + }, + { + immediate: true, + }, + ); + + extendsDefaultFormatter(VxeUI); + + configVxeTable(VxeUI); +} diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/style.css b/apps/vben5/packages/effects/plugins/src/vxe-table/style.css new file mode 100644 index 000000000..2f8fa8434 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/style.css @@ -0,0 +1,104 @@ +:root .vxe-grid { + --vxe-ui-font-color: hsl(var(--foreground)); + --vxe-ui-font-primary-color: hsl(var(--primary)); + + /* --vxe-ui-font-lighten-color: #babdc0; + --vxe-ui-font-darken-color: #86898e; */ + --vxe-ui-font-disabled-color: hsl(var(--foreground) / 50%); + + /* base */ + --vxe-ui-base-popup-border-color: hsl(var(--border)); + --vxe-ui-input-disabled-color: hsl(var(--border) / 60%); + + /* --vxe-ui-base-popup-box-shadow: 0px 12px 30px 8px rgb(0 0 0 / 50%); */ + + /* layout */ + --vxe-ui-layout-background-color: hsl(var(--background)); + --vxe-ui-table-resizable-line-color: hsl(var(--heavy)); + + /* --vxe-ui-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px hsl(var(--accent)); + --vxe-ui-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px hsl(var(--accent)); */ + + /* input */ + --vxe-ui-input-border-color: hsl(var(--border)); + + /* --vxe-ui-input-placeholder-color: #8d9095; */ + + /* --vxe-ui-input-disabled-background-color: #262727; */ + + /* loading */ + --vxe-ui-loading-background-color: hsl(var(--overlay-content)); + + /* table */ + --vxe-ui-table-header-background-color: hsl(var(--accent)); + --vxe-ui-table-border-color: hsl(var(--border)); + --vxe-ui-table-row-hover-background-color: hsl(var(--accent-hover)); + --vxe-ui-table-row-striped-background-color: hsl(var(--accent) / 60%); + --vxe-ui-table-row-hover-striped-background-color: hsl(var(--accent)); + --vxe-ui-table-row-radio-checked-background-color: hsl(var(--accent)); + --vxe-ui-table-row-hover-radio-checked-background-color: hsl( + var(--accent-hover) + ); + --vxe-ui-table-row-checkbox-checked-background-color: hsl(var(--accent)); + --vxe-ui-table-row-hover-checkbox-checked-background-color: hsl( + var(--accent-hover) + ); + --vxe-ui-table-row-current-background-color: hsl(var(--accent)); + --vxe-ui-table-row-hover-current-background-color: hsl(var(--accent-hover)); + + /* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */ +} + +.vxe-pager { + .vxe-pager--prev-btn:not(.is--disabled):active, + .vxe-pager--next-btn:not(.is--disabled):active, + .vxe-pager--num-btn:not(.is--disabled):active, + .vxe-pager--jump-prev:not(.is--disabled):active, + .vxe-pager--jump-next:not(.is--disabled):active, + .vxe-pager--prev-btn:not(.is--disabled):focus, + .vxe-pager--next-btn:not(.is--disabled):focus, + .vxe-pager--num-btn:not(.is--disabled):focus, + .vxe-pager--jump-prev:not(.is--disabled):focus, + .vxe-pager--jump-next:not(.is--disabled):focus { + color: hsl(var(--accent-foreground)); + background-color: hsl(var(--accent)); + border: 1px solid hsl(var(--border)); + box-shadow: 0 0 0 1px hsl(var(--border)); + } + + .vxe-pager--wrapper { + display: flex; + align-items: center; + } + + .vxe-pager--sizes { + margin-right: auto; + } +} + +.vxe-pager--wrapper { + @apply justify-center md:justify-end; +} + +.vxe-tools--operate { + margin-right: 0.25rem; + margin-left: 0.75rem; +} + +.vxe-table-custom--checkbox-option:hover { + background: none !important; +} + +.vxe-toolbar { + padding: 0; +} + +.vxe-buttons--wrapper:not(:empty), +.vxe-tools--operate:not(:empty), +.vxe-tools--wrapper:not(:empty) { + padding: 0.6em 0; +} + +.vxe-tools--operate:not(:has(button)) { + margin-left: 0; +} diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/types.ts b/apps/vben5/packages/effects/plugins/src/vxe-table/types.ts new file mode 100644 index 000000000..e6559a283 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/types.ts @@ -0,0 +1,61 @@ +import type { ClassType, DeepPartial } from '@vben/types'; +import type { VbenFormProps } from '@vben-core/form-ui'; +import type { + VxeGridListeners, + VxeGridProps as VxeTableGridProps, + VxeUIExport, +} from 'vxe-table'; + +import type { VxeGridApi } from './api'; + +import type { Ref } from 'vue'; + +import { useVbenForm } from '@vben-core/form-ui'; + +export interface VxePaginationInfo { + currentPage: number; + pageSize: number; + total: number; +} + +export interface VxeGridProps { + /** + * 标题 + */ + tableTitle?: string; + /** + * 标题帮助 + */ + tableTitleHelp?: string; + /** + * 组件class + */ + class?: ClassType; + /** + * vxe-grid class + */ + gridClass?: ClassType; + /** + * vxe-grid 配置 + */ + gridOptions?: DeepPartial; + /** + * vxe-grid 事件 + */ + gridEvents?: DeepPartial; + /** + * 表单配置 + */ + formOptions?: VbenFormProps; +} + +export type ExtendedVxeGridApi = { + useStore: >( + selector?: (state: NoInfer) => T, + ) => Readonly>; +} & VxeGridApi; + +export interface SetupVxeTable { + configVxeTable: (ui: VxeUIExport) => void; + useVbenForm: typeof useVbenForm; +} diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts b/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts new file mode 100644 index 000000000..a309f5ac4 --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts @@ -0,0 +1,45 @@ +import type { ExtendedVxeGridApi, VxeGridProps } from './types'; + +import { defineComponent, h, onBeforeUnmount } from 'vue'; + +import { useStore } from '@vben-core/shared/store'; + +import { VxeGridApi } from './api'; +import VxeGrid from './use-vxe-grid.vue'; + +export function useVbenVxeGrid(options: VxeGridProps) { + // const IS_REACTIVE = isReactive(options); + const api = new VxeGridApi(options); + const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi; + extendedApi.useStore = (selector) => { + return useStore(api.store, selector); + }; + + const Grid = defineComponent( + (props: VxeGridProps, { attrs, slots }) => { + onBeforeUnmount(() => { + api.unmount(); + }); + api.setState({ ...props, ...attrs }); + return () => h(VxeGrid, { ...props, ...attrs, api: extendedApi }, slots); + }, + { + inheritAttrs: false, + name: 'VbenVxeGrid', + }, + ); + // Add reactivity support + // if (IS_REACTIVE) { + // watch( + // () => options, + // () => { + // api.setState(options); + // }, + // { immediate: true }, + // ); + // } + + return [Grid, extendedApi] as const; +} + +export type UseVbenVxeGrid = typeof useVbenVxeGrid; diff --git a/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue b/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue new file mode 100644 index 000000000..feca4cb2b --- /dev/null +++ b/apps/vben5/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue @@ -0,0 +1,354 @@ + + + diff --git a/apps/vben5/packages/effects/plugins/tsconfig.json b/apps/vben5/packages/effects/plugins/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/effects/plugins/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/effects/request/package.json b/apps/vben5/packages/effects/request/package.json new file mode 100644 index 000000000..b322e85d3 --- /dev/null +++ b/apps/vben5/packages/effects/request/package.json @@ -0,0 +1,30 @@ +{ + "name": "@vben/request", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/effects/request" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben/locales": "workspace:*", + "@vben/utils": "workspace:*", + "axios": "catalog:" + }, + "devDependencies": { + "axios-mock-adapter": "catalog:" + } +} diff --git a/apps/vben5/packages/effects/request/src/index.ts b/apps/vben5/packages/effects/request/src/index.ts new file mode 100644 index 000000000..fae1b058d --- /dev/null +++ b/apps/vben5/packages/effects/request/src/index.ts @@ -0,0 +1,2 @@ +export * from './request-client'; +export * from 'axios'; diff --git a/apps/vben5/packages/effects/request/src/request-client/index.ts b/apps/vben5/packages/effects/request/src/request-client/index.ts new file mode 100644 index 000000000..a44cd1565 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/index.ts @@ -0,0 +1,3 @@ +export * from './preset-interceptors'; +export * from './request-client'; +export type * from './types'; diff --git a/apps/vben5/packages/effects/request/src/request-client/modules/downloader.test.ts b/apps/vben5/packages/effects/request/src/request-client/modules/downloader.test.ts new file mode 100644 index 000000000..0e78b54f4 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/modules/downloader.test.ts @@ -0,0 +1,84 @@ +import type { AxiosRequestConfig } from 'axios'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { FileDownloader } from './downloader'; + +describe('fileDownloader', () => { + let fileDownloader: FileDownloader; + const mockAxiosInstance = { + get: vi.fn(), + } as any; + + beforeEach(() => { + fileDownloader = new FileDownloader(mockAxiosInstance); + }); + + it('should create an instance of FileDownloader', () => { + expect(fileDownloader).toBeInstanceOf(FileDownloader); + }); + + it('should download a file and return a Blob', async () => { + const url = 'https://example.com/file'; + const mockBlob = new Blob(['file content'], { type: 'text/plain' }); + const mockResponse: Blob = mockBlob; + + mockAxiosInstance.get.mockResolvedValueOnce(mockResponse); + + const result = await fileDownloader.download(url); + + expect(result).toBeInstanceOf(Blob); + expect(result).toEqual(mockBlob); + expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, { + responseType: 'blob', + }); + }); + + it('should merge provided config with default config', async () => { + const url = 'https://example.com/file'; + const mockBlob = new Blob(['file content'], { type: 'text/plain' }); + const mockResponse: Blob = mockBlob; + + mockAxiosInstance.get.mockResolvedValueOnce(mockResponse); + + const customConfig: AxiosRequestConfig = { + headers: { 'Custom-Header': 'value' }, + }; + + const result = await fileDownloader.download(url, customConfig); + expect(result).toBeInstanceOf(Blob); + expect(result).toEqual(mockBlob); + expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, { + ...customConfig, + responseType: 'blob', + }); + }); + + it('should handle errors gracefully', async () => { + const url = 'https://example.com/file'; + mockAxiosInstance.get.mockRejectedValueOnce(new Error('Network Error')); + await expect(fileDownloader.download(url)).rejects.toThrow('Network Error'); + }); + + it('should handle empty URL gracefully', async () => { + const url = ''; + mockAxiosInstance.get.mockRejectedValueOnce( + new Error('Request failed with status code 404'), + ); + + await expect(fileDownloader.download(url)).rejects.toThrow( + 'Request failed with status code 404', + ); + }); + + it('should handle null URL gracefully', async () => { + const url = null as unknown as string; + mockAxiosInstance.get.mockRejectedValueOnce( + new Error('Request failed with status code 404'), + ); + + await expect(fileDownloader.download(url)).rejects.toThrow( + 'Request failed with status code 404', + ); + }); +}); diff --git a/apps/vben5/packages/effects/request/src/request-client/modules/downloader.ts b/apps/vben5/packages/effects/request/src/request-client/modules/downloader.ts new file mode 100644 index 000000000..bf065d335 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/modules/downloader.ts @@ -0,0 +1,31 @@ +import type { AxiosRequestConfig } from 'axios'; + +import type { RequestClient } from '../request-client'; +import type { RequestResponse } from '../types'; + +class FileDownloader { + private client: RequestClient; + + constructor(client: RequestClient) { + this.client = client; + } + + public async download( + url: string, + config?: AxiosRequestConfig, + ): Promise> { + const finalConfig: AxiosRequestConfig = { + ...config, + responseType: 'blob', + }; + + const response = await this.client.get>( + url, + finalConfig, + ); + + return response; + } +} + +export { FileDownloader }; diff --git a/apps/vben5/packages/effects/request/src/request-client/modules/interceptor.ts b/apps/vben5/packages/effects/request/src/request-client/modules/interceptor.ts new file mode 100644 index 000000000..f6d2ad85b --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/modules/interceptor.ts @@ -0,0 +1,40 @@ +import type { AxiosInstance, AxiosResponse } from 'axios'; + +import type { + RequestInterceptorConfig, + ResponseInterceptorConfig, +} from '../types'; + +const defaultRequestInterceptorConfig: RequestInterceptorConfig = { + fulfilled: (response) => response, + rejected: (error) => Promise.reject(error), +}; + +const defaultResponseInterceptorConfig: ResponseInterceptorConfig = { + fulfilled: (response: AxiosResponse) => response, + rejected: (error) => Promise.reject(error), +}; + +class InterceptorManager { + private axiosInstance: AxiosInstance; + + constructor(instance: AxiosInstance) { + this.axiosInstance = instance; + } + + addRequestInterceptor({ + fulfilled, + rejected, + }: RequestInterceptorConfig = defaultRequestInterceptorConfig) { + this.axiosInstance.interceptors.request.use(fulfilled, rejected); + } + + addResponseInterceptor({ + fulfilled, + rejected, + }: ResponseInterceptorConfig = defaultResponseInterceptorConfig) { + this.axiosInstance.interceptors.response.use(fulfilled, rejected); + } +} + +export { InterceptorManager }; diff --git a/apps/vben5/packages/effects/request/src/request-client/modules/uploader.test.ts b/apps/vben5/packages/effects/request/src/request-client/modules/uploader.test.ts new file mode 100644 index 000000000..830665624 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/modules/uploader.test.ts @@ -0,0 +1,118 @@ +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { FileUploader } from './uploader'; + +describe('fileUploader', () => { + let fileUploader: FileUploader; + // Mock the AxiosInstance + const mockAxiosInstance = { + post: vi.fn(), + } as any; + + beforeEach(() => { + fileUploader = new FileUploader(mockAxiosInstance); + }); + + it('should create an instance of FileUploader', () => { + expect(fileUploader).toBeInstanceOf(FileUploader); + }); + + it('should upload a file and return the response', async () => { + const url = 'https://example.com/upload'; + const file = new File(['file content'], 'test.txt', { type: 'text/plain' }); + const mockResponse: AxiosResponse = { + config: {} as any, + data: { success: true }, + headers: {}, + status: 200, + statusText: 'OK', + }; + + ( + mockAxiosInstance.post as unknown as ReturnType + ).mockResolvedValueOnce(mockResponse); + + const result = await fileUploader.upload(url, { file }); + expect(result).toEqual(mockResponse); + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + url, + expect.any(FormData), + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + }); + + it('should merge provided config with default config', async () => { + const url = 'https://example.com/upload'; + const file = new File(['file content'], 'test.txt', { type: 'text/plain' }); + const mockResponse: AxiosResponse = { + config: {} as any, + data: { success: true }, + headers: {}, + status: 200, + statusText: 'OK', + }; + + ( + mockAxiosInstance.post as unknown as ReturnType + ).mockResolvedValueOnce(mockResponse); + + const customConfig: AxiosRequestConfig = { + headers: { 'Custom-Header': 'value' }, + }; + + const result = await fileUploader.upload(url, { file }, customConfig); + expect(result).toEqual(mockResponse); + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + url, + expect.any(FormData), + { + headers: { + 'Content-Type': 'multipart/form-data', + 'Custom-Header': 'value', + }, + }, + ); + }); + + it('should handle errors gracefully', async () => { + const url = 'https://example.com/upload'; + const file = new File(['file content'], 'test.txt', { type: 'text/plain' }); + ( + mockAxiosInstance.post as unknown as ReturnType + ).mockRejectedValueOnce(new Error('Network Error')); + + await expect(fileUploader.upload(url, { file })).rejects.toThrow( + 'Network Error', + ); + }); + + it('should handle empty URL gracefully', async () => { + const url = ''; + const file = new File(['file content'], 'test.txt', { type: 'text/plain' }); + ( + mockAxiosInstance.post as unknown as ReturnType + ).mockRejectedValueOnce(new Error('Request failed with status code 404')); + + await expect(fileUploader.upload(url, { file })).rejects.toThrow( + 'Request failed with status code 404', + ); + }); + + it('should handle null URL gracefully', async () => { + const url = null as unknown as string; + const file = new File(['file content'], 'test.txt', { type: 'text/plain' }); + ( + mockAxiosInstance.post as unknown as ReturnType + ).mockRejectedValueOnce(new Error('Request failed with status code 404')); + + await expect(fileUploader.upload(url, { file })).rejects.toThrow( + 'Request failed with status code 404', + ); + }); +}); diff --git a/apps/vben5/packages/effects/request/src/request-client/modules/uploader.ts b/apps/vben5/packages/effects/request/src/request-client/modules/uploader.ts new file mode 100644 index 000000000..e9a9f6024 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/modules/uploader.ts @@ -0,0 +1,35 @@ +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; + +import type { RequestClient } from '../request-client'; + +class FileUploader { + private client: RequestClient; + + constructor(client: RequestClient) { + this.client = client; + } + + public async upload( + url: string, + data: { file: Blob | File } & Record, + config?: AxiosRequestConfig, + ): Promise { + const formData = new FormData(); + + Object.entries(data).forEach(([key, value]) => { + formData.append(key, value); + }); + + const finalConfig: AxiosRequestConfig = { + ...config, + headers: { + 'Content-Type': 'multipart/form-data', + ...config?.headers, + }, + }; + + return this.client.post(url, formData, finalConfig); + } +} + +export { FileUploader }; diff --git a/apps/vben5/packages/effects/request/src/request-client/preset-interceptors.ts b/apps/vben5/packages/effects/request/src/request-client/preset-interceptors.ts new file mode 100644 index 000000000..2049eeaa1 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/preset-interceptors.ts @@ -0,0 +1,126 @@ +import type { RequestClient } from './request-client'; +import type { MakeErrorMessageFn, ResponseInterceptorConfig } from './types'; + +import { $t } from '@vben/locales'; + +import axios from 'axios'; + +export const authenticateResponseInterceptor = ({ + client, + doReAuthenticate, + doRefreshToken, + enableRefreshToken, + formatToken, +}: { + client: RequestClient; + doReAuthenticate: () => Promise; + doRefreshToken: () => Promise; + enableRefreshToken: boolean; + formatToken: (token: string) => null | string; +}): ResponseInterceptorConfig => { + return { + rejected: async (error) => { + const { config, response } = error; + // 如果不是 401 错误,直接抛出异常 + if (response?.status !== 401) { + throw error; + } + // 判断是否启用了 refreshToken 功能 + // 如果没有启用或者已经是重试请求了,直接跳转到重新登录 + if (!enableRefreshToken || config.__isRetryRequest) { + await doReAuthenticate(); + throw error; + } + // 如果正在刷新 token,则将请求加入队列,等待刷新完成 + if (client.isRefreshing) { + return new Promise((resolve) => { + client.refreshTokenQueue.push((newToken: string) => { + config.headers.Authorization = formatToken(newToken); + resolve(client.request(config.url, { ...config })); + }); + }); + } + + // 标记开始刷新 token + client.isRefreshing = true; + // 标记当前请求为重试请求,避免无限循环 + config.__isRetryRequest = true; + + try { + const newToken = await doRefreshToken(); + + // 处理队列中的请求 + client.refreshTokenQueue.forEach((callback) => callback(newToken)); + // 清空队列 + client.refreshTokenQueue = []; + + return client.request(error.config.url, { ...error.config }); + } catch (refreshError) { + // 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面) + client.refreshTokenQueue.forEach((callback) => callback('')); + client.refreshTokenQueue = []; + console.error('Refresh token failed, please login again.'); + await doReAuthenticate(); + + throw refreshError; + } finally { + client.isRefreshing = false; + } + }, + }; +}; + +export const errorMessageResponseInterceptor = ( + makeErrorMessage?: MakeErrorMessageFn, +): ResponseInterceptorConfig => { + return { + rejected: (error: any) => { + if (axios.isCancel(error)) { + return Promise.reject(error); + } + + const err: string = error?.toString?.() ?? ''; + let errMsg = ''; + if (err?.includes('Network Error')) { + errMsg = $t('ui.fallback.http.networkError'); + } else if (error?.message?.includes?.('timeout')) { + errMsg = $t('ui.fallback.http.requestTimeout'); + } + if (errMsg) { + makeErrorMessage?.(errMsg, error); + return Promise.reject(error); + } + + let errorMessage = ''; + const status = error?.response?.status; + + switch (status) { + case 400: { + errorMessage = $t('ui.fallback.http.badRequest'); + break; + } + case 401: { + errorMessage = $t('ui.fallback.http.unauthorized'); + break; + } + case 403: { + errorMessage = $t('ui.fallback.http.forbidden'); + break; + } + case 404: { + errorMessage = $t('ui.fallback.http.notFound'); + break; + } + case 408: { + errorMessage = $t('ui.fallback.http.requestTimeout'); + break; + } + default: { + errorMessage = $t('ui.fallback.http.internalServerError'); + } + } + makeErrorMessage?.(errorMessage, error); + return Promise.reject(error); + }, + }; +}; diff --git a/apps/vben5/packages/effects/request/src/request-client/request-client.test.ts b/apps/vben5/packages/effects/request/src/request-client/request-client.test.ts new file mode 100644 index 000000000..2d94525e2 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/request-client.test.ts @@ -0,0 +1,99 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { RequestClient } from './request-client'; + +describe('requestClient', () => { + let mock: MockAdapter; + let requestClient: RequestClient; + + beforeEach(() => { + mock = new MockAdapter(axios); + requestClient = new RequestClient(); + }); + + afterEach(() => { + mock.reset(); + }); + + it('should successfully make a GET request', async () => { + mock.onGet('test/url').reply(200, { data: 'response' }); + + const response = await requestClient.get('test/url'); + + expect(response.data).toEqual({ data: 'response' }); + }); + + it('should successfully make a POST request', async () => { + const postData = { key: 'value' }; + const mockData = { data: 'response' }; + mock.onPost('/test/post', postData).reply(200, mockData); + const response = await requestClient.post('/test/post', postData); + expect(response.data).toEqual(mockData); + }); + + it('should successfully make a PUT request', async () => { + const putData = { key: 'updatedValue' }; + const mockData = { data: 'updated response' }; + mock.onPut('/test/put', putData).reply(200, mockData); + const response = await requestClient.put('/test/put', putData); + expect(response.data).toEqual(mockData); + }); + + it('should successfully make a DELETE request', async () => { + const mockData = { data: 'delete response' }; + mock.onDelete('/test/delete').reply(200, mockData); + const response = await requestClient.delete('/test/delete'); + expect(response.data).toEqual(mockData); + }); + + it('should handle network errors', async () => { + mock.onGet('/test/error').networkError(); + try { + await requestClient.get('/test/error'); + expect(true).toBe(false); + } catch (error: any) { + expect(error.isAxiosError).toBe(true); + expect(error.message).toBe('Network Error'); + } + }); + + it('should handle timeout', async () => { + mock.onGet('/test/timeout').timeout(); + try { + await requestClient.get('/test/timeout'); + expect(true).toBe(false); + } catch (error: any) { + expect(error.isAxiosError).toBe(true); + expect(error.code).toBe('ECONNABORTED'); + } + }); + + it('should successfully upload a file', async () => { + const fileData = new Blob(['file contents'], { type: 'text/plain' }); + + mock.onPost('/test/upload').reply((config) => { + return config.data instanceof FormData && config.data.has('file') + ? [200, { data: 'file uploaded' }] + : [400, { error: 'Bad Request' }]; + }); + + const response = await requestClient.upload('/test/upload', { + file: fileData, + }); + expect(response.data).toEqual({ data: 'file uploaded' }); + }); + + it('should successfully download a file as a blob', async () => { + const mockFileContent = new Blob(['mock file content'], { + type: 'text/plain', + }); + + mock.onGet('/test/download').reply(200, mockFileContent); + + const res = await requestClient.download('/test/download'); + + expect(res.data).toBeInstanceOf(Blob); + }); +}); diff --git a/apps/vben5/packages/effects/request/src/request-client/request-client.ts b/apps/vben5/packages/effects/request/src/request-client/request-client.ts new file mode 100644 index 000000000..f1c4c08d1 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/request-client.ts @@ -0,0 +1,116 @@ +import type { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + CreateAxiosDefaults, +} from 'axios'; + +import { bindMethods, merge } from '@vben/utils'; + +import axios from 'axios'; + +import { FileDownloader } from './modules/downloader'; +import { InterceptorManager } from './modules/interceptor'; +import { FileUploader } from './modules/uploader'; +import { type RequestClientOptions } from './types'; + +class RequestClient { + private readonly instance: AxiosInstance; + + public addRequestInterceptor: InterceptorManager['addRequestInterceptor']; + public addResponseInterceptor: InterceptorManager['addResponseInterceptor']; + + public download: FileDownloader['download']; + // 是否正在刷新token + public isRefreshing = false; + // 刷新token队列 + public refreshTokenQueue: ((token: string) => void)[] = []; + public upload: FileUploader['upload']; + + /** + * 构造函数,用于创建Axios实例 + * @param options - Axios请求配置,可选 + */ + constructor(options: RequestClientOptions = {}) { + // 合并默认配置和传入的配置 + const defaultConfig: CreateAxiosDefaults = { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + // 默认超时时间 + timeout: 10_000, + }; + const { ...axiosConfig } = options; + const requestConfig = merge(axiosConfig, defaultConfig); + this.instance = axios.create(requestConfig); + + bindMethods(this); + + // 实例化拦截器管理器 + const interceptorManager = new InterceptorManager(this.instance); + this.addRequestInterceptor = + interceptorManager.addRequestInterceptor.bind(interceptorManager); + this.addResponseInterceptor = + interceptorManager.addResponseInterceptor.bind(interceptorManager); + + // 实例化文件上传器 + const fileUploader = new FileUploader(this); + this.upload = fileUploader.upload.bind(fileUploader); + // 实例化文件下载器 + const fileDownloader = new FileDownloader(this); + this.download = fileDownloader.download.bind(fileDownloader); + } + + /** + * DELETE请求方法 + */ + public delete(url: string, config?: AxiosRequestConfig): Promise { + return this.request(url, { ...config, method: 'DELETE' }); + } + + /** + * GET请求方法 + */ + public get(url: string, config?: AxiosRequestConfig): Promise { + return this.request(url, { ...config, method: 'GET' }); + } + + /** + * POST请求方法 + */ + public post( + url: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise { + return this.request(url, { ...config, data, method: 'POST' }); + } + + /** + * PUT请求方法 + */ + public put( + url: string, + data?: any, + config?: AxiosRequestConfig, + ): Promise { + return this.request(url, { ...config, data, method: 'PUT' }); + } + + /** + * 通用的请求方法 + */ + public async request(url: string, config: AxiosRequestConfig): Promise { + try { + const response: AxiosResponse = await this.instance({ + url, + ...config, + }); + return response as T; + } catch (error: any) { + throw error.response ? error.response.data : error; + } + } +} + +export { RequestClient }; diff --git a/apps/vben5/packages/effects/request/src/request-client/types.ts b/apps/vben5/packages/effects/request/src/request-client/types.ts new file mode 100644 index 000000000..b85dded44 --- /dev/null +++ b/apps/vben5/packages/effects/request/src/request-client/types.ts @@ -0,0 +1,53 @@ +import type { + AxiosResponse, + CreateAxiosDefaults, + InternalAxiosRequestConfig, +} from 'axios'; + +type RequestResponse = AxiosResponse; + +type RequestContentType = + | 'application/json;charset=utf-8' + | 'application/octet-stream;charset=utf-8' + | 'application/x-www-form-urlencoded;charset=utf-8' + | 'multipart/form-data;charset=utf-8'; + +type RequestClientOptions = CreateAxiosDefaults; + +interface RequestInterceptorConfig { + fulfilled?: ( + config: InternalAxiosRequestConfig, + ) => + | InternalAxiosRequestConfig + | Promise>; + rejected?: (error: any) => any; +} + +interface ResponseInterceptorConfig { + fulfilled?: ( + response: AxiosResponse, + ) => AxiosResponse | Promise; + rejected?: (error: any) => any; +} + +type MakeErrorMessageFn = (message: string, error: any) => void; + +interface HttpResponse { + /** + * 0 表示成功 其他表示失败 + * 0 means success, others means fail + */ + code: number; + data: T; + message: string; +} + +export type { + HttpResponse, + MakeErrorMessageFn, + RequestClientOptions, + RequestContentType, + RequestInterceptorConfig, + RequestResponse, + ResponseInterceptorConfig, +}; diff --git a/apps/vben5/packages/effects/request/tsconfig.json b/apps/vben5/packages/effects/request/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/effects/request/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/icons/README.md b/apps/vben5/packages/icons/README.md new file mode 100644 index 000000000..bc50d25e0 --- /dev/null +++ b/apps/vben5/packages/icons/README.md @@ -0,0 +1,19 @@ +# @vben/icons + +用于多个 `app` 公用的图标文件,继承了 `@vben-core/icons` 的所有能力。业务上有通用图标可以放在这里。 + +## 用法 + +### 添加依赖 + +```bash +# 进入目标应用目录,例如 apps/xxxx-app +# cd apps/xxxx-app +pnpm add @vben/icons +``` + +### 使用 + +```ts +import { X } from '@vben/icons'; +``` diff --git a/apps/vben5/packages/icons/package.json b/apps/vben5/packages/icons/package.json new file mode 100644 index 000000000..59e46656e --- /dev/null +++ b/apps/vben5/packages/icons/package.json @@ -0,0 +1,22 @@ +{ + "name": "@vben/icons", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/icons" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/icons": "workspace:*" + } +} diff --git a/apps/vben5/packages/icons/src/iconify/index.ts b/apps/vben5/packages/icons/src/iconify/index.ts new file mode 100644 index 000000000..a0985ac15 --- /dev/null +++ b/apps/vben5/packages/icons/src/iconify/index.ts @@ -0,0 +1,13 @@ +import { createIconifyIcon } from '@vben-core/icons'; + +export * from '@vben-core/icons'; + +export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc'); + +export const MdiWechat = createIconifyIcon('mdi:wechat'); + +export const MdiGithub = createIconifyIcon('mdi:github'); + +export const MdiGoogle = createIconifyIcon('mdi:google'); + +export const MdiQqchat = createIconifyIcon('mdi:qqchat'); diff --git a/apps/vben5/packages/icons/src/icons/empty-icon.vue b/apps/vben5/packages/icons/src/icons/empty-icon.vue new file mode 100644 index 000000000..444a765ed --- /dev/null +++ b/apps/vben5/packages/icons/src/icons/empty-icon.vue @@ -0,0 +1,27 @@ + diff --git a/apps/vben5/packages/icons/src/index.ts b/apps/vben5/packages/icons/src/index.ts new file mode 100644 index 000000000..babbf6646 --- /dev/null +++ b/apps/vben5/packages/icons/src/index.ts @@ -0,0 +1,3 @@ +export * from './iconify/index.js'; +export { default as EmptyIcon } from './icons/empty-icon.vue'; +export * from './svg/index.js'; diff --git a/apps/vben5/packages/icons/src/svg/icons/antdv-logo.svg b/apps/vben5/packages/icons/src/svg/icons/antdv-logo.svg new file mode 100644 index 000000000..dbfcee7e5 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/antdv-logo.svg @@ -0,0 +1,29 @@ + + + + Vue + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/vben5/packages/icons/src/svg/icons/avatar-1.svg b/apps/vben5/packages/icons/src/svg/icons/avatar-1.svg new file mode 100644 index 000000000..b05970de3 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/avatar-1.svg @@ -0,0 +1 @@ +Asset 15 diff --git a/apps/vben5/packages/icons/src/svg/icons/avatar-2.svg b/apps/vben5/packages/icons/src/svg/icons/avatar-2.svg new file mode 100644 index 000000000..8e17b0b52 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/avatar-2.svg @@ -0,0 +1 @@ +Asset 16 diff --git a/apps/vben5/packages/icons/src/svg/icons/avatar-3.svg b/apps/vben5/packages/icons/src/svg/icons/avatar-3.svg new file mode 100644 index 000000000..ca79178ce --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/avatar-3.svg @@ -0,0 +1 @@ +Asset 17 diff --git a/apps/vben5/packages/icons/src/svg/icons/avatar-4.svg b/apps/vben5/packages/icons/src/svg/icons/avatar-4.svg new file mode 100644 index 000000000..d9138dfd5 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/avatar-4.svg @@ -0,0 +1 @@ +Asset 120 diff --git a/apps/vben5/packages/icons/src/svg/icons/bell.svg b/apps/vben5/packages/icons/src/svg/icons/bell.svg new file mode 100644 index 000000000..90c34e1bf --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/bell.svg @@ -0,0 +1 @@ +Asset 510 diff --git a/apps/vben5/packages/icons/src/svg/icons/cake.svg b/apps/vben5/packages/icons/src/svg/icons/cake.svg new file mode 100644 index 000000000..43901fbc5 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/cake.svg @@ -0,0 +1 @@ +Asset 480% diff --git a/apps/vben5/packages/icons/src/svg/icons/card.svg b/apps/vben5/packages/icons/src/svg/icons/card.svg new file mode 100644 index 000000000..40ff3e3f7 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/card.svg @@ -0,0 +1 @@ + diff --git a/apps/vben5/packages/icons/src/svg/icons/download.svg b/apps/vben5/packages/icons/src/svg/icons/download.svg new file mode 100644 index 000000000..af9ff15c7 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/icons/download.svg @@ -0,0 +1 @@ + diff --git a/apps/vben5/packages/icons/src/svg/index.ts b/apps/vben5/packages/icons/src/svg/index.ts new file mode 100644 index 000000000..3b5cb9953 --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/index.ts @@ -0,0 +1,25 @@ +import { createIconifyIcon } from '@vben-core/icons'; + +import './load.js'; + +const SvgAvatar1Icon = createIconifyIcon('svg:avatar-1'); +const SvgAvatar2Icon = createIconifyIcon('svg:avatar-2'); +const SvgAvatar3Icon = createIconifyIcon('svg:avatar-3'); +const SvgAvatar4Icon = createIconifyIcon('svg:avatar-4'); +const SvgDownloadIcon = createIconifyIcon('svg:download'); +const SvgCardIcon = createIconifyIcon('svg:card'); +const SvgBellIcon = createIconifyIcon('svg:bell'); +const SvgCakeIcon = createIconifyIcon('svg:cake'); +const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo'); + +export { + SvgAntdvLogoIcon, + SvgAvatar1Icon, + SvgAvatar2Icon, + SvgAvatar3Icon, + SvgAvatar4Icon, + SvgBellIcon, + SvgCakeIcon, + SvgCardIcon, + SvgDownloadIcon, +}; diff --git a/apps/vben5/packages/icons/src/svg/load.ts b/apps/vben5/packages/icons/src/svg/load.ts new file mode 100644 index 000000000..25f4261ef --- /dev/null +++ b/apps/vben5/packages/icons/src/svg/load.ts @@ -0,0 +1,61 @@ +import type { IconifyIconStructure } from '@vben-core/icons'; + +import { addIcon } from '@vben-core/icons'; + +let loaded = false; +if (!loaded) { + loadSvgIcons(); + loaded = true; +} + +function parseSvg(svgData: string): IconifyIconStructure { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml'); + const svgElement = xmlDoc.documentElement; + + const svgContent = [...svgElement.childNodes] + .filter((node) => node.nodeType === Node.ELEMENT_NODE) + .map((node) => new XMLSerializer().serializeToString(node)) + .join(''); + + const viewBoxValue = svgElement.getAttribute('viewBox') || ''; + const [left, top, width, height] = viewBoxValue.split(' ').map((val) => { + const num = Number(val); + return Number.isNaN(num) ? undefined : num; + }); + + return { + body: svgContent, + height, + left, + top, + width, + }; +} + +/** + * 自定义的svg图片转化为组件 + * @example ./svg/avatar.svg + * + */ +async function loadSvgIcons() { + const svgEagers = import.meta.glob('./icons/**', { + eager: true, + query: '?raw', + }); + + await Promise.all( + Object.entries(svgEagers).map((svg) => { + const [key, body] = svg as [string, { default: string } | string]; + + // ./icons/xxxx.svg => xxxxxx + const start = key.lastIndexOf('/') + 1; + const end = key.lastIndexOf('.'); + const iconName = key.slice(start, end); + + return addIcon(`svg:${iconName}`, { + ...parseSvg(typeof body === 'object' ? body.default : body), + }); + }), + ); +} diff --git a/apps/vben5/packages/icons/tsconfig.json b/apps/vben5/packages/icons/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/icons/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/locales/package.json b/apps/vben5/packages/locales/package.json new file mode 100644 index 000000000..9a55c352f --- /dev/null +++ b/apps/vben5/packages/locales/package.json @@ -0,0 +1,28 @@ +{ + "name": "@vben/locales", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/locales" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@intlify/core-base": "catalog:", + "@vben-core/composables": "workspace:*", + "vue": "catalog:", + "vue-i18n": "catalog:" + } +} diff --git a/apps/vben5/packages/locales/src/i18n.ts b/apps/vben5/packages/locales/src/i18n.ts new file mode 100644 index 000000000..99259c3c7 --- /dev/null +++ b/apps/vben5/packages/locales/src/i18n.ts @@ -0,0 +1,146 @@ +import type { Locale } from 'vue-i18n'; + +import type { + ImportLocaleFn, + LoadMessageFn, + LocaleSetupOptions, + SupportedLanguagesType, +} from './typing'; + +import { type App, unref } from 'vue'; +import { createI18n } from 'vue-i18n'; + +import { useSimpleLocale } from '@vben-core/composables'; + +const i18n = createI18n({ + globalInjection: true, + legacy: false, + locale: '', + messages: {}, +}); + +const modules = import.meta.glob('./langs/**/*.json'); + +const { setSimpleLocale } = useSimpleLocale(); + +const localesMap = loadLocalesMapFromDir( + /\.\/langs\/([^/]+)\/(.*)\.json$/, + modules, +); +let loadMessages: LoadMessageFn; + +/** + * Load locale modules + * @param modules + */ +function loadLocalesMap(modules: Record Promise>) { + const localesMap: Record = {}; + + for (const [path, loadLocale] of Object.entries(modules)) { + const key = path.match(/([\w-]*)\.(json)/)?.[1]; + if (key) { + localesMap[key] = loadLocale as ImportLocaleFn; + } + } + return localesMap; +} + +/** + * Load locale modules with directory structure + * @param regexp - Regular expression to match language and file names + * @param modules - The modules object containing paths and import functions + * @returns A map of locales to their corresponding import functions + */ +function loadLocalesMapFromDir( + regexp: RegExp, + modules: Record Promise>, +): Record { + const localesRaw: Record Promise>> = {}; + const localesMap: Record = {}; + + // Iterate over the modules to extract language and file names + for (const path in modules) { + const match = path.match(regexp); + if (match) { + const [_, locale, fileName] = match; + if (locale && fileName) { + if (!localesRaw[locale]) { + localesRaw[locale] = {}; + } + if (modules[path]) { + localesRaw[locale][fileName] = modules[path]; + } + } + } + } + + // Convert raw locale data into async import functions + for (const [locale, files] of Object.entries(localesRaw)) { + localesMap[locale] = async () => { + const messages: Record = {}; + for (const [fileName, importFn] of Object.entries(files)) { + messages[fileName] = ((await importFn()) as any)?.default; + } + return { default: messages }; + }; + } + + return localesMap; +} + +/** + * Set i18n language + * @param locale + */ +function setI18nLanguage(locale: Locale) { + i18n.global.locale.value = locale; + + document?.querySelector('html')?.setAttribute('lang', locale); +} + +async function setupI18n(app: App, options: LocaleSetupOptions = {}) { + const { defaultLocale = 'zh-CN' } = options; + // app可以自行扩展一些第三方库和组件库的国际化 + loadMessages = options.loadMessages || (async () => ({})); + app.use(i18n); + await loadLocaleMessages(defaultLocale); + + // 在控制台打印警告 + i18n.global.setMissingHandler((locale, key) => { + if (options.missingWarn && key.includes('.')) { + console.warn( + `[intlify] Not found '${key}' key in '${locale}' locale messages.`, + ); + } + }); +} + +/** + * Load locale messages + * @param lang + */ +async function loadLocaleMessages(lang: SupportedLanguagesType) { + if (unref(i18n.global.locale) === lang) { + return setI18nLanguage(lang); + } + setSimpleLocale(lang); + + const message = await localesMap[lang]?.(); + + if (message?.default) { + i18n.global.setLocaleMessage(lang, message.default); + } + + const mergeMessage = await loadMessages(lang); + i18n.global.mergeLocaleMessage(lang, mergeMessage); + + return setI18nLanguage(lang); +} + +export { + i18n, + loadLocaleMessages, + loadLocalesMap, + loadLocalesMapFromDir, + setupI18n, +}; diff --git a/apps/vben5/packages/locales/src/index.ts b/apps/vben5/packages/locales/src/index.ts new file mode 100644 index 000000000..eb2251adb --- /dev/null +++ b/apps/vben5/packages/locales/src/index.ts @@ -0,0 +1,28 @@ +import { + i18n, + loadLocaleMessages, + loadLocalesMap, + loadLocalesMapFromDir, + setupI18n, +} from './i18n'; + +const $t = i18n.global.t; + +export { + $t, + i18n, + loadLocaleMessages, + loadLocalesMap, + loadLocalesMapFromDir, + setupI18n, +}; +export { + type ImportLocaleFn, + type LocaleSetupOptions, + type SupportedLanguagesType, +} from './typing'; +export type { CompileError } from '@intlify/core-base'; + +export { useI18n } from 'vue-i18n'; + +export type { Locale } from 'vue-i18n'; diff --git a/apps/vben5/packages/locales/src/langs/en-US/authentication.json b/apps/vben5/packages/locales/src/langs/en-US/authentication.json new file mode 100644 index 000000000..4155f871b --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/en-US/authentication.json @@ -0,0 +1,56 @@ +{ + "welcomeBack": "Welcome Back", + "pageTitle": "Plug-and-play Admin system", + "pageDesc": "Efficient, versatile frontend template", + "loginSuccess": "Login Successful", + "loginSuccessDesc": "Welcome Back", + "loginSubtitle": "Enter your account details to manage your projects", + "selectAccount": "Quick Select Account", + "username": "Username", + "password": "Password", + "usernameTip": "Please enter username", + "passwordErrorTip": "Password is incorrect", + "passwordTip": "Please enter password", + "verifyRequiredTip": "Please complete the verification first", + "rememberMe": "Remember Me", + "createAnAccount": "Create an Account", + "createAccount": "Create Account", + "alreadyHaveAccount": "Already have an account?", + "accountTip": "Don't have an account?", + "signUp": "Sign Up", + "signUpSubtitle": "Make managing your applications simple and fun", + "confirmPassword": "Confirm Password", + "confirmPasswordTip": "The passwords do not match", + "agree": "I agree to", + "privacyPolicy": "Privacy-policy", + "terms": "Terms", + "agreeTip": "Please agree to the Privacy Policy and Terms", + "goToLogin": "Login instead", + "passwordStrength": "Use 8 or more characters with a mix of letters, numbers & symbols", + "forgetPassword": "Forget Password?", + "forgetPasswordSubtitle": "Enter your email and we'll send you instructions to reset your password", + "emailTip": "Please enter email", + "emailValidErrorTip": "The email format you entered is incorrect", + "sendResetLink": "Send Reset Link", + "email": "Email", + "qrcodeSubtitle": "Scan the QR code with your phone to login", + "qrcodePrompt": "Click 'Confirm' after scanning to complete login", + "qrcodeLogin": "QR Code Login", + "codeSubtitle": "Enter your phone number to start managing your project", + "code": "Security code", + "codeTip": "Security code is required", + "mobile": "Mobile", + "mobileLogin": "Mobile Login", + "mobileTip": "Please enter mobile number", + "mobileErrortip": "The phone number format is incorrect", + "sendCode": "Get Security code", + "sendText": "Resend in {0}s", + "thirdPartyLogin": "Or continue with", + "loginAgainTitle": "Please Log In Again", + "loginAgainSubTitle": "Your login session has expired. Please log in again to continue.", + "layout": { + "center": "Align Center", + "alignLeft": "Align Left", + "alignRight": "Align Right" + } +} diff --git a/apps/vben5/packages/locales/src/langs/en-US/common.json b/apps/vben5/packages/locales/src/langs/en-US/common.json new file mode 100644 index 000000000..23308b776 --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/en-US/common.json @@ -0,0 +1,13 @@ +{ + "back": "Back", + "backToHome": "Back To Home", + "login": "Login", + "logout": "Logout", + "prompt": "Prompt", + "cancel": "Cancel", + "confirm": "Confirm", + "noData": "No Data", + "refresh": "Refresh", + "loadingMenu": "Loading Menu", + "query": "Search" +} diff --git a/apps/vben5/packages/locales/src/langs/en-US/preferences.json b/apps/vben5/packages/locales/src/langs/en-US/preferences.json new file mode 100644 index 000000000..8ba3cce62 --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/en-US/preferences.json @@ -0,0 +1,169 @@ +{ + "title": "Preferences", + "subtitle": "Customize Preferences & Preview in Real Time", + "resetTip": "Data has changed, click to reset", + "resetTitle": "Reset Preferences", + "resetSuccess": "Preferences reset successfully", + "appearance": "Appearance", + "layout": "Layout", + "content": "Content", + "other": "Other", + "wide": "Wide", + "compact": "Fixed", + "followSystem": "Follow System", + "vertical": "Vertical", + "verticalTip": "Side vertical menu mode", + "horizontal": "Horizontal", + "horizontalTip": "Horizontal menu mode, all menus displayed at the top", + "twoColumn": "Two Column", + "twoColumnTip": "Vertical Two Column Menu Mode", + "mixedMenu": "Mixed Menu", + "mixedMenuTip": "Vertical & Horizontal Menu Co-exists", + "fullContent": "Full Content", + "fullContentTip": "Only display content body, hide all menus", + "normal": "Normal", + "plain": "Plain", + "rounded": "Rounded", + "copyPreferences": "Copy Preferences", + "copyPreferencesSuccessTitle": "Copy successful", + "copyPreferencesSuccess": "Copy successful, please override in `src/preferences.ts` under app", + "clearAndLogout": "Clear Cache & Logout", + "mode": "Mode", + "general": "General", + "language": "Language", + "dynamicTitle": "Dynamic Title", + "watermark": "Watermark", + "checkUpdates": "Periodic update check", + "position": { + "title": "Preferences Postion", + "header": "Header", + "auto": "Auto", + "fixed": "Fixed" + }, + "sidebar": { + "title": "Sidebar", + "width": "Width", + "visible": "Show Sidebar", + "collapsed": "Collpase Menu", + "collapsedShowTitle": "Show Menu Title" + }, + "tabbar": { + "title": "Tabbar", + "enable": "Enable Tab Bar", + "icon": "Show Tabbar Icon", + "showMore": "Show More Button", + "showMaximize": "Show Maximize Button", + "persist": "Persist Tabs", + "draggable": "Enable Draggable Sort", + "styleType": { + "title": "Tabs Style", + "chrome": "Chrome", + "card": "Card", + "plain": "Plain", + "brisk": "Brisk" + }, + "contextMenu": { + "reload": "Reload", + "close": "Close", + "pin": "Pin", + "unpin": "Unpin", + "closeLeft": "Close Left Tabs", + "closeRight": "Close Right Tabs", + "closeOther": "Close Other Tabs", + "closeAll": "Close All Tabs", + "openInNewWindow": "Open in New Window", + "maximize": "Maximize", + "restoreMaximize": "Restore" + } + }, + "navigationMenu": { + "title": "Navigation Menu", + "style": "Navigation Menu Style", + "accordion": "Sidebar Accordion Menu", + "split": "Navigation Menu Separation", + "splitTip": "When enabled, the sidebar displays the top bar's submenu" + }, + "breadcrumb": { + "title": "Breadcrumb", + "home": "Show Home Button", + "enable": "Enable Breadcrumb", + "icon": "Show Breadcrumb Icon", + "background": "background", + "style": "Breadcrumb Style", + "hideOnlyOne": "Hidden when only one" + }, + "animation": { + "title": "Animation", + "loading": "Page Loading", + "transition": "Page Transition", + "progress": "Page Progress" + }, + "theme": { + "title": "Theme", + "radius": "Radius", + "light": "Light", + "dark": "Dark", + "darkSidebar": "Semi Dark Sidebar", + "darkHeader": "Semi Dark Header", + "weakMode": "Weak Mode", + "grayMode": "Gray Mode", + "builtin": { + "title": "Built-in", + "default": "Default", + "violet": "Violet", + "pink": "Pink", + "rose": "Rose", + "skyBlue": "Sky Blue", + "deepBlue": "Deep Blue", + "green": "Green", + "deepGreen": "Deep Green", + "orange": "Orange", + "yellow": "Yellow", + "zinc": "Zinc", + "neutral": "Neutral", + "slate": "Slate", + "gray": "Gray", + "custom": "Custom" + } + }, + "header": { + "title": "Header", + "visible": "Show Header", + "modeStatic": "Static", + "modeFixed": "Fixed", + "modeAuto": "Auto hide & Show", + "modeAutoScroll": "Scroll to Hide & Show" + }, + "footer": { + "title": "Footer", + "visible": "Show Footer", + "fixed": "Fixed at Bottom" + }, + "copyright": { + "title": "Copyright", + "enable": "Enable Copyright", + "companyName": "Company Name", + "companySiteLink": "Company Site Link", + "date": "Date", + "icp": "ICP License Number", + "icpLink": "ICP Site Link" + }, + "shortcutKeys": { + "title": "Shortcut Keys", + "global": "Global", + "search": "Global Search", + "logout": "Logout", + "preferences": "Preferences" + }, + "widget": { + "title": "Widget", + "globalSearch": "Enable Global Search", + "fullscreen": "Enable Fullscreen", + "themeToggle": "Enable Theme Toggle", + "languageToggle": "Enable Language Toggle", + "notification": "Enable Notification", + "sidebarToggle": "Enable Sidebar Toggle", + "lockScreen": "Enable Lock Screen", + "refresh": "Enable Refresh" + } +} diff --git a/apps/vben5/packages/locales/src/langs/en-US/ui.json b/apps/vben5/packages/locales/src/langs/en-US/ui.json new file mode 100644 index 000000000..dc99c2f7c --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/en-US/ui.json @@ -0,0 +1,77 @@ +{ + "formRules": { + "required": "Please enter {0}", + "selectRequired": "Please select {0}" + }, + "placeholder": { + "input": "Please enter", + "select": "Please select" + }, + "captcha": { + "title": "Please complete the security verification", + "sliderSuccessText": "Passed", + "sliderDefaultText": "Slider and drag", + "alt": "Supports img tag src attribute value", + "sliderRotateDefaultTip": "Click picture to refresh", + "sliderRotateFailTip": "Validation failed", + "sliderRotateSuccessTip": "Validation successful, time {0} seconds", + "refreshAriaLabel": "Refresh captcha", + "confirmAriaLabel": "Confirm selection", + "confirm": "Confirm", + "pointAriaLabel": "Click point", + "clickInOrder": "Please click in order" + }, + "fallback": { + "pageNotFound": "Oops! Page Not Found", + "pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.", + "forbidden": "Oops! Access Denied", + "forbiddenDesc": "Sorry, but you don't have permission to access this page.", + "internalError": "Oops! Something Went Wrong", + "internalErrorDesc": "Sorry, but the server encountered an error.", + "offline": "Offline Page", + "offlineError": "Oops! Network Error", + "offlineErrorDesc": "Sorry, can't connect to the internet. Check your connection.", + "comingSoon": "Coming Soon", + "http": { + "requestTimeout": "The request timed out. Please try again later.", + "networkError": "A network error occurred. Please check your internet connection and try again.", + "badRequest": "Bad Request. Please check your input and try again.", + "unauthorized": "Unauthorized. Please log in to continue.", + "forbidden": "Forbidden. You do not have permission to access this resource.", + "notFound": "Not Found. The requested resource could not be found.", + "internalServerError": "Internal Server Error. Something went wrong on our end. Please try again later." + } + }, + "widgets": { + "document": "Document", + "qa": "Q&A", + "setting": "Settings", + "logoutTip": "Do you want to logout?", + "viewAll": "View All Messages", + "notifications": "Notifications", + "markAllAsRead": "Make All as Read", + "clearNotifications": "Clear", + "checkUpdatesTitle": "New Version Available", + "checkUpdatesDescription": "Click to refresh and get the latest version", + "search": { + "title": "Search", + "searchNavigate": "Search Navigation", + "select": "Select", + "navigate": "Navigate", + "close": "Close", + "noResults": "No Search Results Found", + "noRecent": "No Search History", + "recent": "Search History" + }, + "lockScreen": { + "title": "Lock Screen", + "screenButton": "Locking", + "password": "Password", + "placeholder": "Please enter password", + "unlock": "Click to unlock", + "errorPasswordTip": "Password error, please re-enter", + "backToLogin": "Back to login", + "entry": "Enter the system" + } + } +} diff --git a/apps/vben5/packages/locales/src/langs/zh-CN/authentication.json b/apps/vben5/packages/locales/src/langs/zh-CN/authentication.json new file mode 100644 index 000000000..4533db874 --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/zh-CN/authentication.json @@ -0,0 +1,56 @@ +{ + "welcomeBack": "欢迎回来", + "pageTitle": "开箱即用的大型中后台管理系统", + "pageDesc": "工程化、高性能、跨组件库的前端模版", + "loginSuccess": "登录成功", + "loginSuccessDesc": "欢迎回来", + "loginSubtitle": "请输入您的帐户信息以开始管理您的项目", + "selectAccount": "快速选择账号", + "username": "账号", + "password": "密码", + "usernameTip": "请输入用户名", + "passwordTip": "请输入密码", + "verifyRequiredTip": "请先完成验证", + "passwordErrorTip": "密码错误", + "rememberMe": "记住账号", + "createAnAccount": "创建一个账号", + "createAccount": "创建账号", + "alreadyHaveAccount": "已经有账号了?", + "accountTip": "还没有账号?", + "signUp": "注册", + "signUpSubtitle": "让您的应用程序管理变得简单而有趣", + "confirmPassword": "确认密码", + "confirmPasswordTip": "两次输入的密码不一致", + "agree": "我同意", + "privacyPolicy": "隐私政策", + "terms": "条款", + "agreeTip": "请同意隐私政策和条款", + "goToLogin": "去登录", + "passwordStrength": "使用 8 个或更多字符,混合字母、数字和符号", + "forgetPassword": "忘记密码?", + "forgetPasswordSubtitle": "输入您的电子邮件,我们将向您发送重置密码的连接", + "emailTip": "请输入邮箱", + "emailValidErrorTip": "你输入的邮箱格式不正确", + "sendResetLink": "发送重置链接", + "email": "邮箱", + "qrcodeSubtitle": "请用手机扫描二维码登录", + "qrcodePrompt": "扫码后点击 '确认',即可完成登录", + "qrcodeLogin": "扫码登录", + "codeSubtitle": "请输入您的手机号码以开始管理您的项目", + "code": "验证码", + "codeTip": "请输入验证码", + "mobile": "手机号码", + "mobileTip": "请输入手机号", + "mobileErrortip": "手机号码格式错误", + "mobileLogin": "手机号登录", + "sendCode": "获取验证码", + "sendText": "{0}秒后重新获取", + "thirdPartyLogin": "其他登录方式", + "loginAgainTitle": "重新登录", + "loginAgainSubTitle": "您的登录状态已过期,请重新登录以继续。", + "layout": { + "center": "居中", + "alignLeft": "居左", + "alignRight": "居右" + } +} diff --git a/apps/vben5/packages/locales/src/langs/zh-CN/common.json b/apps/vben5/packages/locales/src/langs/zh-CN/common.json new file mode 100644 index 000000000..fdfc6e49e --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/zh-CN/common.json @@ -0,0 +1,13 @@ +{ + "back": "返回", + "backToHome": "返回首页", + "login": "登录", + "logout": "退出登录", + "prompt": "提示", + "cancel": "取消", + "confirm": "确认", + "noData": "暂无数据", + "refresh": "刷新", + "loadingMenu": "加载菜单中", + "query": "查询" +} diff --git a/apps/vben5/packages/locales/src/langs/zh-CN/preferences.json b/apps/vben5/packages/locales/src/langs/zh-CN/preferences.json new file mode 100644 index 000000000..8f5038ee8 --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/zh-CN/preferences.json @@ -0,0 +1,169 @@ +{ + "title": "偏好设置", + "subtitle": "自定义偏好设置 & 实时预览", + "resetTitle": "重置偏好设置", + "resetTip": "数据有变化,点击可进行重置", + "resetSuccess": "重置偏好设置成功", + "appearance": "外观", + "layout": "布局", + "content": "内容", + "other": "其它", + "wide": "流式", + "compact": "定宽", + "followSystem": "跟随系统", + "vertical": "垂直", + "verticalTip": "侧边垂直菜单模式", + "horizontal": "水平", + "horizontalTip": "水平菜单模式,菜单全部显示在顶部", + "twoColumn": "双列菜单", + "twoColumnTip": "垂直双列菜单模式", + "mixedMenu": "混合菜单", + "mixedMenuTip": "垂直水平菜单共存", + "fullContent": "内容全屏", + "fullContentTip": "不显示任何菜单,只显示内容主体", + "normal": "常规", + "plain": "朴素", + "rounded": "圆润", + "copyPreferences": "复制偏好设置", + "copyPreferencesSuccessTitle": "复制成功", + "copyPreferencesSuccess": "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖", + "clearAndLogout": "清空缓存 & 退出登录", + "mode": "模式", + "general": "通用", + "language": "语言", + "dynamicTitle": "动态标题", + "watermark": "水印", + "checkUpdates": "定时检查更新", + "position": { + "title": "偏好设置位置", + "header": "顶栏", + "auto": "自动", + "fixed": "固定" + }, + "sidebar": { + "title": "侧边栏", + "width": "宽度", + "visible": "显示侧边栏", + "collapsed": "折叠菜单", + "collapsedShowTitle": "折叠显示菜单名" + }, + "tabbar": { + "title": "标签栏", + "enable": "启用标签栏", + "icon": "显示标签栏图标", + "showMore": "显示更多按钮", + "showMaximize": "显示最大化按钮", + "persist": "持久化标签页", + "draggable": "启动拖拽排序", + "styleType": { + "title": "标签页风格", + "chrome": "谷歌", + "card": "卡片", + "plain": "朴素", + "brisk": "轻快" + }, + "contextMenu": { + "reload": "重新加载", + "close": "关闭", + "pin": "固定", + "unpin": "取消固定", + "closeLeft": "关闭左侧标签页", + "closeRight": "关闭右侧标签页", + "closeOther": "关闭其它标签页", + "closeAll": "关闭全部标签页", + "openInNewWindow": "在新窗口打开", + "maximize": "最大化", + "restoreMaximize": "还原" + } + }, + "navigationMenu": { + "title": "导航菜单", + "style": "导航菜单风格", + "accordion": "侧边导航菜单手风琴模式", + "split": "导航菜单分离", + "splitTip": "开启时,侧边栏显示顶栏对应菜单的子菜单" + }, + "breadcrumb": { + "title": "面包屑导航", + "enable": "开启面包屑导航", + "icon": "显示面包屑图标", + "home": "显示首页按钮", + "style": "面包屑风格", + "hideOnlyOne": "仅有一个时隐藏", + "background": "背景" + }, + "animation": { + "title": "动画", + "loading": "页面切换 Loading", + "transition": "页面切换动画", + "progress": "页面切换进度条" + }, + "theme": { + "title": "主题", + "radius": "圆角", + "light": "浅色", + "dark": "深色", + "darkSidebar": "深色侧边栏", + "darkHeader": "深色顶栏", + "weakMode": "色弱模式", + "grayMode": "灰色模式", + "builtin": { + "title": "内置主题", + "default": "默认", + "violet": "紫罗兰", + "pink": "樱花粉", + "rose": "玫瑰红", + "skyBlue": "天蓝色", + "deepBlue": "深蓝色", + "green": "浅绿色", + "deepGreen": "深绿色", + "orange": "橙黄色", + "yellow": "柠檬黄", + "zinc": "锌色灰", + "neutral": "中性色", + "slate": "石板灰", + "gray": "中灰色", + "custom": "自定义" + } + }, + "header": { + "title": "顶栏", + "modeStatic": "静止", + "modeFixed": "固定", + "modeAuto": "自动隐藏和显示", + "modeAutoScroll": "滚动隐藏和显示", + "visible": "显示顶栏" + }, + "footer": { + "title": "底栏", + "visible": "显示底栏", + "fixed": "固定在底部" + }, + "copyright": { + "title": "版权", + "enable": "启用版权", + "companyName": "公司名", + "companySiteLink": "公司主页", + "date": "日期", + "icp": "ICP 备案号", + "icpLink": "ICP 网站链接" + }, + "shortcutKeys": { + "title": "快捷键", + "global": "全局", + "search": "全局搜索", + "logout": "退出登录", + "preferences": "偏好设置" + }, + "widget": { + "title": "小部件", + "globalSearch": "启用全局搜索", + "fullscreen": "启用全屏", + "themeToggle": "启用主题切换", + "languageToggle": "启用语言切换", + "notification": "启用通知", + "sidebarToggle": "启用侧边栏切换", + "lockScreen": "启用锁屏", + "refresh": "启用刷新" + } +} diff --git a/apps/vben5/packages/locales/src/langs/zh-CN/ui.json b/apps/vben5/packages/locales/src/langs/zh-CN/ui.json new file mode 100644 index 000000000..e84c0ba5c --- /dev/null +++ b/apps/vben5/packages/locales/src/langs/zh-CN/ui.json @@ -0,0 +1,77 @@ +{ + "formRules": { + "required": "请输入{0}", + "selectRequired": "请选择{0}" + }, + "placeholder": { + "input": "请输入", + "select": "请选择" + }, + "captcha": { + "title": "请完成安全验证", + "sliderSuccessText": "验证通过", + "sliderDefaultText": "请按住滑块拖动", + "sliderRotateDefaultTip": "点击图片可刷新", + "sliderRotateFailTip": "验证失败", + "sliderRotateSuccessTip": "验证成功,耗时{0}秒", + "alt": "支持img标签src属性值", + "refreshAriaLabel": "刷新验证码", + "confirmAriaLabel": "确认选择", + "confirm": "确认", + "pointAriaLabel": "点击点", + "clickInOrder": "请依次点击" + }, + "fallback": { + "pageNotFound": "哎呀!未找到页面", + "pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。", + "forbidden": "哎呀!访问被拒绝", + "forbiddenDesc": "抱歉,您没有权限访问此页面。", + "internalError": "哎呀!出错了", + "internalErrorDesc": "抱歉,服务器遇到错误。", + "offline": "离线页面", + "offlineError": "哎呀!网络错误", + "offlineErrorDesc": "抱歉,无法连接到互联网,请检查您的网络连接并重试。", + "comingSoon": "即将推出", + "http": { + "requestTimeout": "请求超时,请稍后再试。", + "networkError": "网络异常,请检查您的网络连接后重试。", + "badRequest": "请求错误。请检查您的输入并重试。", + "unauthorized": "登录认证过期,请重新登录后继续。", + "forbidden": "禁止访问, 您没有权限访问此资源。", + "notFound": "未找到, 请求的资源不存在。", + "internalServerError": "内部服务器错误,请稍后再试。" + } + }, + "widgets": { + "document": "文档", + "qa": "问题 & 帮助", + "setting": "设置", + "logoutTip": "是否退出登录?", + "viewAll": "查看所有消息", + "notifications": "通知", + "markAllAsRead": "全部标记为已读", + "clearNotifications": "清空", + "checkUpdatesTitle": "新版本可用", + "checkUpdatesDescription": "点击刷新以获取最新版本", + "search": { + "title": "搜索", + "searchNavigate": "搜索导航菜单", + "select": "选择", + "navigate": "导航", + "close": "关闭", + "noResults": "未找到搜索结果", + "noRecent": "没有搜索历史", + "recent": "搜索历史" + }, + "lockScreen": { + "title": "锁定屏幕", + "screenButton": "锁定", + "password": "密码", + "placeholder": "请输入锁屏密码", + "unlock": "点击解锁", + "errorPasswordTip": "密码错误,请重新输入", + "backToLogin": "返回登录", + "entry": "进入系统" + } + } +} diff --git a/apps/vben5/packages/locales/src/typing.ts b/apps/vben5/packages/locales/src/typing.ts new file mode 100644 index 000000000..fdebac2be --- /dev/null +++ b/apps/vben5/packages/locales/src/typing.ts @@ -0,0 +1,25 @@ +export type SupportedLanguagesType = 'en-US' | 'zh-CN'; + +export type ImportLocaleFn = () => Promise<{ default: Record }>; + +export type LoadMessageFn = ( + lang: SupportedLanguagesType, +) => Promise | undefined>; + +export interface LocaleSetupOptions { + /** + * Default language + * @default zh-CN + */ + defaultLocale?: SupportedLanguagesType; + /** + * Load message function + * @param lang + * @returns + */ + loadMessages?: LoadMessageFn; + /** + * Whether to warn when the key is not found + */ + missingWarn?: boolean; +} diff --git a/apps/vben5/packages/locales/tsconfig.json b/apps/vben5/packages/locales/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/locales/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/preferences/package.json b/apps/vben5/packages/preferences/package.json new file mode 100644 index 000000000..7e9c67a99 --- /dev/null +++ b/apps/vben5/packages/preferences/package.json @@ -0,0 +1,26 @@ +{ + "name": "@vben/preferences", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/preferences" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/preferences": "workspace:*", + "@vben-core/typings": "workspace:*" + } +} diff --git a/apps/vben5/packages/preferences/src/index.ts b/apps/vben5/packages/preferences/src/index.ts new file mode 100644 index 000000000..75fab432b --- /dev/null +++ b/apps/vben5/packages/preferences/src/index.ts @@ -0,0 +1,17 @@ +import type { Preferences } from '@vben-core/preferences'; +import type { DeepPartial } from '@vben-core/typings'; + +/** + * 如果你想所有的app都使用相同的默认偏好设置,你可以在这里定义 + * 而不是去修改 @vben-core/preferences 中的默认偏好设置 + * @param preferences + * @returns + */ + +function defineOverridesPreferences(preferences: DeepPartial) { + return preferences; +} + +export { defineOverridesPreferences }; + +export * from '@vben-core/preferences'; diff --git a/apps/vben5/packages/preferences/tsconfig.json b/apps/vben5/packages/preferences/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/preferences/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/stores/package.json b/apps/vben5/packages/stores/package.json new file mode 100644 index 000000000..999a1e286 --- /dev/null +++ b/apps/vben5/packages/stores/package.json @@ -0,0 +1,30 @@ +{ + "name": "@vben/stores", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/stores" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "pinia": "catalog:", + "pinia-plugin-persistedstate": "catalog:", + "vue": "catalog:", + "vue-router": "catalog:" + } +} diff --git a/apps/vben5/packages/stores/shim-pinia.d.ts b/apps/vben5/packages/stores/shim-pinia.d.ts new file mode 100644 index 000000000..69be75daf --- /dev/null +++ b/apps/vben5/packages/stores/shim-pinia.d.ts @@ -0,0 +1,9 @@ +// https://github.com/vuejs/pinia/issues/2098 +declare module 'pinia' { + export function acceptHMRUpdate( + initialUseStore: any | StoreDefinition, + hot: any, + ): (newModule: any) => any; +} + +export {}; diff --git a/apps/vben5/packages/stores/src/index.ts b/apps/vben5/packages/stores/src/index.ts new file mode 100644 index 000000000..41b3662cf --- /dev/null +++ b/apps/vben5/packages/stores/src/index.ts @@ -0,0 +1,3 @@ +export * from './modules'; +export * from './setup'; +export { defineStore, storeToRefs } from 'pinia'; diff --git a/apps/vben5/packages/stores/src/modules/access.test.ts b/apps/vben5/packages/stores/src/modules/access.test.ts new file mode 100644 index 000000000..211e25607 --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/access.test.ts @@ -0,0 +1,46 @@ +import { createPinia, setActivePinia } from 'pinia'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { useAccessStore } from './access'; + +describe('useAccessStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it('updates accessMenus state', () => { + const store = useAccessStore(); + expect(store.accessMenus).toEqual([]); + store.setAccessMenus([{ name: 'Dashboard', path: '/dashboard' }]); + expect(store.accessMenus).toEqual([ + { name: 'Dashboard', path: '/dashboard' }, + ]); + }); + + it('updates accessToken state correctly', () => { + const store = useAccessStore(); + expect(store.accessToken).toBeNull(); // 初始状态 + store.setAccessToken('abc123'); + expect(store.accessToken).toBe('abc123'); + }); + + it('returns the correct accessToken', () => { + const store = useAccessStore(); + store.setAccessToken('xyz789'); + expect(store.accessToken).toBe('xyz789'); + }); + + // 测试设置空的访问菜单列表 + it('handles empty accessMenus correctly', () => { + const store = useAccessStore(); + store.setAccessMenus([]); + expect(store.accessMenus).toEqual([]); + }); + + // 测试设置空的访问路由列表 + it('handles empty accessRoutes correctly', () => { + const store = useAccessStore(); + store.setAccessRoutes([]); + expect(store.accessRoutes).toEqual([]); + }); +}); diff --git a/apps/vben5/packages/stores/src/modules/access.ts b/apps/vben5/packages/stores/src/modules/access.ts new file mode 100644 index 000000000..ee8641a7f --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/access.ts @@ -0,0 +1,85 @@ +import type { MenuRecordRaw } from '@vben-core/typings'; +import type { RouteRecordRaw } from 'vue-router'; + +import { acceptHMRUpdate, defineStore } from 'pinia'; + +type AccessToken = null | string; + +interface AccessState { + /** + * 权限码 + */ + accessCodes: string[]; + /** + * 可访问的菜单列表 + */ + accessMenus: MenuRecordRaw[]; + /** + * 可访问的路由列表 + */ + accessRoutes: RouteRecordRaw[]; + /** + * 登录 accessToken + */ + accessToken: AccessToken; + /** + * 是否已经检查过权限 + */ + isAccessChecked: boolean; + /** + * 登录是否过期 + */ + loginExpired: boolean; + /** + * 登录 accessToken + */ + refreshToken: AccessToken; +} + +/** + * @zh_CN 访问权限相关 + */ +export const useAccessStore = defineStore('core-access', { + actions: { + setAccessCodes(codes: string[]) { + this.accessCodes = codes; + }, + setAccessMenus(menus: MenuRecordRaw[]) { + this.accessMenus = menus; + }, + setAccessRoutes(routes: RouteRecordRaw[]) { + this.accessRoutes = routes; + }, + setAccessToken(token: AccessToken) { + this.accessToken = token; + }, + setIsAccessChecked(isAccessChecked: boolean) { + this.isAccessChecked = isAccessChecked; + }, + setLoginExpired(loginExpired: boolean) { + this.loginExpired = loginExpired; + }, + setRefreshToken(token: AccessToken) { + this.refreshToken = token; + }, + }, + persist: { + // 持久化 + pick: ['accessToken', 'refreshToken', 'accessCodes'], + }, + state: (): AccessState => ({ + accessCodes: [], + accessMenus: [], + accessRoutes: [], + accessToken: null, + isAccessChecked: false, + loginExpired: false, + refreshToken: null, + }), +}); + +// 解决热更新问题 +const hot = import.meta.hot; +if (hot) { + hot.accept(acceptHMRUpdate(useAccessStore, hot)); +} diff --git a/apps/vben5/packages/stores/src/modules/index.ts b/apps/vben5/packages/stores/src/modules/index.ts new file mode 100644 index 000000000..7941cbea9 --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/index.ts @@ -0,0 +1,4 @@ +export * from './access'; +export * from './lock'; +export * from './tabbar'; +export * from './user'; diff --git a/apps/vben5/packages/stores/src/modules/lock.test.ts b/apps/vben5/packages/stores/src/modules/lock.test.ts new file mode 100644 index 000000000..d8dbe554b --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/lock.test.ts @@ -0,0 +1,31 @@ +import { createPinia, setActivePinia } from 'pinia'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { useLockStore } from './lock'; + +describe('useLockStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it('should initialize with correct default state', () => { + const store = useLockStore(); + expect(store.isLockScreen).toBe(false); + expect(store.lockScreenPassword).toBeUndefined(); + }); + + it('should lock screen with a password', () => { + const store = useLockStore(); + store.lockScreen('1234'); + expect(store.isLockScreen).toBe(true); + expect(store.lockScreenPassword).toBe('1234'); + }); + + it('should unlock screen and clear password', () => { + const store = useLockStore(); + store.lockScreen('1234'); + store.unlockScreen(); + expect(store.isLockScreen).toBe(false); + expect(store.lockScreenPassword).toBeUndefined(); + }); +}); diff --git a/apps/vben5/packages/stores/src/modules/lock.ts b/apps/vben5/packages/stores/src/modules/lock.ts new file mode 100644 index 000000000..76a96006b --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/lock.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia'; + +interface AppState { + /** + * 是否锁屏状态 + */ + isLockScreen: boolean; + /** + * 锁屏密码 + */ + lockScreenPassword?: string; +} + +export const useLockStore = defineStore('core-lock', { + actions: { + lockScreen(password: string) { + this.isLockScreen = true; + this.lockScreenPassword = password; + }, + + unlockScreen() { + this.isLockScreen = false; + this.lockScreenPassword = undefined; + }, + }, + persist: { + pick: ['isLockScreen', 'lockScreenPassword'], + }, + state: (): AppState => ({ + isLockScreen: false, + lockScreenPassword: undefined, + }), +}); diff --git a/apps/vben5/packages/stores/src/modules/tabbar.test.ts b/apps/vben5/packages/stores/src/modules/tabbar.test.ts new file mode 100644 index 000000000..d74615ca0 --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/tabbar.test.ts @@ -0,0 +1,295 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +import { createPinia, setActivePinia } from 'pinia'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useTabbarStore } from './tabbar'; + +describe('useAccessStore', () => { + const router = createRouter({ + history: createWebHistory(), + routes: [], + }); + router.push = vi.fn(); + router.replace = vi.fn(); + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + }); + + it('adds a new tab', () => { + const store = useTabbarStore(); + const tab: any = { + fullPath: '/home', + meta: {}, + name: 'Home', + path: '/home', + }; + store.addTab(tab); + expect(store.tabs.length).toBe(1); + expect(store.tabs[0]).toEqual(tab); + }); + + it('adds a new tab if it does not exist', () => { + const store = useTabbarStore(); + const newTab: any = { + fullPath: '/new', + meta: {}, + name: 'New', + path: '/new', + }; + store.addTab(newTab); + expect(store.tabs).toContainEqual(newTab); + }); + + it('updates an existing tab instead of adding a new one', () => { + const store = useTabbarStore(); + const initialTab: any = { + fullPath: '/existing', + meta: {}, + name: 'Existing', + path: '/existing', + query: {}, + }; + store.tabs.push(initialTab); + const updatedTab = { ...initialTab, query: { id: '1' } }; + store.addTab(updatedTab); + expect(store.tabs.length).toBe(1); + expect(store.tabs[0]?.query).toEqual({ id: '1' }); + }); + + it('closes all tabs', async () => { + const store = useTabbarStore(); + store.tabs = [ + { fullPath: '/home', meta: {}, name: 'Home', path: '/home' }, + ] as any; + router.replace = vi.fn(); + + await store.closeAllTabs(router); + + expect(store.tabs.length).toBe(1); + }); + + it('closes a non-affix tab', () => { + const store = useTabbarStore(); + const tab: any = { + fullPath: '/closable', + meta: {}, + name: 'Closable', + path: '/closable', + }; + store.tabs.push(tab); + store._close(tab); + expect(store.tabs.length).toBe(0); + }); + + it('does not close an affix tab', () => { + const store = useTabbarStore(); + const affixTab: any = { + fullPath: '/affix', + meta: { affixTab: true }, + name: 'Affix', + path: '/affix', + }; + store.tabs.push(affixTab); + store._close(affixTab); + expect(store.tabs.length).toBe(1); // Affix tab should not be closed + }); + + it('returns all cache tabs', () => { + const store = useTabbarStore(); + store.cachedTabs.add('Home'); + store.cachedTabs.add('About'); + expect(store.getCachedTabs).toEqual(['Home', 'About']); + }); + + it('returns all tabs, including affix tabs', () => { + const store = useTabbarStore(); + const normalTab: any = { + fullPath: '/normal', + meta: {}, + name: 'Normal', + path: '/normal', + }; + const affixTab: any = { + fullPath: '/affix', + meta: { affixTab: true }, + name: 'Affix', + path: '/affix', + }; + store.tabs.push(normalTab); + store.affixTabs.push(affixTab); + expect(store.getTabs).toContainEqual(normalTab); + expect(store.affixTabs).toContainEqual(affixTab); + }); + + it('navigates to a specific tab', async () => { + const store = useTabbarStore(); + const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' }; + + await store._goToTab(tab, router); + + expect(router.replace).toHaveBeenCalledWith({ + params: {}, + path: '/dashboard', + query: {}, + }); + }); + + it('closes multiple tabs by paths', async () => { + const store = useTabbarStore(); + store.addTab({ + fullPath: '/home', + meta: {}, + name: 'Home', + path: '/home', + } as any); + store.addTab({ + fullPath: '/about', + meta: {}, + name: 'About', + path: '/about', + } as any); + store.addTab({ + fullPath: '/contact', + meta: {}, + name: 'Contact', + path: '/contact', + } as any); + + await store._bulkCloseByPaths(['/home', '/contact']); + + expect(store.tabs).toHaveLength(1); + expect(store.tabs[0]?.name).toBe('About'); + }); + + it('closes all tabs to the left of the specified tab', async () => { + const store = useTabbarStore(); + store.addTab({ + fullPath: '/home', + meta: {}, + name: 'Home', + path: '/home', + } as any); + store.addTab({ + fullPath: '/about', + meta: {}, + name: 'About', + path: '/about', + } as any); + const targetTab: any = { + fullPath: '/contact', + meta: {}, + name: 'Contact', + path: '/contact', + }; + store.addTab(targetTab); + + await store.closeLeftTabs(targetTab); + + expect(store.tabs).toHaveLength(1); + expect(store.tabs[0]?.name).toBe('Contact'); + }); + + it('closes all tabs except the specified tab', async () => { + const store = useTabbarStore(); + store.addTab({ + fullPath: '/home', + meta: {}, + name: 'Home', + path: '/home', + } as any); + const targetTab: any = { + fullPath: '/about', + meta: {}, + name: 'About', + path: '/about', + }; + store.addTab(targetTab); + store.addTab({ + fullPath: '/contact', + meta: {}, + name: 'Contact', + path: '/contact', + } as any); + + await store.closeOtherTabs(targetTab); + + expect(store.tabs).toHaveLength(1); + expect(store.tabs[0]?.name).toBe('About'); + }); + + it('closes all tabs to the right of the specified tab', async () => { + const store = useTabbarStore(); + const targetTab: any = { + fullPath: '/home', + meta: {}, + name: 'Home', + path: '/home', + }; + store.addTab(targetTab); + store.addTab({ + fullPath: '/about', + meta: {}, + name: 'About', + path: '/about', + } as any); + store.addTab({ + fullPath: '/contact', + meta: {}, + name: 'Contact', + path: '/contact', + } as any); + + await store.closeRightTabs(targetTab); + + expect(store.tabs).toHaveLength(1); + expect(store.tabs[0]?.name).toBe('Home'); + }); + + it('closes the tab with the specified key', async () => { + const store = useTabbarStore(); + const keyToClose = '/about'; + store.addTab({ + fullPath: '/home', + meta: {}, + name: 'Home', + path: '/home', + } as any); + store.addTab({ + fullPath: keyToClose, + meta: {}, + name: 'About', + path: '/about', + } as any); + store.addTab({ + fullPath: '/contact', + meta: {}, + name: 'Contact', + path: '/contact', + } as any); + + await store.closeTabByKey(keyToClose, router); + + expect(store.tabs).toHaveLength(2); + expect( + store.tabs.find((tab) => tab.fullPath === keyToClose), + ).toBeUndefined(); + }); + + it('refreshes the current tab', async () => { + const store = useTabbarStore(); + const currentTab: any = { + fullPath: '/dashboard', + meta: { name: 'Dashboard' }, + name: 'Dashboard', + path: '/dashboard', + }; + router.currentRoute.value = currentTab; + + await store.refresh(router); + + expect(store.excludeCachedTabs.has('Dashboard')).toBe(false); + expect(store.renderRouteView).toBe(true); + }); +}); diff --git a/apps/vben5/packages/stores/src/modules/tabbar.ts b/apps/vben5/packages/stores/src/modules/tabbar.ts new file mode 100644 index 000000000..53a74feb7 --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/tabbar.ts @@ -0,0 +1,558 @@ +import type { TabDefinition } from '@vben-core/typings'; +import type { Router, RouteRecordNormalized } from 'vue-router'; + +import { toRaw } from 'vue'; + +import { + openRouteInNewWindow, + startProgress, + stopProgress, +} from '@vben-core/shared/utils'; + +import { acceptHMRUpdate, defineStore } from 'pinia'; + +interface TabbarState { + /** + * @zh_CN 当前打开的标签页列表缓存 + */ + cachedTabs: Set; + /** + * @zh_CN 拖拽结束的索引 + */ + dragEndIndex: number; + /** + * @zh_CN 需要排除缓存的标签页 + */ + excludeCachedTabs: Set; + /** + * @zh_CN 是否刷新 + */ + renderRouteView?: boolean; + /** + * @zh_CN 当前打开的标签页列表 + */ + tabs: TabDefinition[]; + /** + * @zh_CN 更新时间,用于一些更新场景,使用watch深度监听的话,会损耗性能 + */ + updateTime?: number; +} + +/** + * @zh_CN 访问权限相关 + */ +export const useTabbarStore = defineStore('core-tabbar', { + actions: { + /** + * Close tabs in bulk + */ + async _bulkCloseByPaths(paths: string[]) { + this.tabs = this.tabs.filter((item) => { + return !paths.includes(getTabPath(item)); + }); + + this.updateCacheTabs(); + }, + /** + * @zh_CN 关闭标签页 + * @param tab + */ + _close(tab: TabDefinition) { + const { fullPath } = tab; + if (isAffixTab(tab)) { + return; + } + const index = this.tabs.findIndex((item) => item.fullPath === fullPath); + index !== -1 && this.tabs.splice(index, 1); + }, + /** + * @zh_CN 跳转到默认标签页 + */ + async _goToDefaultTab(router: Router) { + if (this.getTabs.length <= 0) { + return; + } + const firstTab = this.getTabs[0]; + if (firstTab) { + await this._goToTab(firstTab, router); + } + }, + /** + * @zh_CN 跳转到标签页 + * @param tab + * @param router + */ + async _goToTab(tab: TabDefinition, router: Router) { + const { params, path, query } = tab; + const toParams = { + params: params || {}, + path, + query: query || {}, + }; + await router.replace(toParams); + }, + /** + * @zh_CN 添加标签页 + * @param routeTab + */ + addTab(routeTab: TabDefinition) { + const tab = cloneTab(routeTab); + if (!isTabShown(tab)) { + return; + } + + const tabIndex = this.tabs.findIndex((tab) => { + return getTabPath(tab) === getTabPath(routeTab); + }); + + if (tabIndex === -1) { + // 获取动态路由打开数,超过 0 即代表需要控制打开数 + const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ?? + -1) as number; + // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了 + // 获取到已经打开的动态路由数, 判断是否大于某一个值 + if ( + maxNumOfOpenTab > 0 && + this.tabs.filter((tab) => tab.name === routeTab.name).length >= + maxNumOfOpenTab + ) { + // 关闭第一个 + const index = this.tabs.findIndex( + (item) => item.name === routeTab.name, + ); + index !== -1 && this.tabs.splice(index, 1); + } + + this.tabs.push(tab); + } else { + // 页面已经存在,不重复添加选项卡,只更新选项卡参数 + const currentTab = toRaw(this.tabs)[tabIndex]; + const mergedTab = { + ...currentTab, + ...tab, + meta: { ...currentTab?.meta, ...tab.meta }, + }; + if (currentTab) { + const curMeta = currentTab.meta; + if (Reflect.has(curMeta, 'affixTab')) { + mergedTab.meta.affixTab = curMeta.affixTab; + } + if (Reflect.has(curMeta, 'newTabTitle')) { + mergedTab.meta.newTabTitle = curMeta.newTabTitle; + } + } + + this.tabs.splice(tabIndex, 1, mergedTab); + } + this.updateCacheTabs(); + }, + /** + * @zh_CN 关闭所有标签页 + */ + async closeAllTabs(router: Router) { + const newTabs = this.tabs.filter((tab) => isAffixTab(tab)); + this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1); + await this._goToDefaultTab(router); + this.updateCacheTabs(); + }, + /** + * @zh_CN 关闭左侧标签页 + * @param tab + */ + async closeLeftTabs(tab: TabDefinition) { + const index = this.tabs.findIndex( + (item) => getTabPath(item) === getTabPath(tab), + ); + + if (index < 1) { + return; + } + + const leftTabs = this.tabs.slice(0, index); + const paths: string[] = []; + + for (const item of leftTabs) { + if (!isAffixTab(item)) { + paths.push(getTabPath(item)); + } + } + await this._bulkCloseByPaths(paths); + }, + /** + * @zh_CN 关闭其他标签页 + * @param tab + */ + async closeOtherTabs(tab: TabDefinition) { + const closePaths = this.tabs.map((item) => getTabPath(item)); + + const paths: string[] = []; + + for (const path of closePaths) { + if (path !== tab.fullPath) { + const closeTab = this.tabs.find((item) => getTabPath(item) === path); + if (!closeTab) { + continue; + } + if (!isAffixTab(closeTab)) { + paths.push(getTabPath(closeTab)); + } + } + } + await this._bulkCloseByPaths(paths); + }, + /** + * @zh_CN 关闭右侧标签页 + * @param tab + */ + async closeRightTabs(tab: TabDefinition) { + const index = this.tabs.findIndex( + (item) => getTabPath(item) === getTabPath(tab), + ); + + if (index !== -1 && index < this.tabs.length - 1) { + const rightTabs = this.tabs.slice(index + 1); + + const paths: string[] = []; + for (const item of rightTabs) { + if (!isAffixTab(item)) { + paths.push(getTabPath(item)); + } + } + await this._bulkCloseByPaths(paths); + } + }, + + /** + * @zh_CN 关闭标签页 + * @param tab + * @param router + */ + async closeTab(tab: TabDefinition, router: Router) { + const { currentRoute } = router; + + // 关闭不是激活选项卡 + if (getTabPath(currentRoute.value) !== getTabPath(tab)) { + this._close(tab); + this.updateCacheTabs(); + return; + } + const index = this.getTabs.findIndex( + (item) => getTabPath(item) === getTabPath(currentRoute.value), + ); + + const before = this.getTabs[index - 1]; + const after = this.getTabs[index + 1]; + + // 下一个tab存在,跳转到下一个 + if (after) { + this._close(tab); + await this._goToTab(after, router); + // 上一个tab存在,跳转到上一个 + } else if (before) { + this._close(tab); + await this._goToTab(before, router); + } else { + console.error('Failed to close the tab; only one tab remains open.'); + } + }, + + /** + * @zh_CN 通过key关闭标签页 + * @param key + * @param router + */ + async closeTabByKey(key: string, router: Router) { + const originKey = decodeURIComponent(key); + const index = this.tabs.findIndex( + (item) => getTabPath(item) === originKey, + ); + if (index === -1) { + return; + } + + const tab = this.tabs[index]; + if (tab) { + await this.closeTab(tab, router); + } + }, + + /** + * 根据路径获取标签页 + * @param path + */ + getTabByPath(path: string) { + return this.getTabs.find( + (item) => getTabPath(item) === path, + ) as TabDefinition; + }, + /** + * @zh_CN 新窗口打开标签页 + * @param tab + */ + async openTabInNewWindow(tab: TabDefinition) { + openRouteInNewWindow(tab.fullPath || tab.path); + }, + + /** + * @zh_CN 固定标签页 + * @param tab + */ + async pinTab(tab: TabDefinition) { + const index = this.tabs.findIndex( + (item) => getTabPath(item) === getTabPath(tab), + ); + if (index !== -1) { + const oldTab = this.tabs[index]; + tab.meta.affixTab = true; + tab.meta.title = oldTab?.meta?.title as string; + // this.addTab(tab); + this.tabs.splice(index, 1, tab); + } + // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值 + const affixTabs = this.tabs.filter((tab) => isAffixTab(tab)); + // 获得固定tabs的index + const newIndex = affixTabs.findIndex( + (item) => getTabPath(item) === getTabPath(tab), + ); + // 交换位置重新排序 + await this.sortTabs(index, newIndex); + }, + + /** + * 刷新标签页 + */ + async refresh(router: Router) { + const { currentRoute } = router; + const { name } = currentRoute.value; + + this.excludeCachedTabs.add(name as string); + this.renderRouteView = false; + startProgress(); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + this.excludeCachedTabs.delete(name as string); + this.renderRouteView = true; + stopProgress(); + }, + + /** + * @zh_CN 重置标签页标题 + */ + async resetTabTitle(tab: TabDefinition) { + if (tab?.meta?.newTabTitle) { + return; + } + const findTab = this.tabs.find( + (item) => getTabPath(item) === getTabPath(tab), + ); + if (findTab) { + findTab.meta.newTabTitle = undefined; + await this.updateCacheTabs(); + } + }, + + /** + * 设置固定标签页 + * @param tabs + */ + setAffixTabs(tabs: RouteRecordNormalized[]) { + for (const tab of tabs) { + tab.meta.affixTab = true; + this.addTab(routeToTab(tab)); + } + }, + + /** + * @zh_CN 设置标签页标题 + * @param tab + * @param title + */ + async setTabTitle(tab: TabDefinition, title: string) { + const findTab = this.tabs.find( + (item) => getTabPath(item) === getTabPath(tab), + ); + + if (findTab) { + findTab.meta.newTabTitle = title; + + await this.updateCacheTabs(); + } + }, + + setUpdateTime() { + this.updateTime = Date.now(); + }, + /** + * @zh_CN 设置标签页顺序 + * @param oldIndex + * @param newIndex + */ + async sortTabs(oldIndex: number, newIndex: number) { + const currentTab = this.tabs[oldIndex]; + if (!currentTab) { + return; + } + this.tabs.splice(oldIndex, 1); + this.tabs.splice(newIndex, 0, currentTab); + this.dragEndIndex = this.dragEndIndex + 1; + }, + /** + * @zh_CN 切换固定标签页 + * @param tab + */ + async toggleTabPin(tab: TabDefinition) { + const affixTab = tab?.meta?.affixTab ?? false; + + await (affixTab ? this.unpinTab(tab) : this.pinTab(tab)); + }, + + /** + * @zh_CN 取消固定标签页 + * @param tab + */ + async unpinTab(tab: TabDefinition) { + const index = this.tabs.findIndex( + (item) => getTabPath(item) === getTabPath(tab), + ); + + if (index !== -1) { + const oldTab = this.tabs[index]; + tab.meta.affixTab = false; + tab.meta.title = oldTab?.meta?.title as string; + // this.addTab(tab); + this.tabs.splice(index, 1, tab); + } + // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值 + const affixTabs = this.tabs.filter((tab) => isAffixTab(tab)); + // 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置 + const newIndex = affixTabs.length; + // 交换位置重新排序 + await this.sortTabs(index, newIndex); + }, + + /** + * 根据当前打开的选项卡更新缓存 + */ + async updateCacheTabs() { + const cacheMap = new Set(); + + for (const tab of this.tabs) { + // 跳过不需要持久化的标签页 + const keepAlive = tab.meta?.keepAlive; + if (!keepAlive) { + continue; + } + (tab.matched || []).forEach((t, i) => { + if (i > 0) { + cacheMap.add(t.name as string); + } + }); + + const name = tab.name as string; + cacheMap.add(name); + } + this.cachedTabs = cacheMap; + }, + }, + getters: { + affixTabs(): TabDefinition[] { + const affixTabs = this.tabs.filter((tab) => isAffixTab(tab)); + + return affixTabs.sort((a, b) => { + const orderA = (a.meta?.affixTabOrder ?? 0) as number; + const orderB = (b.meta?.affixTabOrder ?? 0) as number; + return orderA - orderB; + }); + }, + getCachedTabs(): string[] { + return [...this.cachedTabs]; + }, + getExcludeCachedTabs(): string[] { + return [...this.excludeCachedTabs]; + }, + getTabs(): TabDefinition[] { + const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab)); + return [...this.affixTabs, ...normalTabs].filter(Boolean); + }, + }, + persist: [ + // tabs不需要保存在localStorage + { + pick: ['tabs'], + storage: sessionStorage, + }, + ], + state: (): TabbarState => ({ + cachedTabs: new Set(), + dragEndIndex: 0, + excludeCachedTabs: new Set(), + renderRouteView: true, + tabs: [], + updateTime: Date.now(), + }), +}); + +// 解决热更新问题 +const hot = import.meta.hot; +if (hot) { + hot.accept(acceptHMRUpdate(useTabbarStore, hot)); +} + +/** + * @zh_CN 克隆路由,防止路由被修改 + * @param route + */ +function cloneTab(route: TabDefinition): TabDefinition { + if (!route) { + return route; + } + const { matched, meta, ...opt } = route; + return { + ...opt, + matched: (matched + ? matched.map((item) => ({ + meta: item.meta, + name: item.name, + path: item.path, + })) + : undefined) as RouteRecordNormalized[], + meta: { + ...meta, + newTabTitle: meta.newTabTitle, + }, + }; +} + +/** + * @zh_CN 是否是固定标签页 + * @param tab + */ +function isAffixTab(tab: TabDefinition) { + return tab?.meta?.affixTab ?? false; +} + +/** + * @zh_CN 是否显示标签 + * @param tab + */ +function isTabShown(tab: TabDefinition) { + const matched = tab?.matched ?? []; + return !tab.meta.hideInTab && matched.every((item) => !item.meta.hideInTab); +} + +/** + * @zh_CN 获取标签页路径 + * @param tab + */ +function getTabPath(tab: RouteRecordNormalized | TabDefinition) { + return decodeURIComponent((tab as TabDefinition).fullPath || tab.path); +} + +function routeToTab(route: RouteRecordNormalized) { + return { + meta: route.meta, + name: route.name, + path: route.path, + } as TabDefinition; +} diff --git a/apps/vben5/packages/stores/src/modules/user.test.ts b/apps/vben5/packages/stores/src/modules/user.test.ts new file mode 100644 index 000000000..3d8a22c48 --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/user.test.ts @@ -0,0 +1,37 @@ +import { createPinia, setActivePinia } from 'pinia'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { useUserStore } from './user'; + +describe('useUserStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it('returns correct userInfo', () => { + const store = useUserStore(); + const userInfo: any = { name: 'Jane Doe', roles: [{ value: 'user' }] }; + store.setUserInfo(userInfo); + expect(store.userInfo).toEqual(userInfo); + }); + + // 测试重置用户信息时的行为 + it('clears userInfo and userRoles when setting null userInfo', () => { + const store = useUserStore(); + store.setUserInfo({ + roles: [{ roleName: 'User', value: 'user' }], + } as any); + expect(store.userInfo).not.toBeNull(); + expect(store.userRoles.length).toBeGreaterThan(0); + + store.setUserInfo(null as any); + expect(store.userInfo).toBeNull(); + expect(store.userRoles).toEqual([]); + }); + + // 测试在没有用户角色时返回空数组 + it('returns an empty array for userRoles if not set', () => { + const store = useUserStore(); + expect(store.userRoles).toEqual([]); + }); +}); diff --git a/apps/vben5/packages/stores/src/modules/user.ts b/apps/vben5/packages/stores/src/modules/user.ts new file mode 100644 index 000000000..9d374335d --- /dev/null +++ b/apps/vben5/packages/stores/src/modules/user.ts @@ -0,0 +1,64 @@ +import { acceptHMRUpdate, defineStore } from 'pinia'; + +interface BasicUserInfo { + [key: string]: any; + /** + * 头像 + */ + avatar: string; + /** + * 用户昵称 + */ + realName: string; + /** + * 用户角色 + */ + roles?: string[]; + /** + * 用户id + */ + userId: string; + /** + * 用户名 + */ + username: string; +} + +interface AccessState { + /** + * 用户信息 + */ + userInfo: BasicUserInfo | null; + /** + * 用户角色 + */ + userRoles: string[]; +} + +/** + * @zh_CN 用户信息相关 + */ +export const useUserStore = defineStore('core-user', { + actions: { + setUserInfo(userInfo: BasicUserInfo | null) { + // 设置用户信息 + this.userInfo = userInfo; + // 设置角色信息 + const roles = userInfo?.roles ?? []; + this.setUserRoles(roles); + }, + setUserRoles(roles: string[]) { + this.userRoles = roles; + }, + }, + state: (): AccessState => ({ + userInfo: null, + userRoles: [], + }), +}); + +// 解决热更新问题 +const hot = import.meta.hot; +if (hot) { + hot.accept(acceptHMRUpdate(useUserStore, hot)); +} diff --git a/apps/vben5/packages/stores/src/setup.ts b/apps/vben5/packages/stores/src/setup.ts new file mode 100644 index 000000000..890c4dc88 --- /dev/null +++ b/apps/vben5/packages/stores/src/setup.ts @@ -0,0 +1,43 @@ +import type { Pinia } from 'pinia'; + +import type { App } from 'vue'; + +import { createPinia } from 'pinia'; + +let pinia: Pinia; + +export interface InitStoreOptions { + /** + * @zh_CN 应用名,由于 @vben/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名,应用名将被用于持久化的前缀 + */ + namespace: string; +} + +/** + * @zh_CN 初始化pinia + */ +export async function initStores(app: App, options: InitStoreOptions) { + const { createPersistedState } = await import('pinia-plugin-persistedstate'); + pinia = createPinia(); + const { namespace } = options; + pinia.use( + createPersistedState({ + // key $appName-$store.id + key: (storeKey) => `${namespace}-${storeKey}`, + storage: localStorage, + }), + ); + app.use(pinia); + return pinia; +} + +export function resetAllStores() { + if (!pinia) { + console.error('Pinia is not installed'); + return; + } + const allStores = (pinia as any)._s; + for (const [_key, store] of allStores) { + store.$reset(); + } +} diff --git a/apps/vben5/packages/stores/tsconfig.json b/apps/vben5/packages/stores/tsconfig.json new file mode 100644 index 000000000..3057820a8 --- /dev/null +++ b/apps/vben5/packages/stores/tsconfig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src", "shim-pinia.d.ts"] +} diff --git a/apps/vben5/packages/styles/README.md b/apps/vben5/packages/styles/README.md new file mode 100644 index 000000000..4826446a0 --- /dev/null +++ b/apps/vben5/packages/styles/README.md @@ -0,0 +1,19 @@ +# @vben/styles + +用于多个 `app` 公用的样式文件,继承了 `@vben-core/design` 的所有能力。业务上有通用的样式文件可以放在这里。 + +## 用法 + +### 添加依赖 + +```bash +# 进入目标应用目录,例如 apps/xxxx-app +# cd apps/xxxx-app +pnpm add @vben/styles +``` + +### 使用 + +```ts +import '@vben/styles'; +``` diff --git a/apps/vben5/packages/styles/package.json b/apps/vben5/packages/styles/package.json new file mode 100644 index 000000000..fa25cb6ac --- /dev/null +++ b/apps/vben5/packages/styles/package.json @@ -0,0 +1,31 @@ +{ + "name": "@vben/styles", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/styles" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./antd": { + "default": "./src/antd/index.css" + }, + "./ele": { + "default": "./src/ele/index.css" + }, + "./global": { + "default": "./src/global/index.scss" + } + }, + "dependencies": { + "@vben-core/design": "workspace:*" + } +} diff --git a/apps/vben5/packages/styles/src/antd/index.css b/apps/vben5/packages/styles/src/antd/index.css new file mode 100644 index 000000000..33766b6f9 --- /dev/null +++ b/apps/vben5/packages/styles/src/antd/index.css @@ -0,0 +1,52 @@ +/* ant-design-vue 组件库的一些样式重置 */ + +.ant-app { + width: 100%; + height: 100%; + overscroll-behavior: none; + color: inherit; +} + +.ant-btn { + .anticon { + display: inline-flex; + } +} + +.ant-message-notice-content, +.ant-notification-notice { + @apply dark:border-border/60 dark:border; +} + +.form-valid-error { + /** select 选择器的样式 */ + + .ant-select .ant-select-selector { + border-color: hsl(var(--destructive)) !important; + } + + .ant-select-focused .ant-select-selector { + box-shadow: 0 0 0 2px rgb(255 38 5 / 6%) !important; + } + + /** 数字输入框样式 */ + .ant-input-number-focused { + box-shadow: 0 0 0 2px rgb(255 38 5 / 6%); + } + + /** 密码输入框样式 */ + .ant-input-affix-wrapper:hover { + border-color: hsl(var(--destructive)); + box-shadow: 0 0 0 2px rgb(255 38 5 / 6%); + } +} + +/** 区间选择器下面来回切换时的样式 */ +.ant-app .form-valid-error .ant-picker-active-bar { + background-color: hsl(var(--destructive)); +} + +/** 时间选择器的样式 */ +.ant-app .form-valid-error .ant-picker-focused { + box-shadow: 0 0 0 2px rgb(255 38 5 / 6%); +} diff --git a/apps/vben5/packages/styles/src/ele/index.css b/apps/vben5/packages/styles/src/ele/index.css new file mode 100644 index 000000000..111af0f5f --- /dev/null +++ b/apps/vben5/packages/styles/src/ele/index.css @@ -0,0 +1,3 @@ +.el-card { + --el-card-border-radius: var(--radius) !important; +} diff --git a/apps/vben5/packages/styles/src/global/index.scss b/apps/vben5/packages/styles/src/global/index.scss new file mode 100644 index 000000000..c7af9bbda --- /dev/null +++ b/apps/vben5/packages/styles/src/global/index.scss @@ -0,0 +1 @@ +@use '@vben-core/design/bem' as *; diff --git a/apps/vben5/packages/styles/src/index.ts b/apps/vben5/packages/styles/src/index.ts new file mode 100644 index 000000000..b375456cf --- /dev/null +++ b/apps/vben5/packages/styles/src/index.ts @@ -0,0 +1 @@ +import '@vben-core/design'; diff --git a/apps/vben5/packages/styles/tsconfig.json b/apps/vben5/packages/styles/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/styles/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/types/README.md b/apps/vben5/packages/types/README.md new file mode 100644 index 000000000..f796f31ca --- /dev/null +++ b/apps/vben5/packages/types/README.md @@ -0,0 +1,20 @@ +# @vben/types + +用于多个 `app` 公用的工具类型,继承了 `@vben-core/typings` 的所有能力。业务上有通用的类型定义可以放在这里。 + +## 用法 + +### 添加依赖 + +```bash +# 进入目标应用目录,例如 apps/xxxx-app +# cd apps/xxxx-app +pnpm add @vben/types +``` + +### 使用 + +```ts +// 推荐加上 type +import type { SelectOption } from '@vben/types'; +``` diff --git a/apps/vben5/packages/types/global.d.ts b/apps/vben5/packages/types/global.d.ts new file mode 100644 index 000000000..11b3b3ced --- /dev/null +++ b/apps/vben5/packages/types/global.d.ts @@ -0,0 +1,26 @@ +import type { RouteMeta as IRouteMeta } from '@vben-core/typings'; + +import 'vue-router'; + +declare module 'vue-router' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface RouteMeta extends IRouteMeta {} +} + +export interface VbenAdminProAppConfigRaw { + VITE_GLOB_API_URL: string; + VITE_GLOB_CLIENT_ID: string; + VITE_GLOB_CLIENT_SECRET: string; +} + +export interface ApplicationConfig { + apiURL: string; + clientId: string; + clientSecret: string; +} + +declare global { + interface Window { + _VBEN_ADMIN_PRO_APP_CONF_: VbenAdminProAppConfigRaw; + } +} diff --git a/apps/vben5/packages/types/package.json b/apps/vben5/packages/types/package.json new file mode 100644 index 000000000..e59723f3c --- /dev/null +++ b/apps/vben5/packages/types/package.json @@ -0,0 +1,27 @@ +{ + "name": "@vben/types", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/types" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./global": { + "types": "./global.d.ts" + } + }, + "dependencies": { + "@vben-core/typings": "workspace:*", + "vue": "catalog:", + "vue-router": "catalog:" + } +} diff --git a/apps/vben5/packages/types/src/index.ts b/apps/vben5/packages/types/src/index.ts new file mode 100644 index 000000000..1e266c47e --- /dev/null +++ b/apps/vben5/packages/types/src/index.ts @@ -0,0 +1,2 @@ +export type * from './user'; +export type * from '@vben-core/typings'; diff --git a/apps/vben5/packages/types/src/user.ts b/apps/vben5/packages/types/src/user.ts new file mode 100644 index 000000000..44dc5b4ab --- /dev/null +++ b/apps/vben5/packages/types/src/user.ts @@ -0,0 +1,20 @@ +import type { BasicUserInfo } from '@vben-core/typings'; + +/** 用户信息 */ +interface UserInfo extends BasicUserInfo { + /** + * 用户描述 + */ + desc: string; + /** + * 首页地址 + */ + homePath: string; + + /** + * accessToken + */ + token: string; +} + +export type { UserInfo }; diff --git a/apps/vben5/packages/types/tsconfig.json b/apps/vben5/packages/types/tsconfig.json new file mode 100644 index 000000000..ce1a891fb --- /dev/null +++ b/apps/vben5/packages/types/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/packages/utils/README.md b/apps/vben5/packages/utils/README.md new file mode 100644 index 000000000..f06068aed --- /dev/null +++ b/apps/vben5/packages/utils/README.md @@ -0,0 +1,19 @@ +# @vben/utils + +用于多个 `app` 公用的工具包,继承了 `@vben-core/shared/utils` 的所有能力。业务上有通用的工具函数可以放在这里。 + +## 用法 + +### 添加依赖 + +```bash +# 进入目标应用目录,例如 apps/xxxx-app +# cd apps/xxxx-app +pnpm add @vben/utils +``` + +### 使用 + +```ts +import { isString } from '@vben/utils'; +``` diff --git a/apps/vben5/packages/utils/package.json b/apps/vben5/packages/utils/package.json new file mode 100644 index 000000000..ccf093e92 --- /dev/null +++ b/apps/vben5/packages/utils/package.json @@ -0,0 +1,27 @@ +{ + "name": "@vben/utils", + "version": "5.4.8", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/utils" + }, + "license": "MIT", + "type": "module", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "dependencies": { + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "vue-router": "catalog:" + } +} diff --git a/apps/vben5/packages/utils/src/helpers/__tests__/find-menu-by-path.test.ts b/apps/vben5/packages/utils/src/helpers/__tests__/find-menu-by-path.test.ts new file mode 100644 index 000000000..fc0d76b58 --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/__tests__/find-menu-by-path.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; + +import { findMenuByPath, findRootMenuByPath } from '../find-menu-by-path'; + +// 示例菜单数据 +const menus: any[] = [ + { path: '/', children: [] }, + { path: '/about', children: [] }, + { + path: '/contact', + children: [ + { path: '/contact/email', children: [] }, + { path: '/contact/phone', children: [] }, + ], + }, + { + path: '/services', + children: [ + { path: '/services/design', children: [] }, + { + path: '/services/development', + children: [{ path: '/services/development/web', children: [] }], + }, + ], + }, +]; + +describe('menu Finder Tests', () => { + it('finds a top-level menu', () => { + const menu = findMenuByPath(menus, '/about'); + expect(menu).toBeDefined(); + expect(menu?.path).toBe('/about'); + }); + + it('finds a nested menu', () => { + const menu = findMenuByPath(menus, '/services/development/web'); + expect(menu).toBeDefined(); + expect(menu?.path).toBe('/services/development/web'); + }); + + it('returns null for a non-existent path', () => { + const menu = findMenuByPath(menus, '/non-existent'); + expect(menu).toBeNull(); + }); + + it('handles empty menus list', () => { + const menu = findMenuByPath([], '/about'); + expect(menu).toBeNull(); + }); + + it('handles menu items without children', () => { + const menu = findMenuByPath( + [{ path: '/only', children: undefined }] as any[], + '/only', + ); + expect(menu).toBeDefined(); + expect(menu?.path).toBe('/only'); + }); + + it('finds root menu by path', () => { + const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath( + menus, + '/services/development/web', + ); + + expect(findMenu).toBeDefined(); + expect(rootMenu).toBeUndefined(); + expect(rootMenuPath).toBeUndefined(); + expect(findMenu?.path).toBe('/services/development/web'); + }); + + it('returns null for undefined or empty path', () => { + const menuUndefinedPath = findMenuByPath(menus); + const menuEmptyPath = findMenuByPath(menus, ''); + expect(menuUndefinedPath).toBeNull(); + expect(menuEmptyPath).toBeNull(); + }); + + it('checks for root menu when path does not exist', () => { + const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath( + menus, + '/non-existent', + ); + expect(findMenu).toBeNull(); + expect(rootMenu).toBeUndefined(); + expect(rootMenuPath).toBeUndefined(); + }); +}); diff --git a/apps/vben5/packages/utils/src/helpers/__tests__/generate-menus.test.ts b/apps/vben5/packages/utils/src/helpers/__tests__/generate-menus.test.ts new file mode 100644 index 000000000..9d98b5a19 --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/__tests__/generate-menus.test.ts @@ -0,0 +1,236 @@ +import type { Router, RouteRecordRaw } from 'vue-router'; + +import { createRouter, createWebHistory } from 'vue-router'; + +import { describe, expect, it, vi } from 'vitest'; + +import { generateMenus } from '../generate-menus'; + +// Nested route setup to test child inclusion and hideChildrenInMenu functionality + +describe('generateMenus', () => { + // 模拟路由数据 + const mockRoutes = [ + { + meta: { icon: 'home-icon', title: '首页' }, + name: 'home', + path: '/home', + }, + { + meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' }, + name: 'about', + path: '/about', + children: [ + { + path: 'team', + name: 'team', + meta: { icon: 'team-icon', title: '团队' }, + }, + ], + }, + ] as RouteRecordRaw[]; + + // 模拟 Vue 路由器实例 + const mockRouter = { + getRoutes: vi.fn(() => [ + { name: 'home', path: '/home' }, + { name: 'about', path: '/about' }, + { name: 'team', path: '/about/team' }, + ]), + }; + + it('the correct menu list should be generated according to the route', async () => { + const expectedMenus = [ + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: 'home-icon', + name: '首页', + order: undefined, + parent: undefined, + parents: undefined, + path: '/home', + show: true, + children: [], + }, + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: 'about-icon', + name: '关于', + order: undefined, + parent: undefined, + parents: undefined, + path: '/about', + show: true, + children: [], + }, + ]; + + const menus = await generateMenus(mockRoutes, mockRouter as any); + expect(menus).toEqual(expectedMenus); + }); + + it('includes additional meta properties in menu items', async () => { + const mockRoutesWithMeta = [ + { + meta: { icon: 'user-icon', order: 1, title: 'Profile' }, + name: 'profile', + path: '/profile', + }, + ] as RouteRecordRaw[]; + + const menus = await generateMenus(mockRoutesWithMeta, mockRouter as any); + expect(menus).toEqual([ + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: 'user-icon', + name: 'Profile', + order: 1, + parent: undefined, + parents: undefined, + path: '/profile', + show: true, + children: [], + }, + ]); + }); + + it('handles dynamic route parameters correctly', async () => { + const mockRoutesWithParams = [ + { + meta: { icon: 'details-icon', title: 'User Details' }, + name: 'userDetails', + path: '/users/:userId', + }, + ] as RouteRecordRaw[]; + + const menus = await generateMenus(mockRoutesWithParams, mockRouter as any); + expect(menus).toEqual([ + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: 'details-icon', + name: 'User Details', + order: undefined, + parent: undefined, + parents: undefined, + path: '/users/:userId', + show: true, + children: [], + }, + ]); + }); + + it('processes routes with redirects correctly', async () => { + const mockRoutesWithRedirect = [ + { + name: 'redirectedRoute', + path: '/old-path', + redirect: '/new-path', + }, + { + meta: { icon: 'path-icon', title: 'New Path' }, + name: 'newPath', + path: '/new-path', + }, + ] as RouteRecordRaw[]; + + const menus = await generateMenus( + mockRoutesWithRedirect, + mockRouter as any, + ); + expect(menus).toEqual([ + // Assuming your generateMenus function excludes redirect routes from the menu + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: undefined, + name: 'redirectedRoute', + order: undefined, + parent: undefined, + parents: undefined, + path: '/old-path', + show: true, + children: [], + }, + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: 'path-icon', + name: 'New Path', + order: undefined, + parent: undefined, + parents: undefined, + path: '/new-path', + show: true, + children: [], + }, + ]); + }); + + const routes: any = [ + { + meta: { order: 2, title: 'Home' }, + name: 'home', + path: '/', + }, + { + meta: { order: 1, title: 'About' }, + name: 'about', + path: '/about', + }, + ]; + + const router: Router = createRouter({ + history: createWebHistory(), + routes, + }); + + it('should generate menu list with correct order', async () => { + const menus = await generateMenus(routes, router); + const expectedMenus = [ + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: undefined, + name: 'About', + order: 1, + parent: undefined, + parents: undefined, + path: '/about', + show: true, + children: [], + }, + { + badge: undefined, + badgeType: undefined, + badgeVariants: undefined, + icon: undefined, + name: 'Home', + order: 2, + parent: undefined, + parents: undefined, + path: '/', + show: true, + children: [], + }, + ]; + + expect(menus).toEqual(expectedMenus); + }); + + it('should handle empty routes', async () => { + const emptyRoutes: any[] = []; + const menus = await generateMenus(emptyRoutes, router); + expect(menus).toEqual([]); + }); +}); diff --git a/apps/vben5/packages/utils/src/helpers/__tests__/generate-routes-frontend.test.ts b/apps/vben5/packages/utils/src/helpers/__tests__/generate-routes-frontend.test.ts new file mode 100644 index 000000000..8e018530a --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/__tests__/generate-routes-frontend.test.ts @@ -0,0 +1,105 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { describe, expect, it } from 'vitest'; + +import { + generateRoutesByFrontend, + hasAuthority, +} from '../generate-routes-frontend'; + +// Mock 路由数据 +const mockRoutes = [ + { + meta: { + authority: ['admin', 'user'], + hideInMenu: false, + }, + path: '/dashboard', + children: [ + { + path: '/dashboard/overview', + meta: { authority: ['admin'], hideInMenu: false }, + }, + { + path: '/dashboard/stats', + meta: { authority: ['user'], hideInMenu: true }, + }, + ], + }, + { + meta: { authority: ['admin'], hideInMenu: false }, + path: '/settings', + }, + { + meta: { hideInMenu: false }, + path: '/profile', + }, +] as RouteRecordRaw[]; + +describe('hasAuthority', () => { + it('should return true if there is no authority defined', () => { + expect(hasAuthority(mockRoutes[2], ['admin'])).toBe(true); + }); + + it('should return true if the user has the required authority', () => { + expect(hasAuthority(mockRoutes[0], ['admin'])).toBe(true); + }); + + it('should return false if the user does not have the required authority', () => { + expect(hasAuthority(mockRoutes[1], ['user'])).toBe(false); + }); +}); + +describe('generateRoutesByFrontend', () => { + it('should handle routes without children', async () => { + const generatedRoutes = await generateRoutesByFrontend(mockRoutes, [ + 'user', + ]); + expect(generatedRoutes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: '/profile', // This route has no children and should be included + }), + ]), + ); + }); + + it('should handle empty roles array', async () => { + const generatedRoutes = await generateRoutesByFrontend(mockRoutes, []); + expect(generatedRoutes).toEqual( + expect.arrayContaining([ + // Only routes without authority should be included + expect.objectContaining({ + path: '/profile', + }), + ]), + ); + expect(generatedRoutes).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: '/dashboard', + }), + expect.objectContaining({ + path: '/settings', + }), + ]), + ); + }); + + it('should handle missing meta fields', async () => { + const routesWithMissingMeta = [ + { path: '/path1' }, // No meta + { meta: {}, path: '/path2' }, // Empty meta + { meta: { authority: ['admin'] }, path: '/path3' }, // Only authority + ]; + const generatedRoutes = await generateRoutesByFrontend( + routesWithMissingMeta as RouteRecordRaw[], + ['admin'], + ); + expect(generatedRoutes).toEqual([ + { path: '/path1' }, + { meta: {}, path: '/path2' }, + { meta: { authority: ['admin'] }, path: '/path3' }, + ]); + }); +}); diff --git a/apps/vben5/packages/utils/src/helpers/__tests__/merge-route-modules.test.ts b/apps/vben5/packages/utils/src/helpers/__tests__/merge-route-modules.test.ts new file mode 100644 index 000000000..36155566a --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/__tests__/merge-route-modules.test.ts @@ -0,0 +1,68 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import type { RouteModuleType } from '../merge-route-modules'; + +import { describe, expect, it } from 'vitest'; + +import { mergeRouteModules } from '../merge-route-modules'; + +describe('mergeRouteModules', () => { + it('should merge route modules correctly', () => { + const routeModules: Record = { + './dynamic-routes/about.ts': { + default: [ + { + component: () => Promise.resolve({ template: '
About
' }), + name: 'About', + path: '/about', + }, + ], + }, + './dynamic-routes/home.ts': { + default: [ + { + component: () => Promise.resolve({ template: '
Home
' }), + name: 'Home', + path: '/', + }, + ], + }, + }; + + const expectedRoutes: RouteRecordRaw[] = [ + { + component: expect.any(Function), + name: 'About', + path: '/about', + }, + { + component: expect.any(Function), + name: 'Home', + path: '/', + }, + ]; + + const mergedRoutes = mergeRouteModules(routeModules); + expect(mergedRoutes).toEqual(expectedRoutes); + }); + + it('should handle empty modules', () => { + const routeModules: Record = {}; + const expectedRoutes: RouteRecordRaw[] = []; + + const mergedRoutes = mergeRouteModules(routeModules); + expect(mergedRoutes).toEqual(expectedRoutes); + }); + + it('should handle modules with no default export', () => { + const routeModules: Record = { + './dynamic-routes/empty.ts': { + default: [], + }, + }; + const expectedRoutes: RouteRecordRaw[] = []; + + const mergedRoutes = mergeRouteModules(routeModules); + expect(mergedRoutes).toEqual(expectedRoutes); + }); +}); diff --git a/apps/vben5/packages/utils/src/helpers/find-menu-by-path.ts b/apps/vben5/packages/utils/src/helpers/find-menu-by-path.ts new file mode 100644 index 000000000..1c69e6228 --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/find-menu-by-path.ts @@ -0,0 +1,37 @@ +import type { MenuRecordRaw } from '@vben-core/typings'; + +function findMenuByPath( + list: MenuRecordRaw[], + path?: string, +): MenuRecordRaw | null { + for (const menu of list) { + if (menu.path === path) { + return menu; + } + const findMenu = menu.children && findMenuByPath(menu.children, path); + if (findMenu) { + return findMenu; + } + } + return null; +} + +/** + * 查找根菜单 + * @param menus + * @param path + */ +function findRootMenuByPath(menus: MenuRecordRaw[], path?: string) { + const findMenu = findMenuByPath(menus, path); + const rootMenuPath = findMenu?.parents?.[0]; + const rootMenu = rootMenuPath + ? menus.find((item) => item.path === rootMenuPath) + : undefined; + return { + findMenu, + rootMenu, + rootMenuPath, + }; +} + +export { findMenuByPath, findRootMenuByPath }; diff --git a/apps/vben5/packages/utils/src/helpers/generate-menus.ts b/apps/vben5/packages/utils/src/helpers/generate-menus.ts new file mode 100644 index 000000000..48c30fd6e --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/generate-menus.ts @@ -0,0 +1,80 @@ +import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings'; +import type { Router, RouteRecordRaw } from 'vue-router'; + +import { filterTree, mapTree } from '@vben-core/shared/utils'; + +/** + * 根据 routes 生成菜单列表 + * @param routes + */ +async function generateMenus( + routes: RouteRecordRaw[], + router: Router, +): Promise { + // 将路由列表转换为一个以 name 为键的对象映射 + // 获取所有router最终的path及name + const finalRoutesMap: { [key: string]: string } = Object.fromEntries( + router.getRoutes().map(({ name, path }) => [name, path]), + ); + + let menus = mapTree(routes, (route) => { + // 路由表的路径写法有多种,这里从router获取到最终的path并赋值 + const path = finalRoutesMap[route.name as string] ?? route.path; + + // 转换为菜单结构 + // const path = matchRoute?.path ?? route.path; + const { meta, name: routeName, redirect, children } = route; + const { + activeIcon, + badge, + badgeType, + badgeVariants, + hideChildrenInMenu = false, + icon, + link, + order, + title = '', + } = meta || {}; + + const name = (title || routeName || '') as string; + + // 隐藏子菜单 + const resultChildren = hideChildrenInMenu + ? [] + : (children as MenuRecordRaw[]); + + // 将菜单的所有父级和父级菜单记录到菜单项内 + if (resultChildren && resultChildren.length > 0) { + resultChildren.forEach((child) => { + child.parents = [...(route.parents || []), path]; + child.parent = path; + }); + } + // 隐藏子菜单 + const resultPath = hideChildrenInMenu ? redirect || path : link || path; + return { + activeIcon, + badge, + badgeType, + badgeVariants, + icon, + name, + order, + parent: route.parent, + parents: route.parents, + path: resultPath as string, + show: !route?.meta?.hideInMenu, + children: resultChildren || [], + }; + }); + + // 对菜单进行排序 + menus = menus.sort((a, b) => (a.order || 999) - (b.order || 999)); + + const finalMenus = filterTree(menus, (menu) => { + return !!menu.show; + }); + return finalMenus; +} + +export { generateMenus }; diff --git a/apps/vben5/packages/utils/src/helpers/generate-routes-backend.ts b/apps/vben5/packages/utils/src/helpers/generate-routes-backend.ts new file mode 100644 index 000000000..4f179080d --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/generate-routes-backend.ts @@ -0,0 +1,82 @@ +import type { + ComponentRecordType, + GenerateMenuAndRoutesOptions, + RouteRecordStringComponent, +} from '@vben-core/typings'; +import type { RouteRecordRaw } from 'vue-router'; + +import { mapTree } from '@vben-core/shared/utils'; + +/** + * 动态生成路由 - 后端方式 + */ +async function generateRoutesByBackend( + options: GenerateMenuAndRoutesOptions, +): Promise { + const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options; + + try { + const menuRoutes = await fetchMenuListAsync?.(); + if (!menuRoutes) { + return []; + } + + const normalizePageMap: ComponentRecordType = {}; + + for (const [key, value] of Object.entries(pageMap)) { + normalizePageMap[normalizeViewPath(key)] = value; + } + + const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap); + + return routes; + } catch (error) { + console.error(error); + return []; + } +} + +function convertRoutes( + routes: RouteRecordStringComponent[], + layoutMap: ComponentRecordType, + pageMap: ComponentRecordType, +): RouteRecordRaw[] { + return mapTree(routes, (node) => { + const route = node as unknown as RouteRecordRaw; + const { component, name } = node; + + if (!name) { + console.error('route name is required', route); + } + + // layout转换 + if (component && layoutMap[component]) { + route.component = layoutMap[component]; + // 页面组件转换 + } else if (component) { + const normalizePath = normalizeViewPath(component); + route.component = + pageMap[ + normalizePath.endsWith('.vue') + ? normalizePath + : `${normalizePath}.vue` + ]; + } + + return route; + }); +} + +function normalizeViewPath(path: string): string { + // 去除相对路径前缀 + const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, ''); + + // 确保路径以 '/' 开头 + const viewPath = normalizedPath.startsWith('/') + ? normalizedPath + : `/${normalizedPath}`; + + // 这里耦合了vben-admin的目录结构 + return viewPath.replace(/^\/views/, ''); +} +export { generateRoutesByBackend }; diff --git a/apps/vben5/packages/utils/src/helpers/generate-routes-frontend.ts b/apps/vben5/packages/utils/src/helpers/generate-routes-frontend.ts new file mode 100644 index 000000000..dafc8a7d3 --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/generate-routes-frontend.ts @@ -0,0 +1,58 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { filterTree, mapTree } from '@vben-core/shared/utils'; + +/** + * 动态生成路由 - 前端方式 + */ +async function generateRoutesByFrontend( + routes: RouteRecordRaw[], + roles: string[], + forbiddenComponent?: RouteRecordRaw['component'], +): Promise { + // 根据角色标识过滤路由表,判断当前用户是否拥有指定权限 + const finalRoutes = filterTree(routes, (route) => { + return hasAuthority(route, roles); + }); + + if (!forbiddenComponent) { + return finalRoutes; + } + + // 如果有禁止访问的页面,将禁止访问的页面替换为403页面 + return mapTree(finalRoutes, (route) => { + if (menuHasVisibleWithForbidden(route)) { + route.component = forbiddenComponent; + } + return route; + }); +} + +/** + * 判断路由是否有权限访问 + * @param route + * @param access + */ +function hasAuthority(route: RouteRecordRaw, access: string[]) { + const authority = route.meta?.authority; + if (!authority) { + return true; + } + const canAccess = access.some((value) => authority.includes(value)); + + return canAccess || (!canAccess && menuHasVisibleWithForbidden(route)); +} + +/** + * 判断路由是否在菜单中显示,但是访问会被重定向到403 + * @param route + */ +function menuHasVisibleWithForbidden(route: RouteRecordRaw) { + return ( + !!route.meta?.authority && + Reflect.has(route.meta || {}, 'menuVisibleWithForbidden') && + !!route.meta?.menuVisibleWithForbidden + ); +} + +export { generateRoutesByFrontend, hasAuthority }; diff --git a/apps/vben5/packages/utils/src/helpers/get-popup-container.ts b/apps/vben5/packages/utils/src/helpers/get-popup-container.ts new file mode 100644 index 000000000..ca5a72ddc --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/get-popup-container.ts @@ -0,0 +1,6 @@ +/** + * Returns the parent node of the given element or the document body if the element is not provided.it + */ +export function getPopupContainer(node?: HTMLElement): HTMLElement { + return (node?.parentNode as HTMLElement) ?? document.body; +} diff --git a/apps/vben5/packages/utils/src/helpers/index.ts b/apps/vben5/packages/utils/src/helpers/index.ts new file mode 100644 index 000000000..da2cd8d7f --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/index.ts @@ -0,0 +1,8 @@ +export * from './find-menu-by-path'; +export * from './generate-menus'; +export * from './generate-routes-backend'; +export * from './generate-routes-frontend'; +export * from './get-popup-container'; +export * from './merge-route-modules'; +export * from './reset-routes'; +export * from './unmount-global-loading'; diff --git a/apps/vben5/packages/utils/src/helpers/merge-route-modules.ts b/apps/vben5/packages/utils/src/helpers/merge-route-modules.ts new file mode 100644 index 000000000..53e21f373 --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/merge-route-modules.ts @@ -0,0 +1,28 @@ +import type { RouteRecordRaw } from 'vue-router'; + +// 定义模块类型 +interface RouteModuleType { + default: RouteRecordRaw[]; +} + +/** + * 合并动态路由模块的默认导出 + * @param routeModules 动态导入的路由模块对象 + * @returns 合并后的路由配置数组 + */ +function mergeRouteModules( + routeModules: Record, +): RouteRecordRaw[] { + const mergedRoutes: RouteRecordRaw[] = []; + + for (const routeModule of Object.values(routeModules)) { + const moduleRoutes = (routeModule as RouteModuleType)?.default ?? []; + mergedRoutes.push(...moduleRoutes); + } + + return mergedRoutes; +} + +export { mergeRouteModules }; + +export type { RouteModuleType }; diff --git a/apps/vben5/packages/utils/src/helpers/reset-routes.ts b/apps/vben5/packages/utils/src/helpers/reset-routes.ts new file mode 100644 index 000000000..0d53a002f --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/reset-routes.ts @@ -0,0 +1,31 @@ +import type { Router, RouteRecordName, RouteRecordRaw } from 'vue-router'; + +import { traverseTreeValues } from '@vben-core/shared/utils'; + +/** + * @zh_CN 重置所有路由,如有指定白名单除外 + */ +export function resetStaticRoutes(router: Router, routes: RouteRecordRaw[]) { + // 获取静态路由所有节点包含子节点的 name,并排除不存在 name 字段的路由 + const staticRouteNames = traverseTreeValues< + RouteRecordRaw, + RouteRecordName | undefined + >(routes, (route) => { + // 这些路由需要指定 name,防止在路由重置时,不能删除没有指定 name 的路由 + if (!route.name) { + console.warn( + `The route with the path ${route.path} needs to have the field name specified.`, + ); + } + return route.name; + }); + + const { getRoutes, hasRoute, removeRoute } = router; + const allRoutes = getRoutes(); + allRoutes.forEach(({ name }) => { + // 存在于路由表且非白名单才需要删除 + if (name && !staticRouteNames.includes(name) && hasRoute(name)) { + removeRoute(name); + } + }); +} diff --git a/apps/vben5/packages/utils/src/helpers/unmount-global-loading.ts b/apps/vben5/packages/utils/src/helpers/unmount-global-loading.ts new file mode 100644 index 000000000..10b88eaeb --- /dev/null +++ b/apps/vben5/packages/utils/src/helpers/unmount-global-loading.ts @@ -0,0 +1,31 @@ +/** + * 移除并销毁loading + * 放在这里是而不是放在 index.html 的app标签内,是因为这样比较不会生硬,渲染过快可能会有闪烁 + * 通过先添加css动画隐藏,在动画结束后在移除loading节点来改善体验 + * 不好的地方是会增加一些代码量 + * 自定义loading可以见:https://doc.vben.pro/guide/in-depth/loading.html + */ +export function unmountGlobalLoading() { + // 查找全局 loading 元素 + const loadingElement = document.querySelector('#__app-loading__'); + + if (loadingElement) { + // 添加隐藏类,触发过渡动画 + loadingElement.classList.add('hidden'); + + // 查找所有需要移除的注入 loading 元素 + const injectLoadingElements = document.querySelectorAll( + '[data-app-loading^="inject"]', + ); + + // 当过渡动画结束时,移除 loading 元素和所有注入的 loading 元素 + loadingElement.addEventListener( + 'transitionend', + () => { + loadingElement.remove(); // 移除 loading 元素 + injectLoadingElements.forEach((el) => el.remove()); // 移除所有注入的 loading 元素 + }, + { once: true }, + ); // 确保事件只触发一次 + } +} diff --git a/apps/vben5/packages/utils/src/index.ts b/apps/vben5/packages/utils/src/index.ts new file mode 100644 index 000000000..80263b6ab --- /dev/null +++ b/apps/vben5/packages/utils/src/index.ts @@ -0,0 +1,4 @@ +export * from './helpers'; +export * from '@vben-core/shared/cache'; +export * from '@vben-core/shared/color'; +export * from '@vben-core/shared/utils'; diff --git a/apps/vben5/packages/utils/tsconfig.json b/apps/vben5/packages/utils/tsconfig.json new file mode 100644 index 000000000..255148a41 --- /dev/null +++ b/apps/vben5/packages/utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/library.json", + "compilerOptions": { + "types": ["@vben-core/typings/vue-router"] + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/playground/.env b/apps/vben5/playground/.env new file mode 100644 index 000000000..329573030 --- /dev/null +++ b/apps/vben5/playground/.env @@ -0,0 +1,5 @@ +# 应用标题 +VITE_APP_TITLE=Vben Admin + +# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 +VITE_APP_NAMESPACE=vben-web-play diff --git a/apps/vben5/playground/.env.analyze b/apps/vben5/playground/.env.analyze new file mode 100644 index 000000000..ffafa8dd5 --- /dev/null +++ b/apps/vben5/playground/.env.analyze @@ -0,0 +1,7 @@ +# public path +VITE_BASE=/ + +# Basic interface address SPA +VITE_GLOB_API_URL=/api + +VITE_VISUALIZER=true diff --git a/apps/vben5/playground/.env.development b/apps/vben5/playground/.env.development new file mode 100644 index 000000000..dcf361e73 --- /dev/null +++ b/apps/vben5/playground/.env.development @@ -0,0 +1,16 @@ +# 端口号 +VITE_PORT=5555 + +VITE_BASE=/ + +# 接口地址 +VITE_GLOB_API_URL=/api + +# 是否开启 Nitro Mock服务,true 为开启,false 为关闭 +VITE_NITRO_MOCK=true + +# 是否打开 devtools,true 为打开,false 为关闭 +VITE_DEVTOOLS=false + +# 是否注入全局loading +VITE_INJECT_APP_LOADING=true diff --git a/apps/vben5/playground/.env.production b/apps/vben5/playground/.env.production new file mode 100644 index 000000000..5375847a6 --- /dev/null +++ b/apps/vben5/playground/.env.production @@ -0,0 +1,19 @@ +VITE_BASE=/ + +# 接口地址 +VITE_GLOB_API_URL=https://mock-napi.vben.pro/api + +# 是否开启压缩,可以设置为 none, brotli, gzip +VITE_COMPRESS=none + +# 是否开启 PWA +VITE_PWA=false + +# vue-router 的模式 +VITE_ROUTER_HISTORY=hash + +# 是否注入全局loading +VITE_INJECT_APP_LOADING=true + +# 打包后是否生成dist.zip +VITE_ARCHIVER=true diff --git a/apps/vben5/playground/__tests__/e2e/auth-login.spec.ts b/apps/vben5/playground/__tests__/e2e/auth-login.spec.ts new file mode 100644 index 000000000..bb6cd2895 --- /dev/null +++ b/apps/vben5/playground/__tests__/e2e/auth-login.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; + +import { authLogin } from './common/auth'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.describe('Auth Login Page Tests', () => { + test('check title and page elements', async ({ page }) => { + // 获取页面标题并断言标题包含 'Vben Admin' + const title = await page.title(); + expect(title).toContain('Vben Admin'); + }); + + // 测试用例: 成功登录 + test('should successfully login with valid credentials', async ({ page }) => { + await authLogin(page); + }); +}); diff --git a/apps/vben5/playground/__tests__/e2e/common/auth.ts b/apps/vben5/playground/__tests__/e2e/common/auth.ts new file mode 100644 index 000000000..26b526fde --- /dev/null +++ b/apps/vben5/playground/__tests__/e2e/common/auth.ts @@ -0,0 +1,46 @@ +import type { Page } from '@playwright/test'; + +import { expect } from '@playwright/test'; + +export async function authLogin(page: Page) { + // 确保登录表单正常 + const usernameInput = await page.locator(`input[name='username']`); + await expect(usernameInput).toBeVisible(); + + const passwordInput = await page.locator(`input[name='password']`); + await expect(passwordInput).toBeVisible(); + + const sliderCaptcha = await page.locator(`div[name='captcha']`); + const sliderCaptchaAction = await page.locator(`div[name='captcha-action']`); + await expect(sliderCaptcha).toBeVisible(); + await expect(sliderCaptchaAction).toBeVisible(); + + // 拖动验证码滑块 + // 获取拖动按钮的位置 + const sliderCaptchaBox = await sliderCaptcha.boundingBox(); + if (!sliderCaptchaBox) throw new Error('滑块未找到'); + + const actionBoundingBox = await sliderCaptchaAction.boundingBox(); + if (!actionBoundingBox) throw new Error('要拖动的按钮未找到'); + + // 计算起始位置和目标位置 + const startX = actionBoundingBox.x + actionBoundingBox.width / 2; // div 中心的 x 坐标 + const startY = actionBoundingBox.y + actionBoundingBox.height / 2; // div 中心的 y 坐标 + + const targetX = startX + sliderCaptchaBox.width + actionBoundingBox.width; // 向右拖动容器的宽度 + const targetY = startY; // y 坐标保持不变 + + // 模拟鼠标拖动 + await page.mouse.move(startX, startY); // 移动到 action 的中心 + await page.mouse.down(); // 按下鼠标 + await page.mouse.move(targetX, targetY, { steps: 20 }); // 拖动到目标位置 + await page.mouse.up(); // 松开鼠标 + + // 在拖动后进行断言,检查action是否在预期位置, + const newActionBoundingBox = await sliderCaptchaAction.boundingBox(); + expect(newActionBoundingBox?.x).toBeGreaterThan(actionBoundingBox.x); + + // 到这里已经校验成功,点击进行登录 + await page.waitForTimeout(300); + await page.getByRole('button', { name: 'login' }).click(); +} diff --git a/apps/vben5/playground/index.html b/apps/vben5/playground/index.html new file mode 100644 index 000000000..ca5326990 --- /dev/null +++ b/apps/vben5/playground/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + <%= VITE_APP_TITLE %> + + + + +
+ + + diff --git a/apps/vben5/playground/package.json b/apps/vben5/playground/package.json new file mode 100644 index 000000000..bde08f8c2 --- /dev/null +++ b/apps/vben5/playground/package.json @@ -0,0 +1,54 @@ +{ + "name": "@vben/playground", + "version": "5.4.8", + "homepage": "https://vben.pro", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "playground" + }, + "license": "MIT", + "author": { + "name": "vben", + "email": "ann.vben@gmail.com", + "url": "https://github.com/anncwb" + }, + "type": "module", + "scripts": { + "build": "pnpm vite build --mode production", + "build:analyze": "pnpm vite build --mode analyze", + "dev": "pnpm vite --mode development", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit --skipLibCheck", + "test:e2e": "playwright test", + "test:e2e-ui": "playwright test --ui", + "test:e2e-codegen": "playwright codegen" + }, + "imports": { + "#/*": "./src/*" + }, + "dependencies": { + "@tanstack/vue-query": "catalog:", + "@vben/access": "workspace:*", + "@vben/common-ui": "workspace:*", + "@vben/constants": "workspace:*", + "@vben/hooks": "workspace:*", + "@vben/icons": "workspace:*", + "@vben/layouts": "workspace:*", + "@vben/locales": "workspace:*", + "@vben/plugins": "workspace:*", + "@vben/preferences": "workspace:*", + "@vben/request": "workspace:*", + "@vben/stores": "workspace:*", + "@vben/styles": "workspace:*", + "@vben/types": "workspace:*", + "@vben/utils": "workspace:*", + "@vueuse/core": "catalog:", + "ant-design-vue": "catalog:", + "dayjs": "catalog:", + "pinia": "catalog:", + "vue": "catalog:", + "vue-router": "catalog:" + } +} diff --git a/apps/vben5/playground/playwright.config.ts b/apps/vben5/playground/playwright.config.ts new file mode 100644 index 000000000..ca0c90852 --- /dev/null +++ b/apps/vben5/playground/playwright.config.ts @@ -0,0 +1,108 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + outputDir: 'node_modules/.e2e/test-results/', + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['list'], + ['html', { outputFolder: 'node_modules/.e2e/test-results' }], + ], + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + testDir: './__tests__/e2e', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:5555', + /* Only on CI systems run the tests headless */ + headless: !!process.env.CI, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + }, + + /* Run your local dev server before starting the tests */ + webServer: { + command: process.env.CI ? 'pnpm preview --port 5555' : 'pnpm dev', + port: 5555, + reuseExistingServer: !process.env.CI, + }, + + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, +}; + +export default config; diff --git a/apps/vben5/playground/postcss.config.mjs b/apps/vben5/playground/postcss.config.mjs new file mode 100644 index 000000000..3d8070455 --- /dev/null +++ b/apps/vben5/playground/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/apps/vben5/playground/public/favicon.ico b/apps/vben5/playground/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fcf9818e2cf855039b272bdbfbb202d3ff3fa159 GIT binary patch literal 5430 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 + component: T, + type: 'input' | 'select', +) => { + return (props: any, { attrs, slots }: Omit) => { + const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`); + return h(component, { ...props, ...attrs, placeholder }, slots); + }; +}; + +// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 +export type ComponentType = + | 'AutoComplete' + | 'Checkbox' + | 'CheckboxGroup' + | 'DatePicker' + | 'DefaultButton' + | 'Divider' + | 'Input' + | 'InputNumber' + | 'InputPassword' + | 'Mentions' + | 'PrimaryButton' + | 'Radio' + | 'RadioGroup' + | 'RangePicker' + | 'Rate' + | 'Select' + | 'Space' + | 'Switch' + | 'Textarea' + | 'TimePicker' + | 'TreeSelect' + | 'Upload' + | BaseFormComponentType; + +async function initComponentAdapter() { + const components: Partial> = { + // 如果你的组件体积比较大,可以使用异步加载 + // Button: () => + // import('xxx').then((res) => res.Button), + + AutoComplete, + Checkbox, + CheckboxGroup, + DatePicker, + // 自定义默认按钮 + DefaultButton: (props, { attrs, slots }) => { + return h(Button, { ...props, attrs, type: 'default' }, slots); + }, + Divider, + Input: withDefaultPlaceholder(Input, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + InputPassword: withDefaultPlaceholder(InputPassword, 'input'), + Mentions: withDefaultPlaceholder(Mentions, 'input'), + // 自定义主要按钮 + PrimaryButton: (props, { attrs, slots }) => { + return h(Button, { ...props, attrs, type: 'primary' }, slots); + }, + Radio, + RadioGroup, + RangePicker, + Rate, + Select: withDefaultPlaceholder(Select, 'select'), + Space, + Switch, + Textarea: withDefaultPlaceholder(Textarea, 'input'), + TimePicker, + TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), + Upload, + }; + + // 将组件注册到全局共享状态中 + globalShareState.setComponents(components); + + // 定义全局共享状态中的消息提示 + globalShareState.defineMessage({ + // 复制成功消息提示 + copyPreferencesSuccess: (title, content) => { + notification.success({ + description: content, + message: title, + placement: 'bottomRight', + }); + }, + }); +} + +export { initComponentAdapter }; diff --git a/apps/vben5/playground/src/adapter/form.ts b/apps/vben5/playground/src/adapter/form.ts new file mode 100644 index 000000000..dfe8fed0d --- /dev/null +++ b/apps/vben5/playground/src/adapter/form.ts @@ -0,0 +1,45 @@ +import type { + VbenFormSchema as FormSchema, + VbenFormProps, +} from '@vben/common-ui'; + +import type { ComponentType } from './component'; + +import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; +import { $t } from '@vben/locales'; + +setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, + }, + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, + }, +}); + +const useVbenForm = useForm; + +export { useVbenForm, z }; +export type VbenFormSchema = FormSchema; +export type { VbenFormProps }; diff --git a/apps/vben5/playground/src/adapter/vxe-table.ts b/apps/vben5/playground/src/adapter/vxe-table.ts new file mode 100644 index 000000000..8f68b15d8 --- /dev/null +++ b/apps/vben5/playground/src/adapter/vxe-table.ts @@ -0,0 +1,68 @@ +import { h } from 'vue'; + +import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; + +import { Button, Image } from 'ant-design-vue'; + +import { useVbenForm } from './form'; + +setupVbenVxeTable({ + configVxeTable: (vxeUI) => { + vxeUI.setConfig({ + grid: { + align: 'center', + border: false, + columnConfig: { + resizable: true, + }, + + formConfig: { + // 全局禁用vxe-table的表单配置,使用formOptions + enabled: false, + }, + minHeight: 180, + proxyConfig: { + autoLoad: true, + response: { + result: 'items', + total: 'total', + list: 'items', + }, + showActiveMsg: true, + showResponseMsg: false, + }, + round: true, + showOverflow: true, + size: 'small', + }, + }); + + // 表格配置项可以用 cellRender: { name: 'CellImage' }, + vxeUI.renderer.add('CellImage', { + renderTableDefault(_renderOpts, params) { + const { column, row } = params; + return h(Image, { src: row[column.field] }); + }, + }); + + // 表格配置项可以用 cellRender: { name: 'CellLink' }, + vxeUI.renderer.add('CellLink', { + renderTableDefault(renderOpts) { + const { props } = renderOpts; + return h( + Button, + { size: 'small', type: 'link' }, + { default: () => props?.text }, + ); + }, + }); + + // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化 + // vxeUI.formats.add + }, + useVbenForm, +}); + +export { useVbenVxeGrid }; + +export type * from '@vben/plugins/vxe-table'; diff --git a/apps/vben5/playground/src/api/core/auth.ts b/apps/vben5/playground/src/api/core/auth.ts new file mode 100644 index 000000000..71d9f9943 --- /dev/null +++ b/apps/vben5/playground/src/api/core/auth.ts @@ -0,0 +1,51 @@ +import { baseRequestClient, requestClient } from '#/api/request'; + +export namespace AuthApi { + /** 登录接口参数 */ + export interface LoginParams { + password?: string; + username?: string; + } + + /** 登录接口返回值 */ + export interface LoginResult { + accessToken: string; + } + + export interface RefreshTokenResult { + data: string; + status: number; + } +} + +/** + * 登录 + */ +export async function loginApi(data: AuthApi.LoginParams) { + return requestClient.post('/auth/login', data); +} + +/** + * 刷新accessToken + */ +export async function refreshTokenApi() { + return baseRequestClient.post('/auth/refresh', { + withCredentials: true, + }); +} + +/** + * 退出登录 + */ +export async function logoutApi() { + return baseRequestClient.post('/auth/logout', { + withCredentials: true, + }); +} + +/** + * 获取用户权限码 + */ +export async function getAccessCodesApi() { + return requestClient.get('/auth/codes'); +} diff --git a/apps/vben5/playground/src/api/core/index.ts b/apps/vben5/playground/src/api/core/index.ts new file mode 100644 index 000000000..28a5aef47 --- /dev/null +++ b/apps/vben5/playground/src/api/core/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './menu'; +export * from './user'; diff --git a/apps/vben5/playground/src/api/core/menu.ts b/apps/vben5/playground/src/api/core/menu.ts new file mode 100644 index 000000000..9ef60b11c --- /dev/null +++ b/apps/vben5/playground/src/api/core/menu.ts @@ -0,0 +1,10 @@ +import type { RouteRecordStringComponent } from '@vben/types'; + +import { requestClient } from '#/api/request'; + +/** + * 获取用户所有菜单 + */ +export async function getAllMenusApi() { + return requestClient.get('/menu/all'); +} diff --git a/apps/vben5/playground/src/api/core/user.ts b/apps/vben5/playground/src/api/core/user.ts new file mode 100644 index 000000000..7e28ea848 --- /dev/null +++ b/apps/vben5/playground/src/api/core/user.ts @@ -0,0 +1,10 @@ +import type { UserInfo } from '@vben/types'; + +import { requestClient } from '#/api/request'; + +/** + * 获取用户信息 + */ +export async function getUserInfoApi() { + return requestClient.get('/user/info'); +} diff --git a/apps/vben5/playground/src/api/examples/index.ts b/apps/vben5/playground/src/api/examples/index.ts new file mode 100644 index 000000000..c830b81f3 --- /dev/null +++ b/apps/vben5/playground/src/api/examples/index.ts @@ -0,0 +1,2 @@ +export * from './status'; +export * from './table'; diff --git a/apps/vben5/playground/src/api/examples/status.ts b/apps/vben5/playground/src/api/examples/status.ts new file mode 100644 index 000000000..4a75fe7e7 --- /dev/null +++ b/apps/vben5/playground/src/api/examples/status.ts @@ -0,0 +1,10 @@ +import { requestClient } from '#/api/request'; + +/** + * 模拟任意状态码 + */ +async function getMockStatusApi(status: string) { + return requestClient.get('/status', { params: { status } }); +} + +export { getMockStatusApi }; diff --git a/apps/vben5/playground/src/api/examples/table.ts b/apps/vben5/playground/src/api/examples/table.ts new file mode 100644 index 000000000..4739ca986 --- /dev/null +++ b/apps/vben5/playground/src/api/examples/table.ts @@ -0,0 +1,18 @@ +import { requestClient } from '#/api/request'; + +export namespace DemoTableApi { + export interface PageFetchParams { + [key: string]: any; + page: number; + pageSize: number; + } +} + +/** + * 获取示例表格数据 + */ +async function getExampleTableApi(params: DemoTableApi.PageFetchParams) { + return requestClient.get('/table/list', { params }); +} + +export { getExampleTableApi }; diff --git a/apps/vben5/playground/src/api/index.ts b/apps/vben5/playground/src/api/index.ts new file mode 100644 index 000000000..ab806c621 --- /dev/null +++ b/apps/vben5/playground/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './core'; +export * from './examples'; diff --git a/apps/vben5/playground/src/api/request.ts b/apps/vben5/playground/src/api/request.ts new file mode 100644 index 000000000..b1f6c060f --- /dev/null +++ b/apps/vben5/playground/src/api/request.ts @@ -0,0 +1,114 @@ +/** + * 该文件可自行根据业务逻辑进行调整 + */ +import type { HttpResponse } from '@vben/request'; + +import { useAppConfig } from '@vben/hooks'; +import { preferences } from '@vben/preferences'; +import { + authenticateResponseInterceptor, + errorMessageResponseInterceptor, + RequestClient, +} from '@vben/request'; +import { useAccessStore } from '@vben/stores'; + +import { message } from 'ant-design-vue'; + +import { useAuthStore } from '#/store'; + +import { refreshTokenApi } from './core'; + +const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); + +function createRequestClient(baseURL: string) { + const client = new RequestClient({ + baseURL, + }); + + /** + * 重新认证逻辑 + */ + async function doReAuthenticate() { + console.warn('Access token or refresh token is invalid or expired. '); + const accessStore = useAccessStore(); + const authStore = useAuthStore(); + accessStore.setAccessToken(null); + if ( + preferences.app.loginExpiredMode === 'modal' && + accessStore.isAccessChecked + ) { + accessStore.setLoginExpired(true); + } else { + await authStore.logout(); + } + } + + /** + * 刷新token逻辑 + */ + async function doRefreshToken() { + const accessStore = useAccessStore(); + const resp = await refreshTokenApi(); + const newToken = resp.data; + accessStore.setAccessToken(newToken); + return newToken; + } + + function formatToken(token: null | string) { + return token ? `Bearer ${token}` : null; + } + + // 请求头处理 + client.addRequestInterceptor({ + fulfilled: async (config) => { + const accessStore = useAccessStore(); + + config.headers.Authorization = formatToken(accessStore.accessToken); + config.headers['Accept-Language'] = preferences.app.locale; + return config; + }, + }); + + // response数据解构 + client.addResponseInterceptor({ + fulfilled: (response) => { + const { data: responseData, status } = response; + + const { code, data } = responseData; + + if (status >= 200 && status < 400 && code === 0) { + return data; + } + throw Object.assign({}, response, { response }); + }, + }); + + // token过期的处理 + client.addResponseInterceptor( + authenticateResponseInterceptor({ + client, + doReAuthenticate, + doRefreshToken, + enableRefreshToken: preferences.app.enableRefreshToken, + formatToken, + }), + ); + + // 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里 + client.addResponseInterceptor( + errorMessageResponseInterceptor((msg: string, error) => { + // 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg + // 当前mock接口返回的错误字段是 error 或者 message + const responseData = error?.response?.data ?? {}; + const errorMessage = responseData?.error ?? responseData?.message ?? ''; + // 如果没有错误信息,则会根据状态码进行提示 + message.error(errorMessage || msg); + }), + ); + + return client; +} + +export const requestClient = createRequestClient(apiURL); + +export const baseRequestClient = new RequestClient({ baseURL: apiURL }); diff --git a/apps/vben5/playground/src/app.vue b/apps/vben5/playground/src/app.vue new file mode 100644 index 000000000..bbaccce13 --- /dev/null +++ b/apps/vben5/playground/src/app.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/vben5/playground/src/bootstrap.ts b/apps/vben5/playground/src/bootstrap.ts new file mode 100644 index 000000000..09b8cfa8b --- /dev/null +++ b/apps/vben5/playground/src/bootstrap.ts @@ -0,0 +1,52 @@ +import { createApp, watchEffect } from 'vue'; + +import { registerAccessDirective } from '@vben/access'; +import { preferences } from '@vben/preferences'; +import { initStores } from '@vben/stores'; +import '@vben/styles'; +import '@vben/styles/antd'; + +import { VueQueryPlugin } from '@tanstack/vue-query'; +import { useTitle } from '@vueuse/core'; + +import { $t, setupI18n } from '#/locales'; +import { router } from '#/router'; + +import { initComponentAdapter } from './adapter/component'; +import App from './app.vue'; + +async function bootstrap(namespace: string) { + // 初始化组件适配器 + await initComponentAdapter(); + + const app = createApp(App); + + // 国际化 i18n 配置 + await setupI18n(app); + + // 配置 pinia-tore + await initStores(app, { namespace }); + + // 安装权限指令 + registerAccessDirective(app); + + // 配置路由及路由守卫 + app.use(router); + + // 配置@tanstack/vue-query + app.use(VueQueryPlugin); + + // 动态更新标题 + watchEffect(() => { + if (preferences.app.dynamicTitle) { + const routeTitle = router.currentRoute.value.meta?.title; + const pageTitle = + (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name; + useTitle(pageTitle); + } + }); + + app.mount('#app'); +} + +export { bootstrap }; diff --git a/apps/vben5/playground/src/layouts/auth.vue b/apps/vben5/playground/src/layouts/auth.vue new file mode 100644 index 000000000..18d415bc7 --- /dev/null +++ b/apps/vben5/playground/src/layouts/auth.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/vben5/playground/src/layouts/basic.vue b/apps/vben5/playground/src/layouts/basic.vue new file mode 100644 index 000000000..f75b3ddc6 --- /dev/null +++ b/apps/vben5/playground/src/layouts/basic.vue @@ -0,0 +1,158 @@ + + + diff --git a/apps/vben5/playground/src/layouts/index.ts b/apps/vben5/playground/src/layouts/index.ts new file mode 100644 index 000000000..a43207805 --- /dev/null +++ b/apps/vben5/playground/src/layouts/index.ts @@ -0,0 +1,6 @@ +const BasicLayout = () => import('./basic.vue'); +const AuthPageLayout = () => import('./auth.vue'); + +const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView); + +export { AuthPageLayout, BasicLayout, IFrameView }; diff --git a/apps/vben5/playground/src/locales/README.md b/apps/vben5/playground/src/locales/README.md new file mode 100644 index 000000000..7b451032e --- /dev/null +++ b/apps/vben5/playground/src/locales/README.md @@ -0,0 +1,3 @@ +# locale + +每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。 diff --git a/apps/vben5/playground/src/locales/index.ts b/apps/vben5/playground/src/locales/index.ts new file mode 100644 index 000000000..1972e06ee --- /dev/null +++ b/apps/vben5/playground/src/locales/index.ts @@ -0,0 +1,100 @@ +import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales'; +import type { Locale } from 'ant-design-vue/es/locale'; + +import type { App } from 'vue'; +import { ref } from 'vue'; + +import { + $t, + setupI18n as coreSetup, + loadLocalesMapFromDir, +} from '@vben/locales'; +import { preferences } from '@vben/preferences'; + +import antdEnLocale from 'ant-design-vue/es/locale/en_US'; +import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN'; +import dayjs from 'dayjs'; + +const antdLocale = ref(antdDefaultLocale); + +const modules = import.meta.glob('./langs/**/*.json'); + +const localesMap = loadLocalesMapFromDir( + /\.\/langs\/([^/]+)\/(.*)\.json$/, + modules, +); +/** + * 加载应用特有的语言包 + * 这里也可以改造为从服务端获取翻译数据 + * @param lang + */ +async function loadMessages(lang: SupportedLanguagesType) { + const [appLocaleMessages] = await Promise.all([ + localesMap[lang]?.(), + loadThirdPartyMessage(lang), + ]); + return appLocaleMessages?.default; +} + +/** + * 加载第三方组件库的语言包 + * @param lang + */ +async function loadThirdPartyMessage(lang: SupportedLanguagesType) { + await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]); +} + +/** + * 加载dayjs的语言包 + * @param lang + */ +async function loadDayjsLocale(lang: SupportedLanguagesType) { + let locale; + switch (lang) { + case 'en-US': { + locale = await import('dayjs/locale/en'); + break; + } + case 'zh-CN': { + locale = await import('dayjs/locale/zh-cn'); + break; + } + // 默认使用英语 + default: { + locale = await import('dayjs/locale/en'); + } + } + if (locale) { + dayjs.locale(locale); + } else { + console.error(`Failed to load dayjs locale for ${lang}`); + } +} + +/** + * 加载antd的语言包 + * @param lang + */ +async function loadAntdLocale(lang: SupportedLanguagesType) { + switch (lang) { + case 'en-US': { + antdLocale.value = antdEnLocale; + break; + } + case 'zh-CN': { + antdLocale.value = antdDefaultLocale; + break; + } + } +} + +async function setupI18n(app: App, options: LocaleSetupOptions = {}) { + await coreSetup(app, { + defaultLocale: preferences.app.locale, + loadMessages, + missingWarn: !import.meta.env.PROD, + ...options, + }); +} + +export { $t, antdLocale, setupI18n }; diff --git a/apps/vben5/playground/src/locales/langs/en-US/demos.json b/apps/vben5/playground/src/locales/langs/en-US/demos.json new file mode 100644 index 000000000..44b12780e --- /dev/null +++ b/apps/vben5/playground/src/locales/langs/en-US/demos.json @@ -0,0 +1,70 @@ +{ + "title": "Demos", + "access": { + "frontendPermissions": "Frontend Permissions", + "backendPermissions": "Backend Permissions", + "pageAccess": "Page Access", + "buttonControl": "Button Control", + "menuVisible403": "Menu Visible(403)", + "superVisible": "Visible to Super", + "adminVisible": "Visible to Admin", + "userVisible": "Visible to User" + }, + "nested": { + "title": "Nested Menu", + "menu1": "Menu 1", + "menu2": "Menu 2", + "menu2_1": "Menu 2-1", + "menu3": "Menu 3", + "menu3_1": "Menu 3-1", + "menu3_2": "Menu 3-2", + "menu3_2_1": "Menu 3-2-1" + }, + "outside": { + "title": "External Pages", + "embedded": "Embedded", + "externalLink": "External Link" + }, + "badge": { + "title": "Menu Badge", + "dot": "Dot Badge", + "text": "Text Badge", + "color": "Badge Color" + }, + "activeIcon": { + "title": "Active Menu Icon", + "children": "Children Active Icon" + }, + "fallback": { + "title": "Fallback Page" + }, + "features": { + "title": "Features", + "hideChildrenInMenu": "Hide Menu Children", + "loginExpired": "Login Expired", + "icons": "Icons", + "watermark": "Watermark", + "tabs": "Tabs", + "tabDetail": "Tab Detail Page", + "fullScreen": "FullScreen", + "clipboard": "Clipboard", + "menuWithQuery": "Menu With Query", + "openInNewWindow": "Open in New Window", + "fileDownload": "File Download" + }, + "breadcrumb": { + "navigation": "Breadcrumb Navigation", + "lateral": "Lateral Mode", + "lateralDetail": "Lateral Mode Detail", + "level": "Level Mode", + "levelDetail": "Level Mode Detail" + }, + "vben": { + "title": "Project", + "about": "About", + "document": "Document", + "antdv": "Ant Design Vue Version", + "naive-ui": "Naive UI Version", + "element-plus": "Element Plus Version" + } +} diff --git a/apps/vben5/playground/src/locales/langs/en-US/examples.json b/apps/vben5/playground/src/locales/langs/en-US/examples.json new file mode 100644 index 000000000..490a17a3b --- /dev/null +++ b/apps/vben5/playground/src/locales/langs/en-US/examples.json @@ -0,0 +1,60 @@ +{ + "title": "Examples", + "modal": { + "title": "Modal" + }, + "drawer": { + "title": "Drawer" + }, + "ellipsis": { + "title": "EllipsisText" + }, + "form": { + "title": "Form", + "basic": "Basic Form", + "query": "Query Form", + "rules": "Form Rules", + "dynamic": "Dynamic Form", + "custom": "Custom Component", + "api": "Api", + "merge": "Merge Form" + }, + "vxeTable": { + "title": "Vxe Table", + "basic": "Basic Table", + "remote": "Remote Load", + "tree": "Tree Table", + "fixed": "Fixed Header/Column", + "virtual": "Virtual Scroll", + "editCell": "Edit Cell", + "editRow": "Edit Row", + "custom-cell": "Custom Cell", + "form": "Form Table" + }, + "captcha": { + "title": "Captcha", + "pointSelection": "Point Selection Captcha", + "sliderCaptcha": "Slider Captcha", + "sliderRotateCaptcha": "Rotate Captcha", + "captchaCardTitle": "Please complete the security verification", + "pageDescription": "Verify user identity by clicking on specific locations in the image.", + "pageTitle": "Captcha Component Example", + "basic": "Basic Usage", + "titlePlaceholder": "Captcha Title Text", + "captchaImageUrlPlaceholder": "Captcha Image (supports img tag src attribute value)", + "hintImage": "Hint Image", + "hintText": "Hint Text", + "hintImagePlaceholder": "Hint Image (supports img tag src attribute value)", + "hintTextPlaceholder": "Hint Text", + "showConfirm": "Show Confirm", + "hideConfirm": "Hide Confirm", + "widthPlaceholder": "Captcha Image Width Default 300px", + "heightPlaceholder": "Captcha Image Height Default 220px", + "paddingXPlaceholder": "Horizontal Padding Default 12px", + "paddingYPlaceholder": "Vertical Padding Default 16px", + "index": "Index:", + "timestamp": "Timestamp:", + "x": "x:", + "y": "y:" + } +} diff --git a/apps/vben5/playground/src/locales/langs/en-US/page.json b/apps/vben5/playground/src/locales/langs/en-US/page.json new file mode 100644 index 000000000..618a258c0 --- /dev/null +++ b/apps/vben5/playground/src/locales/langs/en-US/page.json @@ -0,0 +1,14 @@ +{ + "auth": { + "login": "Login", + "register": "Register", + "codeLogin": "Code Login", + "qrcodeLogin": "Qr Code Login", + "forgetPassword": "Forget Password" + }, + "dashboard": { + "title": "Dashboard", + "analytics": "Analytics", + "workspace": "Workspace" + } +} diff --git a/apps/vben5/playground/src/locales/langs/zh-CN/demos.json b/apps/vben5/playground/src/locales/langs/zh-CN/demos.json new file mode 100644 index 000000000..254e072b5 --- /dev/null +++ b/apps/vben5/playground/src/locales/langs/zh-CN/demos.json @@ -0,0 +1,70 @@ +{ + "title": "演示", + "access": { + "frontendPermissions": "前端权限", + "backendPermissions": "后端权限", + "pageAccess": "页面访问", + "buttonControl": "按钮控制", + "menuVisible403": "菜单可见(403)", + "superVisible": "Super 可见", + "adminVisible": "Admin 可见", + "userVisible": "User 可见" + }, + "nested": { + "title": "嵌套菜单", + "menu1": "菜单 1", + "menu2": "菜单 2", + "menu2_1": "菜单 2-1", + "menu3": "菜单 3", + "menu3_1": "菜单 3-1", + "menu3_2": "菜单 3-2", + "menu3_2_1": "菜单 3-2-1" + }, + "outside": { + "title": "外部页面", + "embedded": "内嵌", + "externalLink": "外链" + }, + "badge": { + "title": "菜单徽标", + "dot": "点徽标", + "text": "文本徽标", + "color": "徽标颜色" + }, + "activeIcon": { + "title": "菜单激活图标", + "children": "子级激活图标" + }, + "fallback": { + "title": "缺省页" + }, + "features": { + "title": "功能", + "hideChildrenInMenu": "隐藏子菜单", + "loginExpired": "登录过期", + "icons": "图标", + "watermark": "水印", + "tabs": "标签页", + "tabDetail": "标签详情页", + "fullScreen": "全屏", + "clipboard": "剪贴板", + "menuWithQuery": "带参菜单", + "openInNewWindow": "新窗口打开", + "fileDownload": "文件下载" + }, + "breadcrumb": { + "navigation": "面包屑导航", + "lateral": "平级模式", + "level": "层级模式", + "levelDetail": "层级模式详情", + "lateralDetail": "平级模式详情" + }, + "vben": { + "title": "项目", + "about": "关于", + "document": "文档", + "antdv": "Ant Design Vue 版本", + "naive-ui": "Naive UI 版本", + "element-plus": "Element Plus 版本" + } +} diff --git a/apps/vben5/playground/src/locales/langs/zh-CN/examples.json b/apps/vben5/playground/src/locales/langs/zh-CN/examples.json new file mode 100644 index 000000000..227ec4943 --- /dev/null +++ b/apps/vben5/playground/src/locales/langs/zh-CN/examples.json @@ -0,0 +1,63 @@ +{ + "title": "示例", + "modal": { + "title": "弹窗" + }, + "drawer": { + "title": "抽屉" + }, + "ellipsis": { + "title": "文本省略" + }, + "resize": { + "title": "拖动调整" + }, + "form": { + "title": "表单", + "basic": "基础表单", + "query": "查询表单", + "rules": "表单校验", + "dynamic": "动态表单", + "custom": "自定义组件", + "api": "Api", + "merge": "合并表单" + }, + "vxeTable": { + "title": "Vxe 表格", + "basic": "基础表格", + "remote": "远程加载", + "tree": "树形表格", + "fixed": "固定表头/列", + "virtual": "虚拟滚动", + "editCell": "单元格编辑", + "editRow": "行编辑", + "custom-cell": "自定义单元格", + "form": "搜索表单" + }, + "captcha": { + "title": "验证码", + "pointSelection": "点选验证", + "sliderCaptcha": "滑块验证", + "sliderRotateCaptcha": "旋转验证", + "captchaCardTitle": "请完成安全验证", + "pageDescription": "通过点击图片中的特定位置来验证用户身份。", + "pageTitle": "验证码组件示例", + "basic": "基本使用", + "titlePlaceholder": "验证码标题文案", + "captchaImageUrlPlaceholder": "验证码图片(支持img标签src属性值)", + "hintImage": "提示图片", + "hintText": "提示文本", + "hintImagePlaceholder": "提示图片(支持img标签src属性值)", + "hintTextPlaceholder": "提示文本", + "showConfirm": "展示确认", + "hideConfirm": "隐藏确认", + "widthPlaceholder": "验证码图片宽度 默认300px", + "heightPlaceholder": "验证码图片高度 默认220px", + "paddingXPlaceholder": "水平内边距 默认12px", + "paddingYPlaceholder": "垂直内边距 默认16px", + "index": "索引:", + "timestamp": "时间戳:", + "x": "x:", + "y": "y:" + } +} diff --git a/apps/vben5/playground/src/locales/langs/zh-CN/page.json b/apps/vben5/playground/src/locales/langs/zh-CN/page.json new file mode 100644 index 000000000..4cb67081c --- /dev/null +++ b/apps/vben5/playground/src/locales/langs/zh-CN/page.json @@ -0,0 +1,14 @@ +{ + "auth": { + "login": "登录", + "register": "注册", + "codeLogin": "验证码登录", + "qrcodeLogin": "二维码登录", + "forgetPassword": "忘记密码" + }, + "dashboard": { + "title": "概览", + "analytics": "分析页", + "workspace": "工作台" + } +} diff --git a/apps/vben5/playground/src/main.ts b/apps/vben5/playground/src/main.ts new file mode 100644 index 000000000..5d728a02a --- /dev/null +++ b/apps/vben5/playground/src/main.ts @@ -0,0 +1,31 @@ +import { initPreferences } from '@vben/preferences'; +import { unmountGlobalLoading } from '@vben/utils'; + +import { overridesPreferences } from './preferences'; + +/** + * 应用初始化完成之后再进行页面加载渲染 + */ +async function initApplication() { + // name用于指定项目唯一标识 + // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据 + const env = import.meta.env.PROD ? 'prod' : 'dev'; + const appVersion = import.meta.env.VITE_APP_VERSION; + const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`; + + // app偏好设置初始化 + await initPreferences({ + namespace, + overrides: overridesPreferences, + }); + + // 启动应用并挂载 + // vue应用主要逻辑及视图 + const { bootstrap } = await import('./bootstrap'); + await bootstrap(namespace); + + // 移除并销毁loading + unmountGlobalLoading(); +} + +initApplication(); diff --git a/apps/vben5/playground/src/preferences.ts b/apps/vben5/playground/src/preferences.ts new file mode 100644 index 000000000..b2e9ace43 --- /dev/null +++ b/apps/vben5/playground/src/preferences.ts @@ -0,0 +1,13 @@ +import { defineOverridesPreferences } from '@vben/preferences'; + +/** + * @description 项目配置文件 + * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置 + * !!! 更改配置后请清空缓存,否则可能不生效 + */ +export const overridesPreferences = defineOverridesPreferences({ + // overrides + app: { + name: import.meta.env.VITE_APP_TITLE, + }, +}); diff --git a/apps/vben5/playground/src/router/access.ts b/apps/vben5/playground/src/router/access.ts new file mode 100644 index 000000000..3a48be237 --- /dev/null +++ b/apps/vben5/playground/src/router/access.ts @@ -0,0 +1,42 @@ +import type { + ComponentRecordType, + GenerateMenuAndRoutesOptions, +} from '@vben/types'; + +import { generateAccessible } from '@vben/access'; +import { preferences } from '@vben/preferences'; + +import { message } from 'ant-design-vue'; + +import { getAllMenusApi } from '#/api'; +import { BasicLayout, IFrameView } from '#/layouts'; +import { $t } from '#/locales'; + +const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); + +async function generateAccess(options: GenerateMenuAndRoutesOptions) { + const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); + + const layoutMap: ComponentRecordType = { + BasicLayout, + IFrameView, + }; + + return await generateAccessible(preferences.app.accessMode, { + ...options, + fetchMenuListAsync: async () => { + message.loading({ + content: `${$t('common.loadingMenu')}...`, + duration: 1.5, + }); + return await getAllMenusApi(); + }, + // 可以指定没有权限跳转403页面 + forbiddenComponent, + // 如果 route.meta.menuVisibleWithForbidden = true + layoutMap, + pageMap, + }); +} + +export { generateAccess }; diff --git a/apps/vben5/playground/src/router/guard.ts b/apps/vben5/playground/src/router/guard.ts new file mode 100644 index 000000000..bddb28db8 --- /dev/null +++ b/apps/vben5/playground/src/router/guard.ts @@ -0,0 +1,123 @@ +import type { Router } from 'vue-router'; + +import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { preferences } from '@vben/preferences'; +import { useAccessStore, useUserStore } from '@vben/stores'; +import { startProgress, stopProgress } from '@vben/utils'; + +import { accessRoutes, coreRouteNames } from '#/router/routes'; +import { useAuthStore } from '#/store'; + +import { generateAccess } from './access'; + +/** + * 通用守卫配置 + * @param router + */ +function setupCommonGuard(router: Router) { + // 记录已经加载的页面 + const loadedPaths = new Set(); + + router.beforeEach(async (to) => { + to.meta.loaded = loadedPaths.has(to.path); + + // 页面加载进度条 + if (!to.meta.loaded && preferences.transition.progress) { + startProgress(); + } + return true; + }); + + router.afterEach((to) => { + // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行 + loadedPaths.add(to.path); + + // 关闭页面加载进度条 + if (preferences.transition.progress) { + stopProgress(); + } + }); +} + +/** + * 权限访问守卫配置 + * @param router + */ +function setupAccessGuard(router: Router) { + router.beforeEach(async (to, from) => { + const accessStore = useAccessStore(); + const userStore = useUserStore(); + const authStore = useAuthStore(); + // 基本路由,这些路由不需要进入权限拦截 + if (coreRouteNames.includes(to.name as string)) { + if (to.path === LOGIN_PATH && accessStore.accessToken) { + return decodeURIComponent( + (to.query?.redirect as string) || DEFAULT_HOME_PATH, + ); + } + return true; + } + + // accessToken 检查 + if (!accessStore.accessToken) { + // 明确声明忽略权限访问权限,则可以访问 + if (to.meta.ignoreAccess) { + return true; + } + + // 没有访问权限,跳转登录页面 + if (to.fullPath !== LOGIN_PATH) { + return { + path: LOGIN_PATH, + // 如不需要,直接删除 query + query: { redirect: encodeURIComponent(to.fullPath) }, + // 携带当前跳转的页面,登录后重新跳转该页面 + replace: true, + }; + } + return to; + } + + // 是否已经生成过动态路由 + if (accessStore.isAccessChecked) { + return true; + } + + // 生成路由表 + // 当前登录用户拥有的角色标识列表 + const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); + const userRoles = userInfo.roles ?? []; + + // 生成菜单和路由 + const { accessibleMenus, accessibleRoutes } = await generateAccess({ + roles: userRoles, + router, + // 则会在菜单中显示,但是访问会被重定向到403 + routes: accessRoutes, + }); + + // 保存菜单信息和路由信息 + accessStore.setAccessMenus(accessibleMenus); + accessStore.setAccessRoutes(accessibleRoutes); + accessStore.setIsAccessChecked(true); + const redirectPath = (from.query.redirect ?? to.fullPath) as string; + + return { + ...router.resolve(decodeURIComponent(redirectPath)), + replace: true, + }; + }); +} + +/** + * 项目守卫配置 + * @param router + */ +function createRouterGuard(router: Router) { + /** 通用 */ + setupCommonGuard(router); + /** 权限访问 */ + setupAccessGuard(router); +} + +export { createRouterGuard }; diff --git a/apps/vben5/playground/src/router/index.ts b/apps/vben5/playground/src/router/index.ts new file mode 100644 index 000000000..484023034 --- /dev/null +++ b/apps/vben5/playground/src/router/index.ts @@ -0,0 +1,37 @@ +import { + createRouter, + createWebHashHistory, + createWebHistory, +} from 'vue-router'; + +import { resetStaticRoutes } from '@vben/utils'; + +import { createRouterGuard } from './guard'; +import { routes } from './routes'; + +/** + * @zh_CN 创建vue-router实例 + */ +const router = createRouter({ + history: + import.meta.env.VITE_ROUTER_HISTORY === 'hash' + ? createWebHashHistory(import.meta.env.VITE_BASE) + : createWebHistory(import.meta.env.VITE_BASE), + // 应该添加到路由的初始路由列表。 + routes, + scrollBehavior: (to, _from, savedPosition) => { + if (savedPosition) { + return savedPosition; + } + return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 }; + }, + // 是否应该禁止尾部斜杠。 + // strict: true, +}); + +const resetRoutes = () => resetStaticRoutes(router, routes); + +// 创建路由守卫 +createRouterGuard(router); + +export { resetRoutes, router }; diff --git a/apps/vben5/playground/src/router/routes/core.ts b/apps/vben5/playground/src/router/routes/core.ts new file mode 100644 index 000000000..fe030a9a2 --- /dev/null +++ b/apps/vben5/playground/src/router/routes/core.ts @@ -0,0 +1,88 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; + +import { AuthPageLayout } from '#/layouts'; +import { $t } from '#/locales'; +import Login from '#/views/_core/authentication/login.vue'; + +/** 全局404页面 */ +const fallbackNotFoundRoute: RouteRecordRaw = { + component: () => import('#/views/_core/fallback/not-found.vue'), + meta: { + hideInBreadcrumb: true, + hideInMenu: true, + hideInTab: true, + title: '404', + }, + name: 'FallbackNotFound', + path: '/:path(.*)*', +}; + +/** 基本路由,这些路由是必须存在的 */ +const coreRoutes: RouteRecordRaw[] = [ + { + meta: { + title: 'Root', + }, + name: 'Root', + path: '/', + redirect: DEFAULT_HOME_PATH, + }, + { + component: AuthPageLayout, + meta: { + hideInTab: true, + title: 'Authentication', + }, + name: 'Authentication', + path: '/auth', + redirect: LOGIN_PATH, + children: [ + { + name: 'Login', + path: 'login', + component: Login, + meta: { + title: $t('page.auth.login'), + }, + }, + { + name: 'CodeLogin', + path: 'code-login', + component: () => import('#/views/_core/authentication/code-login.vue'), + meta: { + title: $t('page.auth.codeLogin'), + }, + }, + { + name: 'QrCodeLogin', + path: 'qrcode-login', + component: () => + import('#/views/_core/authentication/qrcode-login.vue'), + meta: { + title: $t('page.auth.qrcodeLogin'), + }, + }, + { + name: 'ForgetPassword', + path: 'forget-password', + component: () => + import('#/views/_core/authentication/forget-password.vue'), + meta: { + title: $t('page.auth.forgetPassword'), + }, + }, + { + name: 'Register', + path: 'register', + component: () => import('#/views/_core/authentication/register.vue'), + meta: { + title: $t('page.auth.register'), + }, + }, + ], + }, +]; + +export { coreRoutes, fallbackNotFoundRoute }; diff --git a/apps/vben5/playground/src/router/routes/index.ts b/apps/vben5/playground/src/router/routes/index.ts new file mode 100644 index 000000000..e6fb14402 --- /dev/null +++ b/apps/vben5/playground/src/router/routes/index.ts @@ -0,0 +1,37 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { mergeRouteModules, traverseTreeValues } from '@vben/utils'; + +import { coreRoutes, fallbackNotFoundRoute } from './core'; + +const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', { + eager: true, +}); + +// 有需要可以自行打开注释,并创建文件夹 +// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); +// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); + +/** 动态路由 */ +const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); + +/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */ +// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); +// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles); +const staticRoutes: RouteRecordRaw[] = []; +const externalRoutes: RouteRecordRaw[] = []; + +/** 路由列表,由基本路由、外部路由和404兜底路由组成 + * 无需走权限验证(会一直显示在菜单中) */ +const routes: RouteRecordRaw[] = [ + ...coreRoutes, + ...externalRoutes, + fallbackNotFoundRoute, +]; + +/** 基本路由列表,这些路由不需要进入权限拦截 */ +const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name); + +/** 有权限校验的路由列表,包含动态路由和静态路由 */ +const accessRoutes = [...dynamicRoutes, ...staticRoutes]; +export { accessRoutes, coreRouteNames, routes }; diff --git a/apps/vben5/playground/src/router/routes/modules/dashboard.ts b/apps/vben5/playground/src/router/routes/modules/dashboard.ts new file mode 100644 index 000000000..1bddab9db --- /dev/null +++ b/apps/vben5/playground/src/router/routes/modules/dashboard.ts @@ -0,0 +1,40 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'lucide:layout-dashboard', + order: -1, + title: $t('page.dashboard.title'), + }, + name: 'Dashboard', + path: '/', + children: [ + { + name: 'Analytics', + path: '/analytics', + component: () => import('#/views/dashboard/analytics/index.vue'), + meta: { + affixTab: true, + icon: 'lucide:area-chart', + title: $t('page.dashboard.analytics'), + }, + }, + { + name: 'Workspace', + path: '/workspace', + component: () => import('#/views/dashboard/workspace/index.vue'), + meta: { + icon: 'carbon:workspace', + title: $t('page.dashboard.workspace'), + }, + }, + ], + }, +]; + +export default routes; diff --git a/apps/vben5/playground/src/router/routes/modules/demos.ts b/apps/vben5/playground/src/router/routes/modules/demos.ts new file mode 100644 index 000000000..46ebeeb9f --- /dev/null +++ b/apps/vben5/playground/src/router/routes/modules/demos.ts @@ -0,0 +1,572 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout, IFrameView } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'ic:baseline-view-in-ar', + keepAlive: true, + order: 1000, + title: $t('demos.title'), + }, + name: 'Demos', + path: '/demos', + children: [ + // 权限控制 + { + meta: { + icon: 'mdi:shield-key-outline', + title: $t('demos.access.frontendPermissions'), + }, + name: 'AccessDemos', + path: '/demos/access', + children: [ + { + name: 'AccessPageControlDemo', + path: '/demos/access/page-control', + component: () => import('#/views/demos/access/index.vue'), + meta: { + icon: 'mdi:page-previous-outline', + title: $t('demos.access.pageAccess'), + }, + }, + { + name: 'AccessButtonControlDemo', + path: '/demos/access/button-control', + component: () => import('#/views/demos/access/button-control.vue'), + meta: { + icon: 'mdi:button-cursor', + title: $t('demos.access.buttonControl'), + }, + }, + { + name: 'AccessMenuVisible403Demo', + path: '/demos/access/menu-visible-403', + component: () => + import('#/views/demos/access/menu-visible-403.vue'), + meta: { + authority: ['no-body'], + icon: 'mdi:button-cursor', + menuVisibleWithForbidden: true, + title: $t('demos.access.menuVisible403'), + }, + }, + { + name: 'AccessSuperVisibleDemo', + path: '/demos/access/super-visible', + component: () => import('#/views/demos/access/super-visible.vue'), + meta: { + authority: ['super'], + icon: 'mdi:button-cursor', + title: $t('demos.access.superVisible'), + }, + }, + { + name: 'AccessAdminVisibleDemo', + path: '/demos/access/admin-visible', + component: () => import('#/views/demos/access/admin-visible.vue'), + meta: { + authority: ['admin'], + icon: 'mdi:button-cursor', + title: $t('demos.access.adminVisible'), + }, + }, + { + name: 'AccessUserVisibleDemo', + path: '/demos/access/user-visible', + component: () => import('#/views/demos/access/user-visible.vue'), + meta: { + authority: ['user'], + icon: 'mdi:button-cursor', + title: $t('demos.access.userVisible'), + }, + }, + ], + }, + // 功能 + { + meta: { + icon: 'mdi:feature-highlight', + title: $t('demos.features.title'), + }, + name: 'FeaturesDemos', + path: '/demos/features', + children: [ + { + name: 'LoginExpiredDemo', + path: '/demos/features/login-expired', + component: () => + import('#/views/demos/features/login-expired/index.vue'), + meta: { + icon: 'mdi:encryption-expiration', + title: $t('demos.features.loginExpired'), + }, + }, + { + name: 'IconsDemo', + path: '/demos/features/icons', + component: () => import('#/views/demos/features/icons/index.vue'), + meta: { + icon: 'lucide:annoyed', + title: $t('demos.features.icons'), + }, + }, + { + name: 'WatermarkDemo', + path: '/demos/features/watermark', + component: () => + import('#/views/demos/features/watermark/index.vue'), + meta: { + icon: 'lucide:tags', + title: $t('demos.features.watermark'), + }, + }, + { + name: 'FeatureTabsDemo', + path: '/demos/features/tabs', + component: () => import('#/views/demos/features/tabs/index.vue'), + meta: { + icon: 'lucide:app-window', + title: $t('demos.features.tabs'), + }, + }, + { + name: 'FeatureTabDetailDemo', + path: '/demos/features/tabs/detail/:id', + component: () => + import('#/views/demos/features/tabs/tab-detail.vue'), + meta: { + activePath: '/demos/features/tabs', + hideInMenu: true, + maxNumOfOpenTab: 3, + title: $t('demos.features.tabDetail'), + }, + }, + { + name: 'HideChildrenInMenuParentDemo', + path: '/demos/features/hide-menu-children', + component: () => + import('#/views/demos/features/hide-menu-children/parent.vue'), + meta: { + hideChildrenInMenu: true, + icon: 'ic:round-menu', + title: $t('demos.features.hideChildrenInMenu'), + }, + children: [ + { + name: 'HideChildrenInMenuDemo', + path: '', + component: () => + import( + '#/views/demos/features/hide-menu-children/children.vue' + ), + meta: { + hideInMenu: true, + title: $t('demos.features.hideChildrenInMenu'), + }, + }, + { + name: 'HideChildrenInMenuChildrenDemo', + path: '/demos/features/hide-menu-children/children', + component: () => + import( + '#/views/demos/features/hide-menu-children/children.vue' + ), + meta: { title: $t('demos.features.hideChildrenInMenu') }, + }, + ], + }, + { + name: 'FullScreenDemo', + path: '/demos/features/full-screen', + component: () => + import('#/views/demos/features/full-screen/index.vue'), + meta: { + icon: 'lucide:fullscreen', + title: $t('demos.features.fullScreen'), + }, + }, + { + name: 'FileDownloadDemo', + path: '/demos/features/file-download', + component: () => + import('#/views/demos/features/file-download/index.vue'), + meta: { + icon: 'lucide:hard-drive-download', + title: $t('demos.features.fileDownload'), + }, + }, + { + name: 'ClipboardDemo', + path: '/demos/features/clipboard', + component: () => + import('#/views/demos/features/clipboard/index.vue'), + meta: { + icon: 'lucide:copy', + title: $t('demos.features.clipboard'), + }, + }, + { + name: 'MenuQueryDemo', + path: '/demos/menu-query', + component: () => + import('#/views/demos/features/menu-query/index.vue'), + meta: { + icon: 'lucide:curly-braces', + query: { + id: 1, + }, + title: $t('demos.features.menuWithQuery'), + }, + }, + { + name: 'NewWindowDemo', + path: '/demos/new-window', + component: () => + import('#/views/demos/features/new-window/index.vue'), + meta: { + icon: 'lucide:app-window', + openInNewWindow: true, + title: $t('demos.features.openInNewWindow'), + }, + }, + { + name: 'VueQueryDemo', + path: '/demos/features/vue-query', + component: () => + import('#/views/demos/features/vue-query/index.vue'), + meta: { + icon: 'lucide:git-pull-request-arrow', + title: 'Tanstack Query', + }, + }, + ], + }, + // 面包屑导航 + { + name: 'BreadcrumbDemos', + path: '/demos/breadcrumb', + meta: { + icon: 'lucide:navigation', + title: $t('demos.breadcrumb.navigation'), + }, + children: [ + { + name: 'BreadcrumbLateralDemo', + path: '/demos/breadcrumb/lateral', + component: () => import('#/views/demos/breadcrumb/lateral.vue'), + meta: { + icon: 'lucide:navigation', + title: $t('demos.breadcrumb.lateral'), + }, + }, + { + name: 'BreadcrumbLateralDetailDemo', + path: '/demos/breadcrumb/lateral-detail', + component: () => + import('#/views/demos/breadcrumb/lateral-detail.vue'), + meta: { + activePath: '/demos/breadcrumb/lateral', + hideInMenu: true, + title: $t('demos.breadcrumb.lateralDetail'), + }, + }, + { + name: 'BreadcrumbLevelDemo', + path: '/demos/breadcrumb/level', + meta: { + icon: 'lucide:navigation', + title: $t('demos.breadcrumb.level'), + }, + children: [ + { + name: 'BreadcrumbLevelDetailDemo', + path: '/demos/breadcrumb/level/detail', + component: () => + import('#/views/demos/breadcrumb/level-detail.vue'), + meta: { + title: $t('demos.breadcrumb.levelDetail'), + }, + }, + ], + }, + ], + }, + // 缺省页 + { + meta: { + icon: 'mdi:lightbulb-error-outline', + title: $t('demos.fallback.title'), + }, + name: 'FallbackDemos', + path: '/demos/fallback', + children: [ + { + name: 'Fallback403Demo', + path: '/demos/fallback/403', + component: () => import('#/views/_core/fallback/forbidden.vue'), + meta: { + icon: 'mdi:do-not-disturb-alt', + title: '403', + }, + }, + { + name: 'Fallback404Demo', + path: '/demos/fallback/404', + component: () => import('#/views/_core/fallback/not-found.vue'), + meta: { + icon: 'mdi:table-off', + title: '404', + }, + }, + { + name: 'Fallback500Demo', + path: '/demos/fallback/500', + component: () => + import('#/views/_core/fallback/internal-error.vue'), + meta: { + icon: 'mdi:server-network-off', + title: '500', + }, + }, + { + name: 'FallbackOfflineDemo', + path: '/demos/fallback/offline', + component: () => import('#/views/_core/fallback/offline.vue'), + meta: { + icon: 'mdi:offline', + title: $t('ui.fallback.offline'), + }, + }, + ], + }, + // 菜单徽标 + { + meta: { + badgeType: 'dot', + badgeVariants: 'destructive', + icon: 'lucide:circle-dot', + title: $t('demos.badge.title'), + }, + name: 'BadgeDemos', + path: '/demos/badge', + children: [ + { + name: 'BadgeDotDemo', + component: () => import('#/views/demos/badge/index.vue'), + path: '/demos/badge/dot', + meta: { + badgeType: 'dot', + icon: 'lucide:square-dot', + title: $t('demos.badge.dot'), + }, + }, + { + name: 'BadgeTextDemo', + component: () => import('#/views/demos/badge/index.vue'), + path: '/demos/badge/text', + meta: { + badge: '10', + icon: 'lucide:square-dot', + title: $t('demos.badge.text'), + }, + }, + { + name: 'BadgeColorDemo', + component: () => import('#/views/demos/badge/index.vue'), + path: '/demos/badge/color', + meta: { + badge: 'Hot', + badgeVariants: 'destructive', + icon: 'lucide:square-dot', + title: $t('demos.badge.color'), + }, + }, + ], + }, + // 菜单激活图标 + { + meta: { + activeIcon: 'fluent-emoji:radioactive', + icon: 'bi:radioactive', + title: $t('demos.activeIcon.title'), + }, + name: 'ActiveIconDemos', + path: '/demos/active-icon', + children: [ + { + name: 'ActiveIconDemo', + component: () => import('#/views/demos/active-icon/index.vue'), + path: '/demos/active-icon/children', + meta: { + activeIcon: 'fluent-emoji:radioactive', + icon: 'bi:radioactive', + title: $t('demos.activeIcon.children'), + }, + }, + ], + }, + // 外部链接 + { + meta: { + icon: 'ic:round-settings-input-composite', + title: $t('demos.outside.title'), + }, + name: 'OutsideDemos', + path: '/demos/outside', + children: [ + { + name: 'IframeDemos', + path: '/demos/outside/iframe', + meta: { + icon: 'mdi:newspaper-variant-outline', + title: $t('demos.outside.embedded'), + }, + children: [ + { + name: 'VueDocumentDemo', + path: '/demos/outside/iframe/vue-document', + component: IFrameView, + meta: { + icon: 'logos:vue', + iframeSrc: 'https://cn.vuejs.org/', + keepAlive: true, + title: 'Vue', + }, + }, + { + name: 'TailwindcssDemo', + path: '/demos/outside/iframe/tailwindcss', + component: IFrameView, + meta: { + icon: 'devicon:tailwindcss', + iframeSrc: 'https://tailwindcss.com/', + // keepAlive: true, + title: 'Tailwindcss', + }, + }, + ], + }, + { + name: 'ExternalLinkDemos', + path: '/demos/outside/external-link', + meta: { + icon: 'mdi:newspaper-variant-multiple-outline', + title: $t('demos.outside.externalLink'), + }, + children: [ + { + name: 'ViteDemo', + path: '/demos/outside/external-link/vite', + component: IFrameView, + meta: { + icon: 'logos:vitejs', + link: 'https://vitejs.dev/', + title: 'Vite', + }, + }, + { + name: 'VueUseDemo', + path: '/demos/outside/external-link/vue-use', + component: IFrameView, + meta: { + icon: 'logos:vueuse', + link: 'https://vueuse.org', + title: 'VueUse', + }, + }, + ], + }, + ], + }, + // 嵌套菜单 + { + meta: { + icon: 'ic:round-menu', + title: $t('demos.nested.title'), + }, + name: 'NestedDemos', + path: '/demos/nested', + children: [ + { + name: 'Menu1Demo', + path: '/demos/nested/menu1', + component: () => import('#/views/demos/nested/menu-1.vue'), + meta: { + icon: 'ic:round-menu', + keepAlive: true, + title: $t('demos.nested.menu1'), + }, + }, + { + name: 'Menu2Demo', + path: '/demos/nested/menu2', + meta: { + icon: 'ic:round-menu', + keepAlive: true, + title: $t('demos.nested.menu2'), + }, + children: [ + { + name: 'Menu21Demo', + path: '/demos/nested/menu2/menu2-1', + component: () => import('#/views/demos/nested/menu-2-1.vue'), + meta: { + icon: 'ic:round-menu', + keepAlive: true, + title: $t('demos.nested.menu2_1'), + }, + }, + ], + }, + { + name: 'Menu3Demo', + path: '/demos/nested/menu3', + meta: { + icon: 'ic:round-menu', + title: $t('demos.nested.menu3'), + }, + children: [ + { + name: 'Menu31Demo', + path: 'menu3-1', + component: () => import('#/views/demos/nested/menu-3-1.vue'), + meta: { + icon: 'ic:round-menu', + keepAlive: true, + title: $t('demos.nested.menu3_1'), + }, + }, + { + name: 'Menu32Demo', + path: 'menu3-2', + meta: { + icon: 'ic:round-menu', + title: $t('demos.nested.menu3_2'), + }, + children: [ + { + name: 'Menu321Demo', + path: '/demos/nested/menu3/menu3-2/menu3-2-1', + component: () => + import('#/views/demos/nested/menu-3-2-1.vue'), + meta: { + icon: 'ic:round-menu', + keepAlive: true, + title: $t('demos.nested.menu3_2_1'), + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, +]; + +export default routes; diff --git a/apps/vben5/playground/src/router/routes/modules/examples.ts b/apps/vben5/playground/src/router/routes/modules/examples.ts new file mode 100644 index 000000000..82202b381 --- /dev/null +++ b/apps/vben5/playground/src/router/routes/modules/examples.ts @@ -0,0 +1,244 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { BasicLayout } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + icon: 'ion:layers-outline', + keepAlive: true, + order: 1000, + title: $t('examples.title'), + }, + name: 'Examples', + path: '/examples', + children: [ + { + name: 'FormExample', + path: '/examples/form', + meta: { + icon: 'mdi:form-select', + title: $t('examples.form.title'), + }, + children: [ + { + name: 'FormBasicExample', + path: '/examples/form/basic', + component: () => import('#/views/examples/form/basic.vue'), + meta: { + title: $t('examples.form.basic'), + }, + }, + { + name: 'FormQueryExample', + path: '/examples/form/query', + component: () => import('#/views/examples/form/query.vue'), + meta: { + title: $t('examples.form.query'), + }, + }, + { + name: 'FormRulesExample', + path: '/examples/form/rules', + component: () => import('#/views/examples/form/rules.vue'), + meta: { + title: $t('examples.form.rules'), + }, + }, + { + name: 'FormDynamicExample', + path: '/examples/form/dynamic', + component: () => import('#/views/examples/form/dynamic.vue'), + meta: { + title: $t('examples.form.dynamic'), + }, + }, + { + name: 'FormCustomExample', + path: '/examples/form/custom', + component: () => import('#/views/examples/form/custom.vue'), + meta: { + title: $t('examples.form.custom'), + }, + }, + { + name: 'FormApiExample', + path: '/examples/form/api', + component: () => import('#/views/examples/form/api.vue'), + meta: { + title: $t('examples.form.api'), + }, + }, + { + name: 'FormMergeExample', + path: '/examples/form/merge', + component: () => import('#/views/examples/form/merge.vue'), + meta: { + title: $t('examples.form.merge'), + }, + }, + ], + }, + { + name: 'VxeTableExample', + path: '/examples/vxe-table', + meta: { + icon: 'lucide:table', + title: $t('examples.vxeTable.title'), + }, + children: [ + { + name: 'VxeTableBasicExample', + path: '/examples/vxe-table/basic', + component: () => import('#/views/examples/vxe-table/basic.vue'), + meta: { + title: $t('examples.vxeTable.basic'), + }, + }, + { + name: 'VxeTableRemoteExample', + path: '/examples/vxe-table/remote', + component: () => import('#/views/examples/vxe-table/remote.vue'), + meta: { + title: $t('examples.vxeTable.remote'), + }, + }, + { + name: 'VxeTableTreeExample', + path: '/examples/vxe-table/tree', + component: () => import('#/views/examples/vxe-table/tree.vue'), + meta: { + title: $t('examples.vxeTable.tree'), + }, + }, + { + name: 'VxeTableFixedExample', + path: '/examples/vxe-table/fixed', + component: () => import('#/views/examples/vxe-table/fixed.vue'), + meta: { + title: $t('examples.vxeTable.fixed'), + }, + }, + { + name: 'VxeTableCustomCellExample', + path: '/examples/vxe-table/custom-cell', + component: () => + import('#/views/examples/vxe-table/custom-cell.vue'), + meta: { + title: $t('examples.vxeTable.custom-cell'), + }, + }, + { + name: 'VxeTableFormExample', + path: '/examples/vxe-table/form', + component: () => import('#/views/examples/vxe-table/form.vue'), + meta: { + title: $t('examples.vxeTable.form'), + }, + }, + { + name: 'VxeTableEditCellExample', + path: '/examples/vxe-table/edit-cell', + component: () => import('#/views/examples/vxe-table/edit-cell.vue'), + meta: { + title: $t('examples.vxeTable.editCell'), + }, + }, + { + name: 'VxeTableEditRowExample', + path: '/examples/vxe-table/edit-row', + component: () => import('#/views/examples/vxe-table/edit-row.vue'), + meta: { + title: $t('examples.vxeTable.editRow'), + }, + }, + { + name: 'VxeTableVirtualExample', + path: '/examples/vxe-table/virtual', + component: () => import('#/views/examples/vxe-table/virtual.vue'), + meta: { + title: $t('examples.vxeTable.virtual'), + }, + }, + ], + }, + { + name: 'CaptchaExample', + path: '/examples/captcha', + meta: { + icon: 'logos:recaptcha', + title: $t('examples.captcha.title'), + }, + children: [ + { + name: 'DragVerifyExample', + path: '/examples/captcha/slider', + component: () => + import('#/views/examples/captcha/slider-captcha.vue'), + meta: { + title: $t('examples.captcha.sliderCaptcha'), + }, + }, + { + name: 'RotateVerifyExample', + path: '/examples/captcha/slider-rotate', + component: () => + import('#/views/examples/captcha/slider-rotate-captcha.vue'), + meta: { + title: $t('examples.captcha.sliderRotateCaptcha'), + }, + }, + { + name: 'CaptchaPointSelectionExample', + path: '/examples/captcha/point-selection', + component: () => + import('#/views/examples/captcha/point-selection-captcha.vue'), + meta: { + title: $t('examples.captcha.pointSelection'), + }, + }, + ], + }, + { + name: 'ModalExample', + path: '/examples/modal', + component: () => import('#/views/examples/modal/index.vue'), + meta: { + icon: 'system-uicons:window-content', + title: $t('examples.modal.title'), + }, + }, + { + name: 'DrawerExample', + path: '/examples/drawer', + component: () => import('#/views/examples/drawer/index.vue'), + meta: { + icon: 'iconoir:drawer', + title: $t('examples.drawer.title'), + }, + }, + { + name: 'EllipsisExample', + path: '/examples/ellipsis', + component: () => import('#/views/examples/ellipsis/index.vue'), + meta: { + icon: 'ion:ellipsis-horizontal', + title: $t('examples.ellipsis.title'), + }, + }, + { + name: 'VueResizeDemo', + path: '/demos/resize/basic', + component: () => import('#/views/examples/resize/basic.vue'), + meta: { + icon: 'material-symbols:resize', + title: $t('examples.resize.title'), + }, + }, + ], + }, +]; + +export default routes; diff --git a/apps/vben5/playground/src/router/routes/modules/vben.ts b/apps/vben5/playground/src/router/routes/modules/vben.ts new file mode 100644 index 000000000..2f2eb6604 --- /dev/null +++ b/apps/vben5/playground/src/router/routes/modules/vben.ts @@ -0,0 +1,94 @@ +import type { RouteRecordRaw } from 'vue-router'; + +import { + VBEN_ANT_PREVIEW_URL, + VBEN_DOC_URL, + VBEN_ELE_PREVIEW_URL, + VBEN_GITHUB_URL, + VBEN_LOGO_URL, + VBEN_NAIVE_PREVIEW_URL, +} from '@vben/constants'; +import { SvgAntdvLogoIcon } from '@vben/icons'; + +import { BasicLayout, IFrameView } from '#/layouts'; +import { $t } from '#/locales'; + +const routes: RouteRecordRaw[] = [ + { + component: BasicLayout, + meta: { + badgeType: 'dot', + icon: VBEN_LOGO_URL, + order: 9999, + title: $t('demos.vben.title'), + }, + name: 'VbenProject', + path: '/vben-admin', + children: [ + { + name: 'VbenAbout', + path: '/vben-admin/about', + component: () => import('#/views/_core/about/index.vue'), + meta: { + icon: 'lucide:copyright', + title: $t('demos.vben.about'), + }, + }, + { + name: 'VbenDocument', + path: '/vben-admin/document', + component: IFrameView, + meta: { + icon: 'lucide:book-open-text', + link: VBEN_DOC_URL, + title: $t('demos.vben.document'), + }, + }, + { + name: 'VbenGithub', + path: '/vben-admin/github', + component: IFrameView, + meta: { + icon: 'mdi:github', + link: VBEN_GITHUB_URL, + title: 'Github', + }, + }, + { + name: 'VbenAntdv', + path: '/vben-admin/antdv', + component: IFrameView, + meta: { + badgeType: 'dot', + icon: SvgAntdvLogoIcon, + link: VBEN_ANT_PREVIEW_URL, + title: $t('demos.vben.antdv'), + }, + }, + { + name: 'VbenNaive', + path: '/vben-admin/naive', + component: IFrameView, + meta: { + badgeType: 'dot', + icon: 'logos:naiveui', + link: VBEN_NAIVE_PREVIEW_URL, + title: $t('demos.vben.naive-ui'), + }, + }, + { + name: 'VbenElementPlus', + path: '/vben-admin/ele', + component: IFrameView, + meta: { + badgeType: 'dot', + icon: 'logos:element', + link: VBEN_ELE_PREVIEW_URL, + title: $t('demos.vben.element-plus'), + }, + }, + ], + }, +]; + +export default routes; diff --git a/apps/vben5/playground/src/store/auth.ts b/apps/vben5/playground/src/store/auth.ts new file mode 100644 index 000000000..9976ac59f --- /dev/null +++ b/apps/vben5/playground/src/store/auth.ts @@ -0,0 +1,117 @@ +import type { Recordable, UserInfo } from '@vben/types'; + +import { ref } from 'vue'; +import { useRouter } from 'vue-router'; + +import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; + +import { notification } from 'ant-design-vue'; +import { defineStore } from 'pinia'; + +import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; +import { $t } from '#/locales'; + +export const useAuthStore = defineStore('auth', () => { + const accessStore = useAccessStore(); + const userStore = useUserStore(); + const router = useRouter(); + + const loginLoading = ref(false); + + /** + * 异步处理登录操作 + * Asynchronously handle the login process + * @param params 登录表单数据 + * @param onSuccess 成功之后的回调函数 + */ + async function authLogin( + params: Recordable, + onSuccess?: () => Promise | void, + ) { + // 异步处理用户登录操作并获取 accessToken + let userInfo: null | UserInfo = null; + try { + loginLoading.value = true; + const { accessToken } = await loginApi(params); + + // 如果成功获取到 accessToken + if (accessToken) { + accessStore.setAccessToken(accessToken); + + // 获取用户信息并存储到 accessStore 中 + const [fetchUserInfoResult, accessCodes] = await Promise.all([ + fetchUserInfo(), + getAccessCodesApi(), + ]); + + userInfo = fetchUserInfoResult; + + userStore.setUserInfo(userInfo); + accessStore.setAccessCodes(accessCodes); + + if (accessStore.loginExpired) { + accessStore.setLoginExpired(false); + } else { + onSuccess + ? await onSuccess?.() + : await router.push(userInfo.homePath || DEFAULT_HOME_PATH); + } + + if (userInfo?.realName) { + notification.success({ + description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, + duration: 3, + message: $t('authentication.loginSuccess'), + }); + } + } + } finally { + loginLoading.value = false; + } + + return { + userInfo, + }; + } + + async function logout(redirect: boolean = true) { + try { + await logoutApi(); + } catch { + // 不做任何处理 + } + + resetAllStores(); + accessStore.setLoginExpired(false); + + // 回登录页带上当前路由地址 + await router.replace({ + path: LOGIN_PATH, + query: redirect + ? { + redirect: encodeURIComponent(router.currentRoute.value.fullPath), + } + : {}, + }); + } + + async function fetchUserInfo() { + let userInfo: null | UserInfo = null; + userInfo = await getUserInfoApi(); + userStore.setUserInfo(userInfo); + return userInfo; + } + + function $reset() { + loginLoading.value = false; + } + + return { + $reset, + authLogin, + fetchUserInfo, + loginLoading, + logout, + }; +}); diff --git a/apps/vben5/playground/src/store/index.ts b/apps/vben5/playground/src/store/index.ts new file mode 100644 index 000000000..269586ee8 --- /dev/null +++ b/apps/vben5/playground/src/store/index.ts @@ -0,0 +1 @@ +export * from './auth'; diff --git a/apps/vben5/playground/src/views/_core/README.md b/apps/vben5/playground/src/views/_core/README.md new file mode 100644 index 000000000..8248afe6c --- /dev/null +++ b/apps/vben5/playground/src/views/_core/README.md @@ -0,0 +1,3 @@ +# \_core + +此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。 diff --git a/apps/vben5/playground/src/views/_core/about/index.vue b/apps/vben5/playground/src/views/_core/about/index.vue new file mode 100644 index 000000000..0ee524335 --- /dev/null +++ b/apps/vben5/playground/src/views/_core/about/index.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/authentication/code-login.vue b/apps/vben5/playground/src/views/_core/authentication/code-login.vue new file mode 100644 index 000000000..556b273af --- /dev/null +++ b/apps/vben5/playground/src/views/_core/authentication/code-login.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/authentication/forget-password.vue b/apps/vben5/playground/src/views/_core/authentication/forget-password.vue new file mode 100644 index 000000000..34491113f --- /dev/null +++ b/apps/vben5/playground/src/views/_core/authentication/forget-password.vue @@ -0,0 +1,42 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/authentication/login.vue b/apps/vben5/playground/src/views/_core/authentication/login.vue new file mode 100644 index 000000000..4f6f56496 --- /dev/null +++ b/apps/vben5/playground/src/views/_core/authentication/login.vue @@ -0,0 +1,115 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/authentication/qrcode-login.vue b/apps/vben5/playground/src/views/_core/authentication/qrcode-login.vue new file mode 100644 index 000000000..23f5f2dad --- /dev/null +++ b/apps/vben5/playground/src/views/_core/authentication/qrcode-login.vue @@ -0,0 +1,10 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/authentication/register.vue b/apps/vben5/playground/src/views/_core/authentication/register.vue new file mode 100644 index 000000000..b1a5de726 --- /dev/null +++ b/apps/vben5/playground/src/views/_core/authentication/register.vue @@ -0,0 +1,96 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/fallback/coming-soon.vue b/apps/vben5/playground/src/views/_core/fallback/coming-soon.vue new file mode 100644 index 000000000..f394930f2 --- /dev/null +++ b/apps/vben5/playground/src/views/_core/fallback/coming-soon.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/fallback/forbidden.vue b/apps/vben5/playground/src/views/_core/fallback/forbidden.vue new file mode 100644 index 000000000..8ea65fedb --- /dev/null +++ b/apps/vben5/playground/src/views/_core/fallback/forbidden.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/fallback/internal-error.vue b/apps/vben5/playground/src/views/_core/fallback/internal-error.vue new file mode 100644 index 000000000..819a47d5e --- /dev/null +++ b/apps/vben5/playground/src/views/_core/fallback/internal-error.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/fallback/not-found.vue b/apps/vben5/playground/src/views/_core/fallback/not-found.vue new file mode 100644 index 000000000..4d178e9cb --- /dev/null +++ b/apps/vben5/playground/src/views/_core/fallback/not-found.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/vben5/playground/src/views/_core/fallback/offline.vue b/apps/vben5/playground/src/views/_core/fallback/offline.vue new file mode 100644 index 000000000..5de4a88de --- /dev/null +++ b/apps/vben5/playground/src/views/_core/fallback/offline.vue @@ -0,0 +1,9 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/analytics/analytics-trends.vue b/apps/vben5/playground/src/views/dashboard/analytics/analytics-trends.vue new file mode 100644 index 000000000..fadfc917c --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/analytics/analytics-trends.vue @@ -0,0 +1,100 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-data.vue b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-data.vue new file mode 100644 index 000000000..30c4265df --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-data.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-sales.vue b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-sales.vue new file mode 100644 index 000000000..260520b84 --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-sales.vue @@ -0,0 +1,48 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-source.vue b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-source.vue new file mode 100644 index 000000000..e0d0aab5e --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits-source.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits.vue b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits.vue new file mode 100644 index 000000000..7e1f14ee6 --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/analytics/analytics-visits.vue @@ -0,0 +1,57 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/analytics/index.vue b/apps/vben5/playground/src/views/dashboard/analytics/index.vue new file mode 100644 index 000000000..00b34df1e --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/analytics/index.vue @@ -0,0 +1,90 @@ + + + diff --git a/apps/vben5/playground/src/views/dashboard/workspace/index.vue b/apps/vben5/playground/src/views/dashboard/workspace/index.vue new file mode 100644 index 000000000..b95d61381 --- /dev/null +++ b/apps/vben5/playground/src/views/dashboard/workspace/index.vue @@ -0,0 +1,266 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/access/admin-visible.vue b/apps/vben5/playground/src/views/demos/access/admin-visible.vue new file mode 100644 index 000000000..079b93092 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/access/admin-visible.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/access/button-control.vue b/apps/vben5/playground/src/views/demos/access/button-control.vue new file mode 100644 index 000000000..0966a7be6 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/access/button-control.vue @@ -0,0 +1,157 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/access/index.vue b/apps/vben5/playground/src/views/demos/access/index.vue new file mode 100644 index 000000000..abd55254d --- /dev/null +++ b/apps/vben5/playground/src/views/demos/access/index.vue @@ -0,0 +1,98 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/access/menu-visible-403.vue b/apps/vben5/playground/src/views/demos/access/menu-visible-403.vue new file mode 100644 index 000000000..7e3cde88b --- /dev/null +++ b/apps/vben5/playground/src/views/demos/access/menu-visible-403.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/access/super-visible.vue b/apps/vben5/playground/src/views/demos/access/super-visible.vue new file mode 100644 index 000000000..877fcd115 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/access/super-visible.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/access/user-visible.vue b/apps/vben5/playground/src/views/demos/access/user-visible.vue new file mode 100644 index 000000000..a3c5f001d --- /dev/null +++ b/apps/vben5/playground/src/views/demos/access/user-visible.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/active-icon/index.vue b/apps/vben5/playground/src/views/demos/active-icon/index.vue new file mode 100644 index 000000000..af9f69aff --- /dev/null +++ b/apps/vben5/playground/src/views/demos/active-icon/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/badge/index.vue b/apps/vben5/playground/src/views/demos/badge/index.vue new file mode 100644 index 000000000..92d70c7e1 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/badge/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/breadcrumb/lateral-detail.vue b/apps/vben5/playground/src/views/demos/breadcrumb/lateral-detail.vue new file mode 100644 index 000000000..8a4de64e2 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/breadcrumb/lateral-detail.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/breadcrumb/lateral.vue b/apps/vben5/playground/src/views/demos/breadcrumb/lateral.vue new file mode 100644 index 000000000..2c24abff2 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/breadcrumb/lateral.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/breadcrumb/level-detail.vue b/apps/vben5/playground/src/views/demos/breadcrumb/level-detail.vue new file mode 100644 index 000000000..7aa18d4b0 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/breadcrumb/level-detail.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/clipboard/index.vue b/apps/vben5/playground/src/views/demos/features/clipboard/index.vue new file mode 100644 index 000000000..792315ffa --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/clipboard/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/file-download/base64.ts b/apps/vben5/playground/src/views/demos/features/file-download/base64.ts new file mode 100644 index 000000000..ee6ac2bc4 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/file-download/base64.ts @@ -0,0 +1 @@ +export default `data:image/png;base64,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`; diff --git a/apps/vben5/playground/src/views/demos/features/file-download/index.vue b/apps/vben5/playground/src/views/demos/features/file-download/index.vue new file mode 100644 index 000000000..b4125b8a4 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/file-download/index.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/full-screen/index.vue b/apps/vben5/playground/src/views/demos/features/full-screen/index.vue new file mode 100644 index 000000000..485b2e43b --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/full-screen/index.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/hide-menu-children/children.vue b/apps/vben5/playground/src/views/demos/features/hide-menu-children/children.vue new file mode 100644 index 000000000..9b009e36d --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/hide-menu-children/children.vue @@ -0,0 +1,3 @@ + diff --git a/apps/vben5/playground/src/views/demos/features/hide-menu-children/parent.vue b/apps/vben5/playground/src/views/demos/features/hide-menu-children/parent.vue new file mode 100644 index 000000000..920877516 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/hide-menu-children/parent.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/icons/icon-picker.vue b/apps/vben5/playground/src/views/demos/features/icons/icon-picker.vue new file mode 100644 index 000000000..699ebec67 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/icons/icon-picker.vue @@ -0,0 +1,87 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/icons/icons.data.ts b/apps/vben5/playground/src/views/demos/features/icons/icons.data.ts new file mode 100644 index 000000000..833f05197 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/icons/icons.data.ts @@ -0,0 +1,793 @@ +export default { + icons: [ + 'account-book-filled', + 'account-book-outlined', + 'account-book-twotone', + 'aim-outlined', + 'alert-filled', + 'alert-outlined', + 'alert-twotone', + 'alibaba-outlined', + 'align-center-outlined', + 'align-left-outlined', + 'align-right-outlined', + 'alipay-circle-filled', + 'alipay-circle-outlined', + 'alipay-outlined', + 'alipay-square-filled', + 'aliwangwang-filled', + 'aliwangwang-outlined', + 'aliyun-outlined', + 'amazon-circle-filled', + 'amazon-outlined', + 'amazon-square-filled', + 'android-filled', + 'android-outlined', + 'ant-cloud-outlined', + 'ant-design-outlined', + 'apartment-outlined', + 'api-filled', + 'api-outlined', + 'api-twotone', + 'apple-filled', + 'apple-outlined', + 'appstore-add-outlined', + 'appstore-filled', + 'appstore-outlined', + 'appstore-twotone', + 'area-chart-outlined', + 'arrow-down-outlined', + 'arrow-left-outlined', + 'arrow-right-outlined', + 'arrow-up-outlined', + 'arrows-alt-outlined', + 'audio-filled', + 'audio-muted-outlined', + 'audio-outlined', + 'audio-twotone', + 'audit-outlined', + 'backward-filled', + 'backward-outlined', + 'bank-filled', + 'bank-outlined', + 'bank-twotone', + 'bar-chart-outlined', + 'barcode-outlined', + 'bars-outlined', + 'behance-circle-filled', + 'behance-outlined', + 'behance-square-filled', + 'behance-square-outlined', + 'bell-filled', + 'bell-outlined', + 'bell-twotone', + 'bg-colors-outlined', + 'block-outlined', + 'bold-outlined', + 'book-filled', + 'book-outlined', + 'book-twotone', + 'border-bottom-outlined', + 'border-horizontal-outlined', + 'border-inner-outlined', + 'border-left-outlined', + 'border-outer-outlined', + 'border-outlined', + 'border-right-outlined', + 'border-top-outlined', + 'border-verticle-outlined', + 'borderless-table-outlined', + 'box-plot-filled', + 'box-plot-outlined', + 'box-plot-twotone', + 'branches-outlined', + 'bug-filled', + 'bug-outlined', + 'bug-twotone', + 'build-filled', + 'build-outlined', + 'build-twotone', + 'bulb-filled', + 'bulb-outlined', + 'bulb-twotone', + 'calculator-filled', + 'calculator-outlined', + 'calculator-twotone', + 'calendar-filled', + 'calendar-outlined', + 'calendar-twotone', + 'camera-filled', + 'camera-outlined', + 'camera-twotone', + 'car-filled', + 'car-outlined', + 'car-twotone', + 'caret-down-filled', + 'caret-down-outlined', + 'caret-left-filled', + 'caret-left-outlined', + 'caret-right-filled', + 'caret-right-outlined', + 'caret-up-filled', + 'caret-up-outlined', + 'carry-out-filled', + 'carry-out-outlined', + 'carry-out-twotone', + 'check-circle-filled', + 'check-circle-outlined', + 'check-circle-twotone', + 'check-outlined', + 'check-square-filled', + 'check-square-outlined', + 'check-square-twotone', + 'chrome-filled', + 'chrome-outlined', + 'ci-circle-filled', + 'ci-circle-outlined', + 'ci-circle-twotone', + 'ci-outlined', + 'ci-twotone', + 'clear-outlined', + 'clock-circle-filled', + 'clock-circle-outlined', + 'clock-circle-twotone', + 'close-circle-filled', + 'close-circle-outlined', + 'close-circle-twotone', + 'close-outlined', + 'close-square-filled', + 'close-square-outlined', + 'close-square-twotone', + 'cloud-download-outlined', + 'cloud-filled', + 'cloud-outlined', + 'cloud-server-outlined', + 'cloud-sync-outlined', + 'cloud-twotone', + 'cloud-upload-outlined', + 'cluster-outlined', + 'code-filled', + 'code-outlined', + 'code-sandbox-circle-filled', + 'code-sandbox-outlined', + 'code-sandbox-square-filled', + 'code-twotone', + 'codepen-circle-filled', + 'codepen-circle-outlined', + 'codepen-outlined', + 'codepen-square-filled', + 'coffee-outlined', + 'column-height-outlined', + 'column-width-outlined', + 'comment-outlined', + 'compass-filled', + 'compass-outlined', + 'compass-twotone', + 'compress-outlined', + 'console-sql-outlined', + 'contacts-filled', + 'contacts-outlined', + 'contacts-twotone', + 'container-filled', + 'container-outlined', + 'container-twotone', + 'control-filled', + 'control-outlined', + 'control-twotone', + 'copy-filled', + 'copy-outlined', + 'copy-twotone', + 'copyright-circle-filled', + 'copyright-circle-outlined', + 'copyright-circle-twotone', + 'copyright-outlined', + 'copyright-twotone', + 'credit-card-filled', + 'credit-card-outlined', + 'credit-card-twotone', + 'crown-filled', + 'crown-outlined', + 'crown-twotone', + 'customer-service-filled', + 'customer-service-outlined', + 'customer-service-twotone', + 'dash-outlined', + 'dashboard-filled', + 'dashboard-outlined', + 'dashboard-twotone', + 'database-filled', + 'database-outlined', + 'database-twotone', + 'delete-column-outlined', + 'delete-filled', + 'delete-outlined', + 'delete-row-outlined', + 'delete-twotone', + 'delivered-procedure-outlined', + 'deployment-unit-outlined', + 'desktop-outlined', + 'diff-filled', + 'diff-outlined', + 'diff-twotone', + 'dingding-outlined', + 'dingtalk-circle-filled', + 'dingtalk-outlined', + 'dingtalk-square-filled', + 'disconnect-outlined', + 'dislike-filled', + 'dislike-outlined', + 'dislike-twotone', + 'dollar-circle-filled', + 'dollar-circle-outlined', + 'dollar-circle-twotone', + 'dollar-outlined', + 'dollar-twotone', + 'dot-chart-outlined', + 'double-left-outlined', + 'double-right-outlined', + 'down-circle-filled', + 'down-circle-outlined', + 'down-circle-twotone', + 'down-outlined', + 'down-square-filled', + 'down-square-outlined', + 'down-square-twotone', + 'download-outlined', + 'drag-outlined', + 'dribbble-circle-filled', + 'dribbble-outlined', + 'dribbble-square-filled', + 'dribbble-square-outlined', + 'dropbox-circle-filled', + 'dropbox-outlined', + 'dropbox-square-filled', + 'edit-filled', + 'edit-outlined', + 'edit-twotone', + 'ellipsis-outlined', + 'enter-outlined', + 'environment-filled', + 'environment-outlined', + 'environment-twotone', + 'euro-circle-filled', + 'euro-circle-outlined', + 'euro-circle-twotone', + 'euro-outlined', + 'euro-twotone', + 'exception-outlined', + 'exclamation-circle-filled', + 'exclamation-circle-outlined', + 'exclamation-circle-twotone', + 'exclamation-outlined', + 'expand-alt-outlined', + 'expand-outlined', + 'experiment-filled', + 'experiment-outlined', + 'experiment-twotone', + 'export-outlined', + 'eye-filled', + 'eye-invisible-filled', + 'eye-invisible-outlined', + 'eye-invisible-twotone', + 'eye-outlined', + 'eye-twotone', + 'facebook-filled', + 'facebook-outlined', + 'fall-outlined', + 'fast-backward-filled', + 'fast-backward-outlined', + 'fast-forward-filled', + 'fast-forward-outlined', + 'field-binary-outlined', + 'field-number-outlined', + 'field-string-outlined', + 'field-time-outlined', + 'file-add-filled', + 'file-add-outlined', + 'file-add-twotone', + 'file-done-outlined', + 'file-excel-filled', + 'file-excel-outlined', + 'file-excel-twotone', + 'file-exclamation-filled', + 'file-exclamation-outlined', + 'file-exclamation-twotone', + 'file-filled', + 'file-gif-outlined', + 'file-image-filled', + 'file-image-outlined', + 'file-image-twotone', + 'file-jpg-outlined', + 'file-markdown-filled', + 'file-markdown-outlined', + 'file-markdown-twotone', + 'file-outlined', + 'file-pdf-filled', + 'file-pdf-outlined', + 'file-pdf-twotone', + 'file-ppt-filled', + 'file-ppt-outlined', + 'file-ppt-twotone', + 'file-protect-outlined', + 'file-search-outlined', + 'file-sync-outlined', + 'file-text-filled', + 'file-text-outlined', + 'file-text-twotone', + 'file-twotone', + 'file-unknown-filled', + 'file-unknown-outlined', + 'file-unknown-twotone', + 'file-word-filled', + 'file-word-outlined', + 'file-word-twotone', + 'file-zip-filled', + 'file-zip-outlined', + 'file-zip-twotone', + 'filter-filled', + 'filter-outlined', + 'filter-twotone', + 'fire-filled', + 'fire-outlined', + 'fire-twotone', + 'flag-filled', + 'flag-outlined', + 'flag-twotone', + 'folder-add-filled', + 'folder-add-outlined', + 'folder-add-twotone', + 'folder-filled', + 'folder-open-filled', + 'folder-open-outlined', + 'folder-open-twotone', + 'folder-outlined', + 'folder-twotone', + 'folder-view-outlined', + 'font-colors-outlined', + 'font-size-outlined', + 'fork-outlined', + 'form-outlined', + 'format-painter-filled', + 'format-painter-outlined', + 'forward-filled', + 'forward-outlined', + 'frown-filled', + 'frown-outlined', + 'frown-twotone', + 'fullscreen-exit-outlined', + 'fullscreen-outlined', + 'function-outlined', + 'fund-filled', + 'fund-outlined', + 'fund-projection-screen-outlined', + 'fund-twotone', + 'fund-view-outlined', + 'funnel-plot-filled', + 'funnel-plot-outlined', + 'funnel-plot-twotone', + 'gateway-outlined', + 'gif-outlined', + 'gift-filled', + 'gift-outlined', + 'gift-twotone', + 'github-filled', + 'github-outlined', + 'gitlab-filled', + 'gitlab-outlined', + 'global-outlined', + 'gold-filled', + 'gold-outlined', + 'gold-twotone', + 'golden-filled', + 'google-circle-filled', + 'google-outlined', + 'google-plus-circle-filled', + 'google-plus-outlined', + 'google-plus-square-filled', + 'google-square-filled', + 'group-outlined', + 'hdd-filled', + 'hdd-outlined', + 'hdd-twotone', + 'heart-filled', + 'heart-outlined', + 'heart-twotone', + 'heat-map-outlined', + 'highlight-filled', + 'highlight-outlined', + 'highlight-twotone', + 'history-outlined', + 'home-filled', + 'home-outlined', + 'home-twotone', + 'hourglass-filled', + 'hourglass-outlined', + 'hourglass-twotone', + 'html5-filled', + 'html5-outlined', + 'html5-twotone', + 'idcard-filled', + 'idcard-outlined', + 'idcard-twotone', + 'ie-circle-filled', + 'ie-outlined', + 'ie-square-filled', + 'import-outlined', + 'inbox-outlined', + 'info-circle-filled', + 'info-circle-outlined', + 'info-circle-twotone', + 'info-outlined', + 'insert-row-above-outlined', + 'insert-row-below-outlined', + 'insert-row-left-outlined', + 'insert-row-right-outlined', + 'instagram-filled', + 'instagram-outlined', + 'insurance-filled', + 'insurance-outlined', + 'insurance-twotone', + 'interaction-filled', + 'interaction-outlined', + 'interaction-twotone', + 'issues-close-outlined', + 'italic-outlined', + 'key-outlined', + 'laptop-outlined', + 'layout-filled', + 'layout-outlined', + 'layout-twotone', + 'left-circle-filled', + 'left-circle-outlined', + 'left-circle-twotone', + 'left-outlined', + 'left-square-filled', + 'left-square-outlined', + 'left-square-twotone', + 'like-filled', + 'like-outlined', + 'like-twotone', + 'line-chart-outlined', + 'line-height-outlined', + 'line-outlined', + 'link-outlined', + 'linkedin-filled', + 'linkedin-outlined', + 'loading-3-quarters-outlined', + 'loading-outlined', + 'lock-filled', + 'lock-outlined', + 'lock-twotone', + 'login-outlined', + 'logout-outlined', + 'mac-command-filled', + 'mac-command-outlined', + 'mail-filled', + 'mail-outlined', + 'mail-twotone', + 'man-outlined', + 'medicine-box-filled', + 'medicine-box-outlined', + 'medicine-box-twotone', + 'medium-circle-filled', + 'medium-outlined', + 'medium-square-filled', + 'medium-workmark-outlined', + 'meh-filled', + 'meh-outlined', + 'meh-twotone', + 'menu-fold-outlined', + 'menu-outlined', + 'menu-unfold-outlined', + 'merge-cells-outlined', + 'message-filled', + 'message-outlined', + 'message-twotone', + 'minus-circle-filled', + 'minus-circle-outlined', + 'minus-circle-twotone', + 'minus-outlined', + 'minus-square-filled', + 'minus-square-outlined', + 'minus-square-twotone', + 'mobile-filled', + 'mobile-outlined', + 'mobile-twotone', + 'money-collect-filled', + 'money-collect-outlined', + 'money-collect-twotone', + 'monitor-outlined', + 'more-outlined', + 'node-collapse-outlined', + 'node-expand-outlined', + 'node-index-outlined', + 'notification-filled', + 'notification-outlined', + 'notification-twotone', + 'number-outlined', + 'one-to-one-outlined', + 'ordered-list-outlined', + 'paper-clip-outlined', + 'partition-outlined', + 'pause-circle-filled', + 'pause-circle-outlined', + 'pause-circle-twotone', + 'pause-outlined', + 'pay-circle-filled', + 'pay-circle-outlined', + 'percentage-outlined', + 'phone-filled', + 'phone-outlined', + 'phone-twotone', + 'pic-center-outlined', + 'pic-left-outlined', + 'pic-right-outlined', + 'picture-filled', + 'picture-outlined', + 'picture-twotone', + 'pie-chart-filled', + 'pie-chart-outlined', + 'pie-chart-twotone', + 'play-circle-filled', + 'play-circle-outlined', + 'play-circle-twotone', + 'play-square-filled', + 'play-square-outlined', + 'play-square-twotone', + 'plus-circle-filled', + 'plus-circle-outlined', + 'plus-circle-twotone', + 'plus-outlined', + 'plus-square-filled', + 'plus-square-outlined', + 'plus-square-twotone', + 'pound-circle-filled', + 'pound-circle-outlined', + 'pound-circle-twotone', + 'pound-outlined', + 'poweroff-outlined', + 'printer-filled', + 'printer-outlined', + 'printer-twotone', + 'profile-filled', + 'profile-outlined', + 'profile-twotone', + 'project-filled', + 'project-outlined', + 'project-twotone', + 'property-safety-filled', + 'property-safety-outlined', + 'property-safety-twotone', + 'pull-request-outlined', + 'pushpin-filled', + 'pushpin-outlined', + 'pushpin-twotone', + 'qq-circle-filled', + 'qq-outlined', + 'qq-square-filled', + 'qrcode-outlined', + 'question-circle-filled', + 'question-circle-outlined', + 'question-circle-twotone', + 'question-outlined', + 'radar-chart-outlined', + 'radius-bottomleft-outlined', + 'radius-bottomright-outlined', + 'radius-setting-outlined', + 'radius-upleft-outlined', + 'radius-upright-outlined', + 'read-filled', + 'read-outlined', + 'reconciliation-filled', + 'reconciliation-outlined', + 'reconciliation-twotone', + 'red-envelope-filled', + 'red-envelope-outlined', + 'red-envelope-twotone', + 'reddit-circle-filled', + 'reddit-outlined', + 'reddit-square-filled', + 'redo-outlined', + 'reload-outlined', + 'rest-filled', + 'rest-outlined', + 'rest-twotone', + 'retweet-outlined', + 'right-circle-filled', + 'right-circle-outlined', + 'right-circle-twotone', + 'right-outlined', + 'right-square-filled', + 'right-square-outlined', + 'right-square-twotone', + 'rise-outlined', + 'robot-filled', + 'robot-outlined', + 'rocket-filled', + 'rocket-outlined', + 'rocket-twotone', + 'rollback-outlined', + 'rotate-left-outlined', + 'rotate-right-outlined', + 'safety-certificate-filled', + 'safety-certificate-outlined', + 'safety-certificate-twotone', + 'safety-outlined', + 'save-filled', + 'save-outlined', + 'save-twotone', + 'scan-outlined', + 'schedule-filled', + 'schedule-outlined', + 'schedule-twotone', + 'scissor-outlined', + 'search-outlined', + 'security-scan-filled', + 'security-scan-outlined', + 'security-scan-twotone', + 'select-outlined', + 'send-outlined', + 'setting-filled', + 'setting-outlined', + 'setting-twotone', + 'shake-outlined', + 'share-alt-outlined', + 'shop-filled', + 'shop-outlined', + 'shop-twotone', + 'shopping-cart-outlined', + 'shopping-filled', + 'shopping-outlined', + 'shopping-twotone', + 'shrink-outlined', + 'signal-filled', + 'sisternode-outlined', + 'sketch-circle-filled', + 'sketch-outlined', + 'sketch-square-filled', + 'skin-filled', + 'skin-outlined', + 'skin-twotone', + 'skype-filled', + 'skype-outlined', + 'slack-circle-filled', + 'slack-outlined', + 'slack-square-filled', + 'slack-square-outlined', + 'sliders-filled', + 'sliders-outlined', + 'sliders-twotone', + 'small-dash-outlined', + 'smile-filled', + 'smile-outlined', + 'smile-twotone', + 'snippets-filled', + 'snippets-outlined', + 'snippets-twotone', + 'solution-outlined', + 'sort-ascending-outlined', + 'sort-descending-outlined', + 'sound-filled', + 'sound-outlined', + 'sound-twotone', + 'split-cells-outlined', + 'star-filled', + 'star-outlined', + 'star-twotone', + 'step-backward-filled', + 'step-backward-outlined', + 'step-forward-filled', + 'step-forward-outlined', + 'stock-outlined', + 'stop-filled', + 'stop-outlined', + 'stop-twotone', + 'strikethrough-outlined', + 'subnode-outlined', + 'swap-left-outlined', + 'swap-outlined', + 'swap-right-outlined', + 'switcher-filled', + 'switcher-outlined', + 'switcher-twotone', + 'sync-outlined', + 'table-outlined', + 'tablet-filled', + 'tablet-outlined', + 'tablet-twotone', + 'tag-filled', + 'tag-outlined', + 'tag-twotone', + 'tags-filled', + 'tags-outlined', + 'tags-twotone', + 'taobao-circle-filled', + 'taobao-circle-outlined', + 'taobao-outlined', + 'taobao-square-filled', + 'team-outlined', + 'thunderbolt-filled', + 'thunderbolt-outlined', + 'thunderbolt-twotone', + 'to-top-outlined', + 'tool-filled', + 'tool-outlined', + 'tool-twotone', + 'trademark-circle-filled', + 'trademark-circle-outlined', + 'trademark-circle-twotone', + 'trademark-outlined', + 'transaction-outlined', + 'translation-outlined', + 'trophy-filled', + 'trophy-outlined', + 'trophy-twotone', + 'twitter-circle-filled', + 'twitter-outlined', + 'twitter-square-filled', + 'underline-outlined', + 'undo-outlined', + 'ungroup-outlined', + 'unlock-filled', + 'unlock-outlined', + 'unlock-twotone', + 'unordered-list-outlined', + 'up-circle-filled', + 'up-circle-outlined', + 'up-circle-twotone', + 'up-outlined', + 'up-square-filled', + 'up-square-outlined', + 'up-square-twotone', + 'upload-outlined', + 'usb-filled', + 'usb-outlined', + 'usb-twotone', + 'user-add-outlined', + 'user-delete-outlined', + 'user-outlined', + 'user-switch-outlined', + 'usergroup-add-outlined', + 'usergroup-delete-outlined', + 'verified-outlined', + 'vertical-align-bottom-outlined', + 'vertical-align-middle-outlined', + 'vertical-align-top-outlined', + 'vertical-left-outlined', + 'vertical-right-outlined', + 'video-camera-add-outlined', + 'video-camera-filled', + 'video-camera-outlined', + 'video-camera-twotone', + 'wallet-filled', + 'wallet-outlined', + 'wallet-twotone', + 'warning-filled', + 'warning-outlined', + 'warning-twotone', + 'wechat-filled', + 'wechat-outlined', + 'weibo-circle-filled', + 'weibo-circle-outlined', + 'weibo-outlined', + 'weibo-square-filled', + 'weibo-square-outlined', + 'whats-app-outlined', + 'wifi-outlined', + 'windows-filled', + 'windows-outlined', + 'woman-outlined', + 'yahoo-filled', + 'yahoo-outlined', + 'youtube-filled', + 'youtube-outlined', + 'yuque-filled', + 'yuque-outlined', + 'zhihu-circle-filled', + 'zhihu-outlined', + 'zhihu-square-filled', + 'zoom-in-outlined', + 'zoom-out-outlined', + ], + prefix: 'ant-design', +}; diff --git a/apps/vben5/playground/src/views/demos/features/icons/index.vue b/apps/vben5/playground/src/views/demos/features/icons/index.vue new file mode 100644 index 000000000..8c90e29fb --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/icons/index.vue @@ -0,0 +1,86 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/login-expired/index.vue b/apps/vben5/playground/src/views/demos/features/login-expired/index.vue new file mode 100644 index 000000000..532839af8 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/login-expired/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/menu-query/index.vue b/apps/vben5/playground/src/views/demos/features/menu-query/index.vue new file mode 100644 index 000000000..6943a248d --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/menu-query/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/new-window/index.vue b/apps/vben5/playground/src/views/demos/features/new-window/index.vue new file mode 100644 index 000000000..68f89d0d0 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/new-window/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/tabs/index.vue b/apps/vben5/playground/src/views/demos/features/tabs/index.vue new file mode 100644 index 000000000..5e7bfa2eb --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/tabs/index.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/tabs/tab-detail.vue b/apps/vben5/playground/src/views/demos/features/tabs/tab-detail.vue new file mode 100644 index 000000000..c03b61c8c --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/tabs/tab-detail.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/vue-query/index.vue b/apps/vben5/playground/src/views/demos/features/vue-query/index.vue new file mode 100644 index 000000000..d57cf8120 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/vue-query/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/vue-query/infinite-queries.vue b/apps/vben5/playground/src/views/demos/features/vue-query/infinite-queries.vue new file mode 100644 index 000000000..2add4c158 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/vue-query/infinite-queries.vue @@ -0,0 +1,58 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/vue-query/paginated-queries.vue b/apps/vben5/playground/src/views/demos/features/vue-query/paginated-queries.vue new file mode 100644 index 000000000..b2e3c26d4 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/vue-query/paginated-queries.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/vue-query/query-retries.vue b/apps/vben5/playground/src/views/demos/features/vue-query/query-retries.vue new file mode 100644 index 000000000..0080386f8 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/vue-query/query-retries.vue @@ -0,0 +1,34 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/features/vue-query/typing.ts b/apps/vben5/playground/src/views/demos/features/vue-query/typing.ts new file mode 100644 index 000000000..95558dd8c --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/vue-query/typing.ts @@ -0,0 +1,18 @@ +export interface IProducts { + limit: number; + products: { + brand: string; + category: string; + description: string; + discountPercentage: string; + id: string; + images: string[]; + price: string; + rating: string; + stock: string; + thumbnail: string; + title: string; + }[]; + skip: number; + total: number; +} diff --git a/apps/vben5/playground/src/views/demos/features/watermark/index.vue b/apps/vben5/playground/src/views/demos/features/watermark/index.vue new file mode 100644 index 000000000..77b3f1799 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/features/watermark/index.vue @@ -0,0 +1,86 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/nested/menu-1.vue b/apps/vben5/playground/src/views/demos/nested/menu-1.vue new file mode 100644 index 000000000..f394930f2 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/nested/menu-1.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/nested/menu-2-1.vue b/apps/vben5/playground/src/views/demos/nested/menu-2-1.vue new file mode 100644 index 000000000..f394930f2 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/nested/menu-2-1.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/nested/menu-3-1.vue b/apps/vben5/playground/src/views/demos/nested/menu-3-1.vue new file mode 100644 index 000000000..f394930f2 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/nested/menu-3-1.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/vben5/playground/src/views/demos/nested/menu-3-2-1.vue b/apps/vben5/playground/src/views/demos/nested/menu-3-2-1.vue new file mode 100644 index 000000000..f394930f2 --- /dev/null +++ b/apps/vben5/playground/src/views/demos/nested/menu-3-2-1.vue @@ -0,0 +1,7 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/captcha/point-selection-captcha.vue b/apps/vben5/playground/src/views/examples/captcha/point-selection-captcha.vue new file mode 100644 index 000000000..ae8ab0aff --- /dev/null +++ b/apps/vben5/playground/src/views/examples/captcha/point-selection-captcha.vue @@ -0,0 +1,181 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/captcha/slider-captcha.vue b/apps/vben5/playground/src/views/examples/captcha/slider-captcha.vue new file mode 100644 index 000000000..0a397655a --- /dev/null +++ b/apps/vben5/playground/src/views/examples/captcha/slider-captcha.vue @@ -0,0 +1,117 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/captcha/slider-rotate-captcha.vue b/apps/vben5/playground/src/views/examples/captcha/slider-rotate-captcha.vue new file mode 100644 index 000000000..237757741 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/captcha/slider-rotate-captcha.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/doc-button.vue b/apps/vben5/playground/src/views/examples/doc-button.vue new file mode 100644 index 000000000..529c743a2 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/doc-button.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/drawer/auto-height-demo.vue b/apps/vben5/playground/src/views/examples/drawer/auto-height-demo.vue new file mode 100644 index 000000000..0c327340c --- /dev/null +++ b/apps/vben5/playground/src/views/examples/drawer/auto-height-demo.vue @@ -0,0 +1,47 @@ + + diff --git a/apps/vben5/playground/src/views/examples/drawer/base-demo.vue b/apps/vben5/playground/src/views/examples/drawer/base-demo.vue new file mode 100644 index 000000000..ffa5e3900 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/drawer/base-demo.vue @@ -0,0 +1,24 @@ + + diff --git a/apps/vben5/playground/src/views/examples/drawer/dynamic-demo.vue b/apps/vben5/playground/src/views/examples/drawer/dynamic-demo.vue new file mode 100644 index 000000000..ce5560fab --- /dev/null +++ b/apps/vben5/playground/src/views/examples/drawer/dynamic-demo.vue @@ -0,0 +1,31 @@ + + diff --git a/apps/vben5/playground/src/views/examples/drawer/form-drawer-demo.vue b/apps/vben5/playground/src/views/examples/drawer/form-drawer-demo.vue new file mode 100644 index 000000000..5af392f65 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/drawer/form-drawer-demo.vue @@ -0,0 +1,56 @@ + + diff --git a/apps/vben5/playground/src/views/examples/drawer/index.vue b/apps/vben5/playground/src/views/examples/drawer/index.vue new file mode 100644 index 000000000..d5fd784b7 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/drawer/index.vue @@ -0,0 +1,125 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/drawer/shared-data-demo.vue b/apps/vben5/playground/src/views/examples/drawer/shared-data-demo.vue new file mode 100644 index 000000000..d54227398 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/drawer/shared-data-demo.vue @@ -0,0 +1,29 @@ + + diff --git a/apps/vben5/playground/src/views/examples/ellipsis/index.vue b/apps/vben5/playground/src/views/examples/ellipsis/index.vue new file mode 100644 index 000000000..ff5066f30 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/ellipsis/index.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/api.vue b/apps/vben5/playground/src/views/examples/form/api.vue new file mode 100644 index 000000000..eceeea3ef --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/api.vue @@ -0,0 +1,249 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/basic.vue b/apps/vben5/playground/src/views/examples/form/basic.vue new file mode 100644 index 000000000..5124a3cfd --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/basic.vue @@ -0,0 +1,349 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/custom.vue b/apps/vben5/playground/src/views/examples/form/custom.vue new file mode 100644 index 000000000..a035e597b --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/custom.vue @@ -0,0 +1,75 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/dynamic.vue b/apps/vben5/playground/src/views/examples/form/dynamic.vue new file mode 100644 index 000000000..c11a3c1aa --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/dynamic.vue @@ -0,0 +1,262 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/merge.vue b/apps/vben5/playground/src/views/examples/form/merge.vue new file mode 100644 index 000000000..22f20fa10 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/merge.vue @@ -0,0 +1,116 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/query.vue b/apps/vben5/playground/src/views/examples/form/query.vue new file mode 100644 index 000000000..232923624 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/query.vue @@ -0,0 +1,147 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/form/rules.vue b/apps/vben5/playground/src/views/examples/form/rules.vue new file mode 100644 index 000000000..dd95894cd --- /dev/null +++ b/apps/vben5/playground/src/views/examples/form/rules.vue @@ -0,0 +1,243 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/modal/auto-height-demo.vue b/apps/vben5/playground/src/views/examples/modal/auto-height-demo.vue new file mode 100644 index 000000000..28939a62c --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/auto-height-demo.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/modal/base-demo.vue b/apps/vben5/playground/src/views/examples/modal/base-demo.vue new file mode 100644 index 000000000..40afb869d --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/base-demo.vue @@ -0,0 +1,26 @@ + + diff --git a/apps/vben5/playground/src/views/examples/modal/drag-demo.vue b/apps/vben5/playground/src/views/examples/modal/drag-demo.vue new file mode 100644 index 000000000..452030363 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/drag-demo.vue @@ -0,0 +1,19 @@ + + diff --git a/apps/vben5/playground/src/views/examples/modal/dynamic-demo.vue b/apps/vben5/playground/src/views/examples/modal/dynamic-demo.vue new file mode 100644 index 000000000..33d1c4eeb --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/dynamic-demo.vue @@ -0,0 +1,41 @@ + + diff --git a/apps/vben5/playground/src/views/examples/modal/form-modal-demo.vue b/apps/vben5/playground/src/views/examples/modal/form-modal-demo.vue new file mode 100644 index 000000000..5179ffebf --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/form-modal-demo.vue @@ -0,0 +1,78 @@ + + diff --git a/apps/vben5/playground/src/views/examples/modal/index.vue b/apps/vben5/playground/src/views/examples/modal/index.vue new file mode 100644 index 000000000..d6db2fba2 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/index.vue @@ -0,0 +1,126 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/modal/shared-data-demo.vue b/apps/vben5/playground/src/views/examples/modal/shared-data-demo.vue new file mode 100644 index 000000000..8ba6e7e9a --- /dev/null +++ b/apps/vben5/playground/src/views/examples/modal/shared-data-demo.vue @@ -0,0 +1,29 @@ + + diff --git a/apps/vben5/playground/src/views/examples/resize/basic.vue b/apps/vben5/playground/src/views/examples/resize/basic.vue new file mode 100644 index 000000000..6295839a6 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/resize/basic.vue @@ -0,0 +1,58 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/basic.vue b/apps/vben5/playground/src/views/examples/vxe-table/basic.vue new file mode 100644 index 000000000..1216468d4 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/basic.vue @@ -0,0 +1,96 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/custom-cell.vue b/apps/vben5/playground/src/views/examples/vxe-table/custom-cell.vue new file mode 100644 index 000000000..46fd29cce --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/custom-cell.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/edit-cell.vue b/apps/vben5/playground/src/views/examples/vxe-table/edit-cell.vue new file mode 100644 index 000000000..9aebde867 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/edit-cell.vue @@ -0,0 +1,57 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/edit-row.vue b/apps/vben5/playground/src/views/examples/vxe-table/edit-row.vue new file mode 100644 index 000000000..f9d25cac5 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/edit-row.vue @@ -0,0 +1,94 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/fixed.vue b/apps/vben5/playground/src/views/examples/vxe-table/fixed.vue new file mode 100644 index 000000000..399d35210 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/fixed.vue @@ -0,0 +1,69 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/form.vue b/apps/vben5/playground/src/views/examples/vxe-table/form.vue new file mode 100644 index 000000000..fad0cf58f --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/form.vue @@ -0,0 +1,110 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/remote.vue b/apps/vben5/playground/src/views/examples/vxe-table/remote.vue new file mode 100644 index 000000000..7fc98a08b --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/remote.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/table-data.ts b/apps/vben5/playground/src/views/examples/vxe-table/table-data.ts new file mode 100644 index 000000000..b4eb5ede1 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/table-data.ts @@ -0,0 +1,172 @@ +interface TableRowData { + address: string; + age: number; + id: number; + name: string; + nickname: string; + role: string; +} + +const roles = ['User', 'Admin', 'Manager', 'Guest']; + +export const MOCK_TABLE_DATA: TableRowData[] = (() => { + const data: TableRowData[] = []; + for (let i = 0; i < 40; i++) { + data.push({ + address: `New York${i}`, + age: i + 1, + id: i, + name: `Test${i}`, + nickname: `Test${i}`, + role: roles[Math.floor(Math.random() * roles.length)] as string, + }); + } + return data; +})(); + +export const MOCK_TREE_TABLE_DATA = [ + { + date: '2020-08-01', + id: 10_000, + name: 'Test1', + parentId: null, + size: 1024, + type: 'mp3', + }, + { + date: '2021-04-01', + id: 10_050, + name: 'Test2', + parentId: null, + size: 0, + type: 'mp4', + }, + { + date: '2020-03-01', + id: 24_300, + name: 'Test3', + parentId: 10_050, + size: 1024, + type: 'avi', + }, + { + date: '2021-04-01', + id: 20_045, + name: 'Test4', + parentId: 24_300, + size: 600, + type: 'html', + }, + { + date: '2021-04-01', + id: 10_053, + name: 'Test5', + parentId: 24_300, + size: 0, + type: 'avi', + }, + { + date: '2021-10-01', + id: 24_330, + name: 'Test6', + parentId: 10_053, + size: 25, + type: 'txt', + }, + { + date: '2020-01-01', + id: 21_011, + name: 'Test7', + parentId: 10_053, + size: 512, + type: 'pdf', + }, + { + date: '2021-06-01', + id: 22_200, + name: 'Test8', + parentId: 10_053, + size: 1024, + type: 'js', + }, + { + date: '2020-11-01', + id: 23_666, + name: 'Test9', + parentId: null, + size: 2048, + type: 'xlsx', + }, + { + date: '2021-06-01', + id: 23_677, + name: 'Test10', + parentId: 23_666, + size: 1024, + type: 'js', + }, + { + date: '2021-06-01', + id: 23_671, + name: 'Test11', + parentId: 23_677, + size: 1024, + type: 'js', + }, + { + date: '2021-06-01', + id: 23_672, + name: 'Test12', + parentId: 23_677, + size: 1024, + type: 'js', + }, + { + date: '2021-06-01', + id: 23_688, + name: 'Test13', + parentId: 23_666, + size: 1024, + type: 'js', + }, + { + date: '2021-06-01', + id: 23_681, + name: 'Test14', + parentId: 23_688, + size: 1024, + type: 'js', + }, + { + date: '2021-06-01', + id: 23_682, + name: 'Test15', + parentId: 23_688, + size: 1024, + type: 'js', + }, + { + date: '2020-10-01', + id: 24_555, + name: 'Test16', + parentId: null, + size: 224, + type: 'avi', + }, + { + date: '2021-06-01', + id: 24_566, + name: 'Test17', + parentId: 24_555, + size: 1024, + type: 'js', + }, + { + date: '2021-06-01', + id: 24_577, + name: 'Test18', + parentId: 24_555, + size: 1024, + type: 'js', + }, +]; diff --git a/apps/vben5/playground/src/views/examples/vxe-table/tree.vue b/apps/vben5/playground/src/views/examples/vxe-table/tree.vue new file mode 100644 index 000000000..c8a1df46f --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/tree.vue @@ -0,0 +1,62 @@ + + + diff --git a/apps/vben5/playground/src/views/examples/vxe-table/virtual.vue b/apps/vben5/playground/src/views/examples/vxe-table/virtual.vue new file mode 100644 index 000000000..f35a691b3 --- /dev/null +++ b/apps/vben5/playground/src/views/examples/vxe-table/virtual.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/vben5/playground/tailwind.config.mjs b/apps/vben5/playground/tailwind.config.mjs new file mode 100644 index 000000000..f17f556fa --- /dev/null +++ b/apps/vben5/playground/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/apps/vben5/playground/tsconfig.json b/apps/vben5/playground/tsconfig.json new file mode 100644 index 000000000..02c287fe6 --- /dev/null +++ b/apps/vben5/playground/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web-app.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "#/*": ["./src/*"] + } + }, + "references": [{ "path": "./tsconfig.node.json" }], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/apps/vben5/playground/tsconfig.node.json b/apps/vben5/playground/tsconfig.node.json new file mode 100644 index 000000000..c2f0d86cc --- /dev/null +++ b/apps/vben5/playground/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/node.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "noEmit": false + }, + "include": ["vite.config.mts"] +} diff --git a/apps/vben5/playground/vite.config.mts b/apps/vben5/playground/vite.config.mts new file mode 100644 index 000000000..b6360f1d4 --- /dev/null +++ b/apps/vben5/playground/vite.config.mts @@ -0,0 +1,20 @@ +import { defineConfig } from '@vben/vite-config'; + +export default defineConfig(async () => { + return { + application: {}, + vite: { + server: { + proxy: { + '/api': { + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + // mock代理目标地址 + target: 'http://localhost:5320/api', + ws: true, + }, + }, + }, + }, + }; +}); diff --git a/apps/vben5/pnpm-workspace.yaml b/apps/vben5/pnpm-workspace.yaml new file mode 100644 index 000000000..cfbfa7f21 --- /dev/null +++ b/apps/vben5/pnpm-workspace.yaml @@ -0,0 +1,181 @@ +packages: + - internal/* + - internal/lint-configs/* + - packages/* + - packages/@abp/* + - packages/@core/base/* + - packages/@core/ui-kit/* + - packages/@core/forward/* + - packages/@core/* + - packages/effects/* + - packages/business/* + - apps/* + - scripts/* + - docs + - playground +catalog: + '@ast-grep/napi': ^0.30.1 + '@ant-design/icons-vue': ^7.0.1 + '@changesets/changelog-github': ^0.5.0 + '@changesets/cli': ^2.27.10 + '@changesets/git': ^3.0.2 + '@clack/prompts': ^0.8.2 + '@commitlint/cli': ^19.6.0 + '@commitlint/config-conventional': ^19.6.0 + '@ctrl/tinycolor': ^4.1.0 + '@eslint/js': ^9.16.0 + '@faker-js/faker': ^9.2.0 + '@iconify/json': ^2.2.278 + '@iconify/tailwind': ^1.1.3 + '@iconify/vue': ^4.1.2 + '@intlify/core-base': ^10.0.5 + '@intlify/unplugin-vue-i18n': ^6.0.0 + '@jspm/generator': ^2.4.1 + '@manypkg/get-packages': ^2.2.2 + '@nolebase/vitepress-plugin-git-changelog': ^2.11.1 + '@playwright/test': ^1.49.0 + '@pnpm/workspace.read-manifest': ^1000.0.0 + '@stylistic/stylelint-plugin': ^3.1.1 + '@tailwindcss/nesting': 0.0.0-insiders.565cd3e + '@tailwindcss/typography': ^0.5.15 + '@tanstack/vue-query': ^5.62.0 + '@tanstack/vue-store': ^0.6.0 + '@types/archiver': ^6.0.3 + '@types/eslint': ^9.6.1 + '@types/html-minifier-terser': ^7.0.2 + '@types/jsonwebtoken': ^9.0.7 + '@types/lodash.clonedeep': ^4.5.9 + '@types/node': ^22.10.0 + '@types/nprogress': ^0.2.3 + '@types/postcss-import': ^14.0.3 + '@types/qrcode': ^1.5.5 + '@types/sortablejs': ^1.15.8 + '@typescript-eslint/eslint-plugin': ^8.16.0 + '@typescript-eslint/parser': ^8.16.0 + '@vee-validate/zod': ^4.14.7 + '@vite-pwa/vitepress': ^0.5.3 + '@vitejs/plugin-vue': ^5.2.1 + '@vitejs/plugin-vue-jsx': ^4.1.1 + '@vue/reactivity': ^3.5.13 + '@vue/shared': ^3.5.13 + '@vue/test-utils': ^2.4.6 + '@vueuse/core': ^12.0.0 + '@vueuse/integrations': ^12.0.0 + ant-design-vue: ^4.2.6 + archiver: ^7.0.1 + autoprefixer: ^10.4.20 + axios: ^1.7.8 + axios-mock-adapter: ^2.1.0 + cac: ^6.7.14 + chalk: ^5.3.0 + cheerio: 1.0.0 + circular-dependency-scanner: ^2.3.0 + class-variance-authority: ^0.7.1 + clsx: ^2.1.1 + commitlint-plugin-function-rules: ^4.0.1 + consola: ^3.2.3 + cross-env: ^7.0.3 + cspell: ^8.16.1 + cssnano: ^7.0.6 + cz-git: ^1.11.0 + czg: ^1.11.0 + dayjs: ^1.11.13 + defu: ^6.1.4 + depcheck: ^1.4.7 + dotenv: ^16.4.5 + echarts: ^5.5.1 + element-plus: ^2.9.0 + eslint: ^9.16.0 + eslint-config-turbo: ^2.3.3 + eslint-plugin-command: ^0.2.6 + eslint-plugin-eslint-comments: ^3.2.0 + eslint-plugin-import-x: ^4.4.3 + eslint-plugin-jsdoc: ^50.6.0 + eslint-plugin-jsonc: ^2.18.2 + eslint-plugin-n: ^17.14.0 + eslint-plugin-no-only-tests: ^3.3.0 + eslint-plugin-perfectionist: ^3.9.1 + eslint-plugin-prettier: ^5.2.1 + eslint-plugin-regexp: ^2.7.0 + eslint-plugin-unicorn: ^56.0.1 + eslint-plugin-unused-imports: ^4.1.4 + eslint-plugin-vitest: ^0.5.4 + eslint-plugin-vue: ^9.32.0 + execa: ^9.5.1 + find-up: ^7.0.0 + get-port: ^7.1.0 + globals: ^15.12.0 + h3: ^1.13.0 + happy-dom: ^15.11.7 + html-minifier-terser: ^7.2.0 + husky: ^9.1.7 + is-ci: ^3.0.1 + jsonc-eslint-parser: ^2.4.0 + jsonwebtoken: ^9.0.2 + lint-staged: ^15.2.10 + lodash.clonedeep: ^4.5.0 + lucide-vue-next: ^0.461.0 + medium-zoom: ^1.1.0 + naive-ui: ^2.40.2 + nitropack: ^2.10.4 + nprogress: ^0.2.0 + ora: ^8.1.1 + pinia: 2.2.2 + pinia-plugin-persistedstate: ^4.1.3 + pkg-types: ^1.2.1 + playwright: ^1.49.0 + postcss: ^8.4.49 + postcss-antd-fixes: ^0.2.0 + postcss-html: ^1.7.0 + postcss-import: ^16.1.0 + postcss-preset-env: ^10.1.1 + postcss-scss: ^4.0.9 + prettier: ^3.4.1 + prettier-plugin-tailwindcss: ^0.6.9 + publint: ^0.2.12 + qrcode: ^1.5.4 + radix-vue: ^1.9.10 + resolve.exports: ^2.0.2 + rimraf: ^6.0.1 + rollup: ^4.28.0 + rollup-plugin-visualizer: ^5.12.0 + sass: 1.80.6 + sortablejs: ^1.15.6 + stylelint: ^16.11.0 + stylelint-config-recess-order: ^5.1.1 + stylelint-config-recommended: ^14.0.1 + stylelint-config-recommended-scss: ^14.1.0 + stylelint-config-recommended-vue: ^1.5.0 + stylelint-config-standard: ^36.0.1 + stylelint-order: ^6.0.4 + stylelint-prettier: ^5.0.2 + stylelint-scss: ^6.10.0 + tailwind-merge: ^2.5.5 + tailwindcss: ^3.4.15 + tailwindcss-animate: ^1.0.7 + theme-colors: ^0.1.0 + turbo: ^2.3.3 + typescript: 5.6.3 + unbuild: ^3.0.0-rc.11 + unplugin-element-plus: ^0.8.0 + vee-validate: ^4.14.7 + vite: ^6.0.1 + vite-plugin-compression: ^0.5.1 + vite-plugin-dts: 4.2.1 + vite-plugin-html: ^3.2.2 + vite-plugin-lazy-import: ^1.0.7 + vite-plugin-pwa: ^0.21.1 + vite-plugin-vue-devtools: ^7.6.7 + vitepress: ^1.5.0 + vitepress-plugin-group-icons: ^1.3.0 + vitest: ^2.1.6 + vue: ^3.5.13 + vue-eslint-parser: ^9.4.3 + vue-i18n: ^10.0.5 + vue-router: ^4.5.0 + vue-tsc: ^2.1.10 + vxe-pc-ui: ^4.3.10 + vxe-table: ^4.9.10 + watermark-js-plus: ^1.5.7 + zod: ^3.23.8 + zod-defaults: ^0.1.3 diff --git a/apps/vben5/scripts/clean.mjs b/apps/vben5/scripts/clean.mjs new file mode 100644 index 000000000..e2840d380 --- /dev/null +++ b/apps/vben5/scripts/clean.mjs @@ -0,0 +1,53 @@ +import { promises as fs } from 'node:fs'; +import { join } from 'node:path'; + +const rootDir = process.cwd(); + +/** + * 递归查找并删除目标目录 + * @param {string} currentDir - 当前遍历的目录路径 + */ +async function cleanTargetsRecursively(currentDir, targets) { + const items = await fs.readdir(currentDir); + + for (const item of items) { + try { + const itemPath = join(currentDir, item); + if (targets.includes(item)) { + // 匹配到目标目录或文件时直接删除 + await fs.rm(itemPath, { force: true, recursive: true }); + console.log(`Deleted: ${itemPath}`); + } + const stat = await fs.lstat(itemPath); + if (stat.isDirectory()) { + await cleanTargetsRecursively(itemPath, targets); + } + } catch { + // console.error( + // `Error handling item ${item} in ${currentDir}: ${error.message}`, + // ); + } + } +} + +(async function startCleanup() { + // 要删除的目录及文件名称 + const targets = ['node_modules', 'dist', '.turbo', 'dist.zip']; + + const deleteLockFile = process.argv.includes('--del-lock'); + const cleanupTargets = [...targets]; + if (deleteLockFile) { + cleanupTargets.push('pnpm-lock.yaml'); + } + + console.log( + `Starting cleanup of targets: ${cleanupTargets.join(', ')} from root: ${rootDir}`, + ); + + try { + await cleanTargetsRecursively(rootDir, cleanupTargets); + console.log('Cleanup process completed.'); + } catch (error) { + console.error(`Unexpected error during cleanup: ${error.message}`); + } +})(); diff --git a/apps/vben5/scripts/deploy/Dockerfile b/apps/vben5/scripts/deploy/Dockerfile new file mode 100644 index 000000000..9fd151994 --- /dev/null +++ b/apps/vben5/scripts/deploy/Dockerfile @@ -0,0 +1,31 @@ +FROM node:20-slim AS builder + +# --max-old-space-size +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +ENV NODE_OPTIONS=--max-old-space-size=8192 +ENV TZ=Asia/Shanghai + +RUN corepack enable + +WORKDIR /app + +# copy package.json and pnpm-lock.yaml to workspace +COPY . /app + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build --filter=\!./docs + +RUN echo "Builder Success 🎉" + +FROM nginx:stable-alpine AS production + +RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf +COPY --from=builder /app/playground/dist /usr/share/nginx/html + +COPY --from=builder /app/scripts/deploy/nginx.conf /etc/nginx/nginx.conf + +EXPOSE 8080 + +# start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/vben5/scripts/deploy/build-local-docker-image.sh b/apps/vben5/scripts/deploy/build-local-docker-image.sh new file mode 100644 index 000000000..4881487f4 --- /dev/null +++ b/apps/vben5/scripts/deploy/build-local-docker-image.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +LOG_FILE=${SCRIPT_DIR}/build-local-docker-image.log +ERROR="" +IMAGE_NAME="vben-admin-local" + +function stop_and_remove_container() { + # Stop and remove the existing container + docker stop ${IMAGE_NAME} >/dev/null 2>&1 + docker rm ${IMAGE_NAME} >/dev/null 2>&1 +} + +function remove_image() { + # Remove the existing image + docker rmi vben-admin-pro >/dev/null 2>&1 +} + +function install_dependencies() { + # Install all dependencies + cd ${SCRIPT_DIR} + pnpm install || ERROR="install_dependencies failed" +} + +function build_image() { + # build docker + docker build ../../ -f Dockerfile -t ${IMAGE_NAME} || ERROR="build_image failed" +} + +function log_message() { + if [[ ${ERROR} != "" ]]; + then + >&2 echo "build failed, Please check build-local-docker-image.log for more details" + >&2 echo "ERROR: ${ERROR}" + exit 1 + else + echo "docker image with tag '${IMAGE_NAME}' built sussessfully. Use below sample command to run the container" + echo "" + echo "docker run -d -p 8010:8080 --name ${IMAGE_NAME} ${IMAGE_NAME}" + fi +} + +echo "Info: Stopping and removing existing container and image" | tee ${LOG_FILE} +stop_and_remove_container +remove_image + +echo "Info: Installing dependencies" | tee -a ${LOG_FILE} +install_dependencies 1>> ${LOG_FILE} 2>> ${LOG_FILE} + +if [[ ${ERROR} == "" ]]; then + echo "Info: Building docker image" | tee -a ${LOG_FILE} + build_image 1>> ${LOG_FILE} 2>> ${LOG_FILE} +fi + +log_message | tee -a ${LOG_FILE} diff --git a/apps/vben5/scripts/deploy/nginx.conf b/apps/vben5/scripts/deploy/nginx.conf new file mode 100644 index 000000000..8e6ab1016 --- /dev/null +++ b/apps/vben5/scripts/deploy/nginx.conf @@ -0,0 +1,75 @@ + +#user nobody; +worker_processes 1; + +#error_log logs/error.log; +#error_log logs/error.log notice; +#error_log logs/error.log info; + +#pid logs/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include mime.types; + default_type application/octet-stream; + + types { + application/javascript js mjs; + text/css css; + text/html html; + } + + sendfile on; + # tcp_nopush on; + + #keepalive_timeout 0; + # keepalive_timeout 65; + + # gzip on; + # gzip_buffers 32 16k; + # gzip_comp_level 6; + # gzip_min_length 1k; + # gzip_static on; + # gzip_types text/plain + # text/css + # application/javascript + # application/json + # application/x-javascript + # text/xml + # application/xml + # application/xml+rss + # text/javascript; #设置压缩的文件类型 + # gzip_vary on; + + server { + listen 8080; + server_name localhost; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + index index.html; + # Enable CORS + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } + } +} diff --git a/apps/vben5/scripts/turbo-run/README.md b/apps/vben5/scripts/turbo-run/README.md new file mode 100644 index 000000000..65ffcf4f6 --- /dev/null +++ b/apps/vben5/scripts/turbo-run/README.md @@ -0,0 +1,3 @@ +# @vben/turbo-run + +turbo-run is a command line tool that allows you to run multiple commands in parallel. diff --git a/apps/vben5/scripts/turbo-run/build.config.ts b/apps/vben5/scripts/turbo-run/build.config.ts new file mode 100644 index 000000000..97e572c56 --- /dev/null +++ b/apps/vben5/scripts/turbo-run/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/apps/vben5/scripts/turbo-run/package.json b/apps/vben5/scripts/turbo-run/package.json new file mode 100644 index 000000000..84afb2da4 --- /dev/null +++ b/apps/vben5/scripts/turbo-run/package.json @@ -0,0 +1,29 @@ +{ + "name": "@vben/turbo-run", + "version": "5.4.8", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "stub": "pnpm unbuild --stub" + }, + "files": [ + "dist" + ], + "bin": { + "turbo-run": "./bin/turbo-run.mjs" + }, + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "dependencies": { + "@clack/prompts": "catalog:", + "@vben/node-utils": "workspace:*", + "cac": "catalog:" + } +} diff --git a/apps/vben5/scripts/turbo-run/src/index.ts b/apps/vben5/scripts/turbo-run/src/index.ts new file mode 100644 index 000000000..2f8dad0ff --- /dev/null +++ b/apps/vben5/scripts/turbo-run/src/index.ts @@ -0,0 +1,29 @@ +import { colors, consola } from '@vben/node-utils'; + +import { cac } from 'cac'; + +import { run } from './run'; + +try { + const turboRun = cac('turbo-run'); + + turboRun + .command('[script]') + .usage(`Run turbo interactively.`) + .action(async (command: string) => { + run({ command }); + }); + + // Invalid command + turboRun.on('command:*', () => { + consola.error(colors.red('Invalid command!')); + process.exit(1); + }); + + turboRun.usage('turbo-run'); + turboRun.help(); + turboRun.parse(); +} catch (error) { + consola.error(error); + process.exit(1); +} diff --git a/apps/vben5/scripts/turbo-run/src/run.ts b/apps/vben5/scripts/turbo-run/src/run.ts new file mode 100644 index 000000000..6a8762fa3 --- /dev/null +++ b/apps/vben5/scripts/turbo-run/src/run.ts @@ -0,0 +1,67 @@ +import { execaCommand, getPackages } from '@vben/node-utils'; + +import { cancel, isCancel, select } from '@clack/prompts'; + +interface RunOptions { + command?: string; +} + +export async function run(options: RunOptions) { + const { command } = options; + if (!command) { + console.error('Please enter the command to run'); + process.exit(1); + } + const { packages } = await getPackages(); + // const appPkgs = await findApps(process.cwd(), packages); + // const websitePkg = packages.find( + // (item) => item.packageJson.name === '@vben/website', + // ); + + // 只显示有对应命令的包 + const selectPkgs = packages.filter((pkg) => { + return (pkg?.packageJson as Record)?.scripts?.[command]; + }); + + let selectPkg: string | symbol; + if (selectPkgs.length > 1) { + selectPkg = await select({ + message: `Select the app you need to run [${command}]:`, + options: selectPkgs.map((item) => ({ + label: item?.packageJson.name, + value: item?.packageJson.name, + })), + }); + + if (isCancel(selectPkg) || !selectPkg) { + cancel('👋 Has cancelled'); + process.exit(0); + } + } else { + selectPkg = selectPkgs[0]?.packageJson?.name ?? ''; + } + + if (!selectPkg) { + console.error('No app found'); + process.exit(1); + } + + execaCommand(`pnpm --filter=${selectPkg} run ${command}`, { + stdio: 'inherit', + }); +} + +/** + * 过滤app包 + * @param root + * @param packages + */ +// async function findApps(root: string, packages: Package[]) { +// // apps内的 +// const appPackages = packages.filter((pkg) => { +// const viteConfigExists = fs.existsSync(join(pkg.dir, 'vite.config.mts')); +// return pkg.dir.startsWith(join(root, 'apps')) && viteConfigExists; +// }); + +// return appPackages; +// } diff --git a/apps/vben5/scripts/turbo-run/tsconfig.json b/apps/vben5/scripts/turbo-run/tsconfig.json new file mode 100644 index 000000000..b2ec3b61e --- /dev/null +++ b/apps/vben5/scripts/turbo-run/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/node.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/scripts/vsh/README.md b/apps/vben5/scripts/vsh/README.md new file mode 100644 index 000000000..7cc8fbf5f --- /dev/null +++ b/apps/vben5/scripts/vsh/README.md @@ -0,0 +1,3 @@ +# @vben/vsh + +shell 脚本工具集合 diff --git a/apps/vben5/scripts/vsh/build.config.ts b/apps/vben5/scripts/vsh/build.config.ts new file mode 100644 index 000000000..97e572c56 --- /dev/null +++ b/apps/vben5/scripts/vsh/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/apps/vben5/scripts/vsh/package.json b/apps/vben5/scripts/vsh/package.json new file mode 100644 index 000000000..1c6162c7c --- /dev/null +++ b/apps/vben5/scripts/vsh/package.json @@ -0,0 +1,31 @@ +{ + "name": "@vben/vsh", + "version": "5.4.8", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "stub": "pnpm unbuild --stub" + }, + "files": [ + "dist" + ], + "bin": { + "vsh": "./bin/vsh.mjs" + }, + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "dependencies": { + "@vben/node-utils": "workspace:*", + "cac": "catalog:", + "circular-dependency-scanner": "catalog:", + "depcheck": "catalog:", + "publint": "catalog:" + } +} diff --git a/apps/vben5/scripts/vsh/src/check-circular/index.ts b/apps/vben5/scripts/vsh/src/check-circular/index.ts new file mode 100644 index 000000000..8af97894b --- /dev/null +++ b/apps/vben5/scripts/vsh/src/check-circular/index.ts @@ -0,0 +1,80 @@ +import type { CAC } from 'cac'; + +import { extname } from 'node:path'; + +import { getStagedFiles } from '@vben/node-utils'; + +import { circularDepsDetect, printCircles } from 'circular-dependency-scanner'; + +const IGNORE_DIR = [ + 'dist', + '.turbo', + 'output', + '.cache', + 'scripts', + 'internal', + 'packages/effects/request/src/', + 'packages/@core/ui-kit/menu-ui/src/', + 'packages/@core/ui-kit/popup-ui/src/', +].join(','); + +const IGNORE = [`**/{${IGNORE_DIR}}/**`]; + +interface CommandOptions { + staged: boolean; + verbose: boolean; +} + +async function checkCircular({ staged, verbose }: CommandOptions) { + const results = await circularDepsDetect({ + absolute: staged, + cwd: process.cwd(), + ignore: IGNORE, + }); + + if (staged) { + let files = await getStagedFiles(); + + const allowedExtensions = new Set([ + '.cjs', + '.js', + '.jsx', + '.mjs', + '.ts', + '.tsx', + '.vue', + ]); + + // 过滤文件列表 + files = files.filter((file) => allowedExtensions.has(extname(file))); + + const circularFiles: string[][] = []; + + for (const file of files) { + for (const result of results) { + const resultFiles = result.flat(); + if (resultFiles.includes(file)) { + circularFiles.push(result); + } + } + } + verbose && printCircles(circularFiles); + } else { + verbose && printCircles(results); + } +} + +function defineCheckCircularCommand(cac: CAC) { + cac + .command('check-circular') + .option( + '--staged', + 'Whether it is the staged commit mode, in which mode, if there is a circular dependency, an alarm will be given.', + ) + .usage(`Analysis of project circular dependencies.`) + .action(async ({ staged }) => { + await checkCircular({ staged, verbose: true }); + }); +} + +export { defineCheckCircularCommand }; diff --git a/apps/vben5/scripts/vsh/src/check-dep/index.ts b/apps/vben5/scripts/vsh/src/check-dep/index.ts new file mode 100644 index 000000000..762622995 --- /dev/null +++ b/apps/vben5/scripts/vsh/src/check-dep/index.ts @@ -0,0 +1,85 @@ +import type { CAC } from 'cac'; + +import { getPackages } from '@vben/node-utils'; + +import depcheck from 'depcheck'; + +async function runDepcheck() { + const { packages } = await getPackages(); + await Promise.all( + packages.map(async (pkg) => { + if ( + [ + '@vben/backend-mock', + '@vben/commitlint-config', + '@vben/eslint-config', + '@vben/lint-staged-config', + '@vben/node-utils', + '@vben/prettier-config', + '@vben/stylelint-config', + '@vben/tailwind-config', + '@vben/tsconfig', + '@vben/vite-config', + '@vben/vite-config', + '@vben/vsh', + ].includes(pkg.packageJson.name) + ) { + return; + } + + const unused = await depcheck(pkg.dir, { + ignoreMatches: [ + 'vite', + 'vitest', + 'unbuild', + '@vben/tsconfig', + '@vben/vite-config', + '@vben/tailwind-config', + '@types/*', + '@vben-core/design', + ], + ignorePatterns: ['dist', 'node_modules', 'public'], + }); + + // 删除file:前缀的依赖提示,该依赖是本地依赖 + Reflect.deleteProperty(unused.missing, 'file:'); + Object.keys(unused.missing).forEach((key) => { + unused.missing[key] = (unused.missing[key] || []).filter( + (item: string) => !item.startsWith('/'), + ); + if (unused.missing[key].length === 0) { + Reflect.deleteProperty(unused.missing, key); + } + }); + + if ( + Object.keys(unused.missing).length === 0 && + unused.dependencies.length === 0 && + unused.devDependencies.length === 0 + ) { + return; + } + console.error( + '\n', + pkg.packageJson.name, + '\n missing:', + unused.missing, + '\n dependencies:', + unused.dependencies, + '\n devDependencies:', + unused.devDependencies, + ); + }), + ); +} + +function defineDepcheckCommand(cac: CAC) { + cac + .command('check-dep') + .usage(`Analysis of project circular dependencies.`) + .action(async () => { + await runDepcheck(); + }); +} + +export { defineDepcheckCommand }; diff --git a/apps/vben5/scripts/vsh/src/code-workspace/index.ts b/apps/vben5/scripts/vsh/src/code-workspace/index.ts new file mode 100644 index 000000000..d5ec4ee90 --- /dev/null +++ b/apps/vben5/scripts/vsh/src/code-workspace/index.ts @@ -0,0 +1,78 @@ +import type { CAC } from 'cac'; + +import { join, relative } from 'node:path'; + +import { + colors, + consola, + findMonorepoRoot, + getPackages, + gitAdd, + outputJSON, + prettierFormat, + toPosixPath, +} from '@vben/node-utils'; + +const CODE_WORKSPACE_FILE = join('vben-admin.code-workspace'); + +interface CodeWorkspaceCommandOptions { + autoCommit?: boolean; + spaces?: number; +} + +async function createCodeWorkspace({ + autoCommit = false, + spaces = 2, +}: CodeWorkspaceCommandOptions) { + const { packages, rootDir } = await getPackages(); + + let folders = packages.map((pkg) => { + const { dir, packageJson } = pkg; + return { + name: packageJson.name, + path: toPosixPath(relative(rootDir, dir)), + }; + }); + + folders = folders.filter(Boolean); + + const monorepoRoot = findMonorepoRoot(); + const outputPath = join(monorepoRoot, CODE_WORKSPACE_FILE); + await outputJSON(outputPath, { folders }, spaces); + + await prettierFormat(outputPath); + if (autoCommit) { + await gitAdd(CODE_WORKSPACE_FILE, monorepoRoot); + } +} + +async function runCodeWorkspace({ + autoCommit, + spaces, +}: CodeWorkspaceCommandOptions) { + await createCodeWorkspace({ + autoCommit, + spaces, + }); + if (autoCommit) { + return; + } + consola.log(''); + consola.success(colors.green(`${CODE_WORKSPACE_FILE} is updated!`)); + consola.log(''); +} + +function defineCodeWorkspaceCommand(cac: CAC) { + cac + .command('code-workspace') + .usage('Update the `.code-workspace` file') + .option('--spaces [number]', '.code-workspace JSON file spaces.', { + default: 2, + }) + .option('--auto-commit', 'auto commit .code-workspace JSON file.', { + default: false, + }) + .action(runCodeWorkspace); +} + +export { defineCodeWorkspaceCommand }; diff --git a/apps/vben5/scripts/vsh/src/index.ts b/apps/vben5/scripts/vsh/src/index.ts new file mode 100644 index 000000000..2b00448c3 --- /dev/null +++ b/apps/vben5/scripts/vsh/src/index.ts @@ -0,0 +1,41 @@ +import { colors, consola } from '@vben/node-utils'; + +import { cac } from 'cac'; + +import { defineCheckCircularCommand } from './check-circular'; +import { defineDepcheckCommand } from './check-dep'; +import { defineCodeWorkspaceCommand } from './code-workspace'; +import { defineLintCommand } from './lint'; +import { definePubLintCommand } from './publint'; + +try { + const vsh = cac('vsh'); + + // vsh lint + defineLintCommand(vsh); + + // vsh publint + definePubLintCommand(vsh); + + // vsh code-workspace + defineCodeWorkspaceCommand(vsh); + + // vsh check-circular + defineCheckCircularCommand(vsh); + + // vsh check-dep + defineDepcheckCommand(vsh); + + // Invalid command + vsh.on('command:*', () => { + consola.error(colors.red('Invalid command!')); + process.exit(1); + }); + + vsh.usage('vsh'); + vsh.help(); + vsh.parse(); +} catch (error) { + consola.error(error); + process.exit(1); +} diff --git a/apps/vben5/scripts/vsh/src/lint/index.ts b/apps/vben5/scripts/vsh/src/lint/index.ts new file mode 100644 index 000000000..5d381e819 --- /dev/null +++ b/apps/vben5/scripts/vsh/src/lint/index.ts @@ -0,0 +1,48 @@ +import type { CAC } from 'cac'; + +import { execaCommand } from '@vben/node-utils'; + +interface LintCommandOptions { + /** + * Format lint problem. + */ + format?: boolean; +} + +async function runLint({ format }: LintCommandOptions) { + // process.env.FORCE_COLOR = '3'; + + if (format) { + await execaCommand(`stylelint "**/*.{vue,css,less.scss}" --cache --fix`, { + stdio: 'inherit', + }); + await execaCommand(`eslint . --cache --fix`, { + stdio: 'inherit', + }); + await execaCommand(`prettier . --write --cache --log-level warn`, { + stdio: 'inherit', + }); + return; + } + await Promise.all([ + execaCommand(`eslint . --cache`, { + stdio: 'inherit', + }), + execaCommand(`prettier . --ignore-unknown --check --cache`, { + stdio: 'inherit', + }), + execaCommand(`stylelint "**/*.{vue,css,less.scss}" --cache`, { + stdio: 'inherit', + }), + ]); +} + +function defineLintCommand(cac: CAC) { + cac + .command('lint') + .usage('Batch execute project lint check.') + .option('--format', 'Format lint problem.') + .action(runLint); +} + +export { defineLintCommand }; diff --git a/apps/vben5/scripts/vsh/src/publint/index.ts b/apps/vben5/scripts/vsh/src/publint/index.ts new file mode 100644 index 000000000..658c0c5c0 --- /dev/null +++ b/apps/vben5/scripts/vsh/src/publint/index.ts @@ -0,0 +1,185 @@ +import type { CAC } from 'cac'; +import type { Result } from 'publint'; + +import { basename, dirname, join } from 'node:path'; + +import { + colors, + consola, + ensureFile, + findMonorepoRoot, + generatorContentHash, + getPackages, + outputJSON, + readJSON, + UNICODE, +} from '@vben/node-utils'; + +import { publint } from 'publint'; +import { formatMessage } from 'publint/utils'; + +const CACHE_FILE = join( + 'node_modules', + '.cache', + 'publint', + '.pkglintcache.json', +); + +interface PubLintCommandOptions { + /** + * Only errors are checked, no program exit is performed + */ + check?: boolean; +} + +/** + * Get files that require lint + * @param files + */ +async function getLintFiles(files: string[] = []) { + const lintFiles: string[] = []; + + if (files?.length > 0) { + return files.filter((file) => basename(file) === 'package.json'); + } + + const { packages } = await getPackages(); + + for (const { dir } of packages) { + lintFiles.push(join(dir, 'package.json')); + } + return lintFiles; +} + +function getCacheFile() { + const root = findMonorepoRoot(); + return join(root, CACHE_FILE); +} + +async function readCache(cacheFile: string) { + try { + await ensureFile(cacheFile); + return await readJSON(cacheFile); + } catch { + return {}; + } +} + +async function runPublint(files: string[], { check }: PubLintCommandOptions) { + const lintFiles = await getLintFiles(files); + const cacheFile = getCacheFile(); + + const cacheData = await readCache(cacheFile); + const cache: Record = cacheData; + + const results = await Promise.all( + lintFiles.map(async (file) => { + try { + const pkgJson = await readJSON(file); + + if (pkgJson.private) { + return null; + } + + Reflect.deleteProperty(pkgJson, 'dependencies'); + Reflect.deleteProperty(pkgJson, 'devDependencies'); + Reflect.deleteProperty(pkgJson, 'peerDependencies'); + const content = JSON.stringify(pkgJson); + const hash = generatorContentHash(content); + + const publintResult: Result = + cache?.[file]?.hash === hash + ? (cache?.[file]?.result ?? []) + : await publint({ + level: 'suggestion', + pkgDir: dirname(file), + strict: true, + }); + + cache[file] = { + hash, + result: publintResult, + }; + + return { pkgJson, pkgPath: file, publintResult }; + } catch { + return null; + } + }), + ); + + await outputJSON(cacheFile, cache); + printResult(results, check); +} + +function printResult( + results: Array<{ + pkgJson: Record; + pkgPath: string; + publintResult: Result; + } | null>, + check?: boolean, +) { + let errorCount = 0; + let warningCount = 0; + let suggestionsCount = 0; + + for (const result of results) { + if (!result) { + continue; + } + const { pkgJson, pkgPath, publintResult } = result; + const messages = publintResult?.messages ?? []; + if (messages?.length < 1) { + continue; + } + + consola.log(''); + consola.log(pkgPath); + for (const message of messages) { + switch (message.type) { + case 'error': { + errorCount++; + + break; + } + case 'suggestion': { + suggestionsCount++; + break; + } + case 'warning': { + warningCount++; + + break; + } + // No default + } + const ruleUrl = `https://publint.dev/rules#${message.code.toLocaleLowerCase()}`; + consola.log( + ` ${formatMessage(message, pkgJson)}${colors.dim(` ${ruleUrl}`)}`, + ); + } + } + + const totalCount = warningCount + errorCount + suggestionsCount; + if (totalCount > 0) { + consola.error( + colors.red( + `${UNICODE.FAILURE} ${totalCount} problem (${errorCount} errors, ${warningCount} warnings, ${suggestionsCount} suggestions)`, + ), + ); + !check && process.exit(1); + } else { + consola.log(colors.green(`${UNICODE.SUCCESS} No problem`)); + } +} + +function definePubLintCommand(cac: CAC) { + cac + .command('publint [...files]') + .usage('Check if the monorepo package conforms to the publint standard.') + .option('--check', 'Only errors are checked, no program exit is performed.') + .action(runPublint); +} + +export { definePubLintCommand }; diff --git a/apps/vben5/scripts/vsh/tsconfig.json b/apps/vben5/scripts/vsh/tsconfig.json new file mode 100644 index 000000000..b2ec3b61e --- /dev/null +++ b/apps/vben5/scripts/vsh/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/node.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/apps/vben5/stylelint.config.mjs b/apps/vben5/stylelint.config.mjs new file mode 100644 index 000000000..e380674fd --- /dev/null +++ b/apps/vben5/stylelint.config.mjs @@ -0,0 +1,4 @@ +export default { + extends: ['@vben/stylelint-config'], + root: true, +}; diff --git a/apps/vben5/tea.yaml b/apps/vben5/tea.yaml new file mode 100644 index 000000000..6e56d6f6c --- /dev/null +++ b/apps/vben5/tea.yaml @@ -0,0 +1,6 @@ +# https://tea.xyz/what-is-this-file +--- +version: 1.0.0 +codeOwners: + - '0xB33cc732DFc15Cd39eF50Fb165c876E24417E48f' +quorum: 1 diff --git a/apps/vben5/turbo.json b/apps/vben5/turbo.json new file mode 100644 index 000000000..3443e27cf --- /dev/null +++ b/apps/vben5/turbo.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": [ + "pnpm-lock.yaml", + "**/.env.*local", + "**/tsconfig*.json", + "internal/node-utils/*.json", + "internal/node-utils/src/**/*.ts", + "internal/tailwind-config/src/**/*.ts", + "internal/vite-config/*.json", + "internal/vite-config/src/**/*.ts", + "scripts/*/src/**/*.ts", + "scripts/*/src/**/*.json" + ], + "globalEnv": ["NODE_ENV"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": [ + "dist/**", + "dist.zip", + ".vitepress/dist.zip", + ".vitepress/dist/**" + ] + }, + "preview": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "build:analyze": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "@vben/backend-mock#build": { + "dependsOn": ["^build"], + "outputs": [".nitro/**", ".output/**"] + }, + "test:e2e": {}, + "dev": { + "dependsOn": [], + "outputs": [], + "cache": false, + "persistent": true + }, + "typecheck": { + "outputs": [] + } + } +} diff --git a/apps/vben5/vben-admin.code-workspace b/apps/vben5/vben-admin.code-workspace new file mode 100644 index 000000000..9b7519c1a --- /dev/null +++ b/apps/vben5/vben-admin.code-workspace @@ -0,0 +1,196 @@ +{ + "folders": [ + { + "name": "@vben/backend-mock", + "path": "apps/backend-mock", + }, + { + "name": "@vben/web-antd", + "path": "apps/web-antd", + }, + { + "name": "@vben/web-ele", + "path": "apps/web-ele", + }, + { + "name": "@vben/web-naive", + "path": "apps/web-naive", + }, + { + "name": "@vben/docs", + "path": "docs", + }, + { + "name": "@vben/commitlint-config", + "path": "internal/lint-configs/commitlint-config", + }, + { + "name": "@vben/eslint-config", + "path": "internal/lint-configs/eslint-config", + }, + { + "name": "@vben/prettier-config", + "path": "internal/lint-configs/prettier-config", + }, + { + "name": "@vben/stylelint-config", + "path": "internal/lint-configs/stylelint-config", + }, + { + "name": "@vben/node-utils", + "path": "internal/node-utils", + }, + { + "name": "@vben/tailwind-config", + "path": "internal/tailwind-config", + }, + { + "name": "@vben/tsconfig", + "path": "internal/tsconfig", + }, + { + "name": "@vben/vite-config", + "path": "internal/vite-config", + }, + { + "name": "@vben-core/design", + "path": "packages/@core/base/design", + }, + { + "name": "@vben-core/icons", + "path": "packages/@core/base/icons", + }, + { + "name": "@vben-core/shared", + "path": "packages/@core/base/shared", + }, + { + "name": "@vben-core/typings", + "path": "packages/@core/base/typings", + }, + { + "name": "@vben-core/composables", + "path": "packages/@core/composables", + }, + { + "name": "@vben-core/preferences", + "path": "packages/@core/preferences", + }, + { + "name": "@vben-core/form-ui", + "path": "packages/@core/ui-kit/form-ui", + }, + { + "name": "@vben-core/layout-ui", + "path": "packages/@core/ui-kit/layout-ui", + }, + { + "name": "@vben-core/menu-ui", + "path": "packages/@core/ui-kit/menu-ui", + }, + { + "name": "@vben-core/popup-ui", + "path": "packages/@core/ui-kit/popup-ui", + }, + { + "name": "@vben-core/shadcn-ui", + "path": "packages/@core/ui-kit/shadcn-ui", + }, + { + "name": "@vben-core/tabs-ui", + "path": "packages/@core/ui-kit/tabs-ui", + }, + { + "name": "@vben/constants", + "path": "packages/constants", + }, + { + "name": "@vben/access", + "path": "packages/effects/access", + }, + { + "name": "@vben/common-ui", + "path": "packages/effects/common-ui", + }, + { + "name": "@vben/hooks", + "path": "packages/effects/hooks", + }, + { + "name": "@vben/layouts", + "path": "packages/effects/layouts", + }, + { + "name": "@vben/plugins", + "path": "packages/effects/plugins", + }, + { + "name": "@vben/request", + "path": "packages/effects/request", + }, + { + "name": "@vben/icons", + "path": "packages/icons", + }, + { + "name": "@vben/locales", + "path": "packages/locales", + }, + { + "name": "@vben/preferences", + "path": "packages/preferences", + }, + { + "name": "@vben/stores", + "path": "packages/stores", + }, + { + "name": "@vben/styles", + "path": "packages/styles", + }, + { + "name": "@vben/types", + "path": "packages/types", + }, + { + "name": "@vben/utils", + "path": "packages/utils", + }, + { + "name": "@vben/playground", + "path": "playground", + }, + { + "name": "@vben/turbo-run", + "path": "scripts/turbo-run", + }, + { + "name": "@vben/vsh", + "path": "scripts/vsh", + }, + { + "name": "@vben/app", + "path": "apps/app-antd", + }, + { + "name": "@abp/core", + "path": "packages/@abp/core", + }, + { + "name": "@abp/request", + "path": "packages/@abp/request", + }, + { + "name": "@abp/ui", + "path": "packages/@abp/ui", + }, + { + "name": "@abp/identity", + "path": "packages/@abp/identity", + }, + { + "name": "@abp/account", + "path": "packages/@abp/account", + }, + ], +} diff --git a/apps/vben5/vitest.config.ts b/apps/vben5/vitest.config.ts new file mode 100644 index 000000000..a10b5fa3a --- /dev/null +++ b/apps/vben5/vitest.config.ts @@ -0,0 +1,11 @@ +import Vue from '@vitejs/plugin-vue'; +import VueJsx from '@vitejs/plugin-vue-jsx'; +import { configDefaults, defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Vue(), VueJsx()], + test: { + environment: 'happy-dom', + exclude: [...configDefaults.exclude, '**/e2e/**'], + }, +}); diff --git a/apps/vben5/vitest.workspace.ts b/apps/vben5/vitest.workspace.ts new file mode 100644 index 000000000..f00d6f686 --- /dev/null +++ b/apps/vben5/vitest.workspace.ts @@ -0,0 +1,3 @@ +import { defineWorkspace } from 'vitest/config'; + +export default defineWorkspace(['vitest.config.ts']); From 06fd819a4872250123c05d6fbbfbb7a4a36b1e9c Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 5 Dec 2024 18:20:35 +0800 Subject: [PATCH 11/81] feat(vben5-identity): increased organizational implementation --- .../apps/app-antd/.vite/deps/_metadata.json | 8 + .../apps/app-antd/.vite/deps/package.json | 3 + .../app-antd/src/locales/langs/en-US/abp.json | 3 +- .../app-antd/src/locales/langs/zh-CN/abp.json | 3 +- .../app-antd/src/router/routes/modules/abp.ts | 14 +- .../src/views/identity/claim-types/index.vue | 4 + .../identity/organization-units/index.vue | 15 ++ .../views/identity/{role => roles}/index.vue | 4 + .../views/identity/security-logs/index.vue | 4 + .../views/identity/{user => users}/index.vue | 4 + .../packages/@abp/core/src/utils/index.ts | 1 + .../packages/@abp/core/src/utils/tree.ts | 37 +++ .../identity/src/api/organization-units.ts | 227 ++++++++++++++++++ .../identity/src/api/{role.ts => roles.ts} | 16 +- .../identity/src/api/{user.ts => users.ts} | 16 +- .../components/claim-types/ClaimTypeModal.vue | 4 +- .../components/claim-types/ClaimTypeTable.vue | 2 +- .../@abp/identity/src/components/index.ts | 5 +- .../OrganizationUnitModal.vue | 11 + .../OrganizationUnitPage.vue | 29 +++ .../OrganizationUnitRoleTable.vue | 181 ++++++++++++++ .../OrganizationUnitTable.vue | 36 +++ .../OrganizationUnitTree.vue | 203 ++++++++++++++++ .../OrganizationUnitUserTable.vue | 183 ++++++++++++++ .../organization-units/SelectMemberModal.vue | 134 +++++++++++ .../organization-units/SelectRoleModal.vue | 127 ++++++++++ .../components/{role => roles}/RoleModal.vue | 4 +- .../src/components/roles/RoleSelectModal.vue | 7 + .../components/{role => roles}/RoleTable.vue | 6 +- .../security-logs/SecurityLogTable.vue | 3 +- .../components/{user => users}/UserModal.vue | 6 +- .../components/{user => users}/UserTable.vue | 4 +- .../packages/@abp/identity/src/types/index.ts | 5 +- .../identity/src/types/organization-units.ts | 96 ++++++++ .../identity/src/types/{role.ts => roles.ts} | 0 .../identity/src/types/{user.ts => users.ts} | 0 36 files changed, 1380 insertions(+), 25 deletions(-) create mode 100644 apps/vben5/apps/app-antd/.vite/deps/_metadata.json create mode 100644 apps/vben5/apps/app-antd/.vite/deps/package.json create mode 100644 apps/vben5/apps/app-antd/src/views/identity/organization-units/index.vue rename apps/vben5/apps/app-antd/src/views/identity/{role => roles}/index.vue (79%) rename apps/vben5/apps/app-antd/src/views/identity/{user => users}/index.vue (79%) create mode 100644 apps/vben5/packages/@abp/core/src/utils/tree.ts create mode 100644 apps/vben5/packages/@abp/identity/src/api/organization-units.ts rename apps/vben5/packages/@abp/identity/src/api/{role.ts => roles.ts} (82%) rename apps/vben5/packages/@abp/identity/src/api/{user.ts => users.ts} (82%) create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitModal.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitPage.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitRoleTable.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitTable.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitTree.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/OrganizationUnitUserTable.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/SelectMemberModal.vue create mode 100644 apps/vben5/packages/@abp/identity/src/components/organization-units/SelectRoleModal.vue rename apps/vben5/packages/@abp/identity/src/components/{role => roles}/RoleModal.vue (95%) create mode 100644 apps/vben5/packages/@abp/identity/src/components/roles/RoleSelectModal.vue rename apps/vben5/packages/@abp/identity/src/components/{role => roles}/RoleTable.vue (96%) rename apps/vben5/packages/@abp/identity/src/components/{user => users}/UserModal.vue (96%) rename apps/vben5/packages/@abp/identity/src/components/{user => users}/UserTable.vue (97%) create mode 100644 apps/vben5/packages/@abp/identity/src/types/organization-units.ts rename apps/vben5/packages/@abp/identity/src/types/{role.ts => roles.ts} (100%) rename apps/vben5/packages/@abp/identity/src/types/{user.ts => users.ts} (100%) diff --git a/apps/vben5/apps/app-antd/.vite/deps/_metadata.json b/apps/vben5/apps/app-antd/.vite/deps/_metadata.json new file mode 100644 index 000000000..5f0b346a8 --- /dev/null +++ b/apps/vben5/apps/app-antd/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "62932afa", + "configHash": "fdef0d8d", + "lockfileHash": "5ba0e736", + "browserHash": "cfcea6c4", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/apps/vben5/apps/app-antd/.vite/deps/package.json b/apps/vben5/apps/app-antd/.vite/deps/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/apps/vben5/apps/app-antd/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json index a7a462088..199e139ab 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json @@ -7,7 +7,8 @@ "user": "User", "role": "Role", "claimTypes": "Claim Types", - "securityLogs": "Security Logs" + "securityLogs": "Security Logs", + "organizationUnits": "Organization Units" } } } diff --git a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json index ef6be38c5..fd9d57776 100644 --- a/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json +++ b/apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json @@ -7,7 +7,8 @@ "user": "用户", "role": "角色", "claimTypes": "身份标识", - "securityLogs": "安全日志" + "securityLogs": "安全日志", + "organizationUnits": "组织机构" } } } diff --git a/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts b/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts index acae5784f..6fd11549e 100644 --- a/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts +++ b/apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts @@ -32,7 +32,7 @@ const routes: RouteRecordRaw[] = [ path: '/manage/identity', children: [ { - component: () => import('#/views/identity/user/index.vue'), + component: () => import('#/views/identity/users/index.vue'), name: 'Users', path: '/manage/identity/users', meta: { @@ -41,7 +41,7 @@ const routes: RouteRecordRaw[] = [ }, }, { - component: () => import('#/views/identity/role/index.vue'), + component: () => import('#/views/identity/roles/index.vue'), name: 'Roles', path: '/manage/identity/roles', meta: { @@ -69,6 +69,16 @@ const routes: RouteRecordRaw[] = [ icon: 'carbon:security', }, }, + { + component: () => + import('#/views/identity/organization-units/index.vue'), + name: 'OrganizationUnits', + path: '/manage/identity/organization-units', + meta: { + title: $t('abp.manage.identity.organizationUnits'), + icon: 'clarity:organization-line', + }, + }, ], }, ], diff --git a/apps/vben5/apps/app-antd/src/views/identity/claim-types/index.vue b/apps/vben5/apps/app-antd/src/views/identity/claim-types/index.vue index 7d36dd612..4dfebe2c3 100644 --- a/apps/vben5/apps/app-antd/src/views/identity/claim-types/index.vue +++ b/apps/vben5/apps/app-antd/src/views/identity/claim-types/index.vue @@ -2,6 +2,10 @@ import { Page } from '@vben/common-ui'; import { ClaimTypeTable } from '@abp/identity'; + +defineOptions({ + name: 'IdentityClaimTypes', +});