diff --git a/Common.props b/Common.props index e01f9df5af6edbc4a978882f2fbc94244a8fee93..a518bd035f5d58f90bca8e9aa3ed0d03d157143d 100644 --- a/Common.props +++ b/Common.props @@ -4,7 +4,7 @@ net8.0 enable enable - NU1803;NU1507;1701;1702;1591;8002 + NU1803;NU1507;1701;1702;1591;8002;CS1573; diff --git a/DaprTool.Solution.sln b/DaprTool.Solution.sln index 116fa3fd8439df1620a268c18f4392d6b1dcc125..a9c01186e54b8f640944b14839a4f5d4feb6c096 100644 --- a/DaprTool.Solution.sln +++ b/DaprTool.Solution.sln @@ -115,9 +115,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleInterfaces", "samples EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ordering.Domain.Interfaces", "src\Services\Ordering\Domain\Ordering.Domain.Interfaces\Ordering.Domain.Interfaces.csproj", "{88C29601-0AA3-4751-9AF2-65F93743AA5D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dependency", "src\BuildingBlocks\Dependency\Dependency.csproj", "{631B0704-ED1C-444A-95AB-E52952756F43}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dependency", "src\BuildingBlocks\Dependency\Dependency.csproj", "{631B0704-ED1C-444A-95AB-E52952756F43}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation", "src\BuildingBlocks\Validation\Validation.csproj", "{7449D8D4-0F2A-4D35-BC2F-0DEF40833C91}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Validation", "src\BuildingBlocks\Validation\Validation.csproj", "{7449D8D4-0F2A-4D35-BC2F-0DEF40833C91}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAdmin.Shared", "src\Web\WebAdmin.Shared\WebAdmin.Shared.csproj", "{9FCF0D70-CF60-4C0D-8667-42DB50BC6EE2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAdmin.Client", "src\Web\WebAdmin.Client\WebAdmin.Client.csproj", "{306E933B-914F-4935-A924-DA756F766753}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAdmin", "src\Web\WebAdmin\WebAdmin.csproj", "{EA794699-4DD1-4622-A80F-DB138AB53D86}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -209,6 +215,18 @@ Global {7449D8D4-0F2A-4D35-BC2F-0DEF40833C91}.Debug|Any CPU.Build.0 = Debug|Any CPU {7449D8D4-0F2A-4D35-BC2F-0DEF40833C91}.Release|Any CPU.ActiveCfg = Release|Any CPU {7449D8D4-0F2A-4D35-BC2F-0DEF40833C91}.Release|Any CPU.Build.0 = Release|Any CPU + {9FCF0D70-CF60-4C0D-8667-42DB50BC6EE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FCF0D70-CF60-4C0D-8667-42DB50BC6EE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FCF0D70-CF60-4C0D-8667-42DB50BC6EE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FCF0D70-CF60-4C0D-8667-42DB50BC6EE2}.Release|Any CPU.Build.0 = Release|Any CPU + {306E933B-914F-4935-A924-DA756F766753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {306E933B-914F-4935-A924-DA756F766753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {306E933B-914F-4935-A924-DA756F766753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {306E933B-914F-4935-A924-DA756F766753}.Release|Any CPU.Build.0 = Release|Any CPU + {EA794699-4DD1-4622-A80F-DB138AB53D86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA794699-4DD1-4622-A80F-DB138AB53D86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA794699-4DD1-4622-A80F-DB138AB53D86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA794699-4DD1-4622-A80F-DB138AB53D86}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -257,6 +275,9 @@ Global {88C29601-0AA3-4751-9AF2-65F93743AA5D} = {7910C4F6-E2F6-4309-BC0F-45285E1124D7} {631B0704-ED1C-444A-95AB-E52952756F43} = {121D78F3-3F0F-4F00-B2C9-3C797653C951} {7449D8D4-0F2A-4D35-BC2F-0DEF40833C91} = {121D78F3-3F0F-4F00-B2C9-3C797653C951} + {9FCF0D70-CF60-4C0D-8667-42DB50BC6EE2} = {6720D68A-1AF5-48BA-8CB5-349E70E151B0} + {306E933B-914F-4935-A924-DA756F766753} = {6720D68A-1AF5-48BA-8CB5-349E70E151B0} + {EA794699-4DD1-4622-A80F-DB138AB53D86} = {6720D68A-1AF5-48BA-8CB5-349E70E151B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CE377903-BDA1-4347-BEC2-62ED2F807EE3} diff --git a/DaprTool.Solution.sln.DotSettings b/DaprTool.Solution.sln.DotSettings index 5447d4e34a9777517bd4c7b728d89a6e4b191b6d..8541c3c8bf26e22044b38f734160cff8711af09f 100644 --- a/DaprTool.Solution.sln.DotSettings +++ b/DaprTool.Solution.sln.DotSettings @@ -1,15 +1,24 @@  True True + True True + True True True + True + True + True + True True + True True True True True True + True True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index fec2624ccb29ca6c80a552329b70064929deffab..b0e6f36e9c2522c2c8244afbe53780e0725f6483 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ 8.0.0 8.0.0 + 8.0.4 1.13.0 @@ -15,30 +16,53 @@ - - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + - + + + diff --git a/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/AppDataConnectionExtensions.cs b/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/AppDataConnectionExtensions.cs index 4fa58784c68a1440fdf0dd29a4bb9cf65ffddb93..f98ecb75a077c594e3d2d0d1190a1bb35e41922a 100644 --- a/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/AppDataConnectionExtensions.cs +++ b/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/AppDataConnectionExtensions.cs @@ -34,7 +34,7 @@ public static class AppDataConnectionExtensions /// /// /// - public static void AddAppDataConnection(this IServiceCollection services, string connectionString) + public static void AddOrderAppDataConnection(this IServiceCollection services, string connectionString) { if (string.IsNullOrEmpty(connectionString)) throw new ArgumentException("connectionString can not be null", nameof(connectionString)); @@ -53,8 +53,8 @@ public static class AppDataConnectionExtensions /// /// /// - public static void AddAppDataConnection(this IServiceCollection services, IConfiguration configuration) + public static void AddOrderAppDataConnection(this IServiceCollection services, IConfiguration configuration) { - services.AddAppDataConnection(configuration.GetConnectionString(DaprConstants.Ordering.AppId)!); + services.AddOrderAppDataConnection(configuration.GetConnectionString(DaprConstants.Ordering.AppId)!); } } \ No newline at end of file diff --git a/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/Ordering.Infrastructure.Repository.xml b/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/Ordering.Infrastructure.Repository.xml index 0d4934beee7db47a1c806867bd85c234449c5225..05928c5b20e9e7eecb3a8750e2252c1c0f36aeb5 100644 --- a/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/Ordering.Infrastructure.Repository.xml +++ b/src/Services/Ordering/Infrastructure/Ordering.Infrastructure.Repository/Ordering.Infrastructure.Repository.xml @@ -104,7 +104,7 @@ - + 注册 DataConnection @@ -113,7 +113,7 @@ - + 注册 DataConnection diff --git a/src/Services/Ordering/Presentation/Ordering.Api/ProgramExtensions.cs b/src/Services/Ordering/Presentation/Ordering.Api/ProgramExtensions.cs index a232a7235b8babe8ebe448d63f9feef94a670b7f..d2ae6f6405bba590f49d8af9a55067068067246a 100644 --- a/src/Services/Ordering/Presentation/Ordering.Api/ProgramExtensions.cs +++ b/src/Services/Ordering/Presentation/Ordering.Api/ProgramExtensions.cs @@ -17,6 +17,6 @@ public static class ProgramExtensions // 注册 应用配置 builder.Services.Configure(builder.Configuration); // 注册 业务数据库 - builder.Services.AddAppDataConnection(builder.Configuration); + builder.Services.AddOrderAppDataConnection(builder.Configuration); } } \ No newline at end of file diff --git a/src/Web/WebAdmin.Client/Layout/NavMenu.razor b/src/Web/WebAdmin.Client/Layout/NavMenu.razor new file mode 100644 index 0000000000000000000000000000000000000000..8f3dffe4b44ca00be6180c9fa3ebbf31f5059025 --- /dev/null +++ b/src/Web/WebAdmin.Client/Layout/NavMenu.razor @@ -0,0 +1,18 @@ +@rendermode InteractiveAuto + + + +@code { + private bool expanded = true; + +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Client/Pages/Counter.razor b/src/Web/WebAdmin.Client/Pages/Counter.razor new file mode 100644 index 0000000000000000000000000000000000000000..36b79802609ffd8f7dca48a7930e8584a38c8ee5 --- /dev/null +++ b/src/Web/WebAdmin.Client/Pages/Counter.razor @@ -0,0 +1,21 @@ +@page "/counter" +@rendermode InteractiveAuto + +Counter + +

Counter

+ +
+ Current count: @currentCount +
+ +Click me + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/src/Web/WebAdmin.Client/Program.cs b/src/Web/WebAdmin.Client/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..519269f21bb897ab2a7443799e30744c95b67c9c --- /dev/null +++ b/src/Web/WebAdmin.Client/Program.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +await builder.Build().RunAsync(); diff --git a/src/Web/WebAdmin.Client/WebAdmin.Client.csproj b/src/Web/WebAdmin.Client/WebAdmin.Client.csproj new file mode 100644 index 0000000000000000000000000000000000000000..1ad68c0ecbe5b421282629bdce394b16efb0ba9c --- /dev/null +++ b/src/Web/WebAdmin.Client/WebAdmin.Client.csproj @@ -0,0 +1,17 @@ + + + + true + Default + + + + + + + + + + + + diff --git a/src/Web/WebAdmin.Client/WebAdmin.Client.xml b/src/Web/WebAdmin.Client/WebAdmin.Client.xml new file mode 100644 index 0000000000000000000000000000000000000000..46229ea4db2315f5cdf1e82e52724e289b4d98e5 --- /dev/null +++ b/src/Web/WebAdmin.Client/WebAdmin.Client.xml @@ -0,0 +1,8 @@ + + + + WebAdmin.Client + + + + diff --git a/src/Web/WebAdmin.Client/_Imports.razor b/src/Web/WebAdmin.Client/_Imports.razor new file mode 100644 index 0000000000000000000000000000000000000000..8f2ecdb055f90741f3ce877ee9ed6d13ba8e5d0b --- /dev/null +++ b/src/Web/WebAdmin.Client/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.JSInterop +@using WebAdmin.Client diff --git a/src/Web/WebAdmin.Client/wwwroot/appsettings.Development.json b/src/Web/WebAdmin.Client/wwwroot/appsettings.Development.json new file mode 100644 index 0000000000000000000000000000000000000000..0c208ae9181e5e5717e47ec1bd59368aebc6066e --- /dev/null +++ b/src/Web/WebAdmin.Client/wwwroot/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Web/WebAdmin.Client/wwwroot/appsettings.json b/src/Web/WebAdmin.Client/wwwroot/appsettings.json new file mode 100644 index 0000000000000000000000000000000000000000..0c208ae9181e5e5717e47ec1bd59368aebc6066e --- /dev/null +++ b/src/Web/WebAdmin.Client/wwwroot/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Web/WebAdmin.Shared/Components/CultureSelector.razor b/src/Web/WebAdmin.Shared/Components/CultureSelector.razor new file mode 100644 index 0000000000000000000000000000000000000000..ae7c62cd11843dee0c2e424cedae06023662ddc3 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/CultureSelector.razor @@ -0,0 +1,32 @@ +
+ + + + @foreach (var item in CultureOptions) + { + + @item.Text + + } + +
+ + +@code { + private bool _showMenu = false; + private string _status = ""; + + private void OnMenuChange(MenuChangeEventArgs args) + { + if (args is not null && args.Value is not null) + _status = $"Item \"{args.Value}\" clicked"; + Console.WriteLine(_status); + } + +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/CultureSelector.razor.cs b/src/Web/WebAdmin.Shared/Components/CultureSelector.razor.cs new file mode 100644 index 0000000000000000000000000000000000000000..54544340e8f901fd4dbf4fae85a8ea6388cecd9a --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/CultureSelector.razor.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.JSInterop; +using WebAdmin.Shared.Configurations; + +namespace WebAdmin.Shared.Components; + +public partial class CultureSelector +{ + [Inject] protected IJSRuntime JsRuntime { get; set; } = null!; + + [Inject] protected NavigationManager NavigationManager { get; set; } = null!; + + [Inject] protected CultureConfiguration CultureConfiguration { get; set; } = null!; + + private string _culture = string.Empty; + + public string Culture + { + get => _culture; + set + { + _culture = value; + ChangeCulture(value); + } + } + + private static readonly List> CultureOptions = + [ + new Option { Value = "zh-Hans", Text = "中文(简体)" }, + new Option { Value = "en", Text = "English" } + ]; + + + protected override Task OnInitializedAsync() + { + _culture = CultureInfo.CurrentCulture.Name; + + return base.OnInitializedAsync(); + } + + public void ChangeCulture(string? _) + { + var redirect = new Uri(NavigationManager.Uri).GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, + UriFormat.UriEscaped); + + var query = $"?culture={Uri.EscapeDataString(_culture)}&redirectUri={redirect}"; + + NavigationManager.NavigateTo("Culture/SetCulture" + query, true); + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/CultureSelector.razor.css b/src/Web/WebAdmin.Shared/Components/CultureSelector.razor.css new file mode 100644 index 0000000000000000000000000000000000000000..4ff7011d2fe96da8c74a531688e449ff81abf7f2 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/CultureSelector.razor.css @@ -0,0 +1,3 @@ +.language { + +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteLinks.razor b/src/Web/WebAdmin.Shared/Components/SiteLinks.razor new file mode 100644 index 0000000000000000000000000000000000000000..fdc618459c9477eb6d0c75ae0da0177683ca40c5 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteLinks.razor @@ -0,0 +1,22 @@ + + + +@code { + + [Inject] protected IJSRuntime JsRuntime { get; set; } = null!; + + private async Task NavigateToGitHub() + { + await JsRuntime.InvokeVoidAsync("open", "http://github.com/iamshen", "_blank"); + } + +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteLinks.razor.css b/src/Web/WebAdmin.Shared/Components/SiteLinks.razor.css new file mode 100644 index 0000000000000000000000000000000000000000..6f563d698d3cb6f7608e6e78a4acc403206306db --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteLinks.razor.css @@ -0,0 +1,4 @@ +.links { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteSearch.razor b/src/Web/WebAdmin.Shared/Components/SiteSearch.razor new file mode 100644 index 0000000000000000000000000000000000000000..6188b8ca37e1109b5629697f03d8eaaaad42f77d --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteSearch.razor @@ -0,0 +1,22 @@ +@inject IStringLocalizer L + + \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteSearch.razor.cs b/src/Web/WebAdmin.Shared/Components/SiteSearch.razor.cs new file mode 100644 index 0000000000000000000000000000000000000000..54c785b0bcb09ae5bb3f6f469684d672350b761b --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteSearch.razor.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components; +using System.Diagnostics; +using WebAdmin.Shared.Infrastructure; +using WebAdmin.Shared.Records; + +namespace WebAdmin.Shared.Components; + +public partial class SiteSearch +{ + [Inject] + protected NavProvider NavProvider { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + private string? _searchTerm = ""; + private IEnumerable? _selectedOptions = []; + private IReadOnlyList FlattenedMenuItems => NavProvider.FlattenedMenuItems + .Select(x => new NavLink(href: x.Href, icon: x.Icon, match: x.Match, name: L[x.Name])) + .ToList() + .AsReadOnly(); + + private void HandleSearchInput(OptionsSearchEventArgs e) + { + var searchTerm = e.Text; + + if (string.IsNullOrWhiteSpace(searchTerm)) + { + e.Items = FlattenedMenuItems; + } + else + { + e.Items = FlattenedMenuItems + .Where(x => x.Href != null) + .Where(x => x.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)); + } + } + + private void HandleSearchClicked() + { + _searchTerm = null; + var targetHref = _selectedOptions?.SingleOrDefault()?.Href; + _selectedOptions = []; + InvokeAsync(StateHasChanged); + + // Ignore clearing the search bar + if (targetHref is null) + { + return; + } + + NavigationManager.NavigateTo(targetHref ?? throw new UnreachableException("无效项")); + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteSettings.razor b/src/Web/WebAdmin.Shared/Components/SiteSettings.razor new file mode 100644 index 0000000000000000000000000000000000000000..7330777bb89c1648782c73c9b12ef8082f857aeb --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteSettings.razor @@ -0,0 +1,7 @@ +@inject IDialogService DialogService + +
+ + + +
\ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteSettings.razor.cs b/src/Web/WebAdmin.Shared/Components/SiteSettings.razor.cs new file mode 100644 index 0000000000000000000000000000000000000000..fdbf713e416fe7192800fbb6ddea3239a40afbbd --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteSettings.razor.cs @@ -0,0 +1,24 @@ +using Microsoft.FluentUI.AspNetCore.Components; + +namespace WebAdmin.Shared.Components; + +public partial class SiteSettings +{ + private IDialogReference? _dialog; + + private async Task OpenSiteSettingsAsync() + { + Console.WriteLine($"Open site settings"); + _dialog = await DialogService.ShowPanelAsync(new DialogParameters() + { + ShowTitle = true, + Title = "网站设置", + Alignment = HorizontalAlignment.Right, + PrimaryAction = "OK", + SecondaryAction = null, + ShowDismiss = true + }); + + DialogResult result = await _dialog.Result; + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/SiteSettingsPanel.razor b/src/Web/WebAdmin.Shared/Components/SiteSettingsPanel.razor new file mode 100644 index 0000000000000000000000000000000000000000..fb330df4c0fd207ed4a588d401770c709cf27179 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteSettingsPanel.razor @@ -0,0 +1,79 @@ +@implements IDialogContentComponent + +
+ + + + + + + + + + @OfficeColorNameMapper.Map(context) + + + + + + + 这些值(方向值除外)将被持久保存在 LocalStorage 中,并将在下次访问时恢复。 +

+ "重置设置" 按钮 重置系统主题和颜色。 +
+ "随机设置" 按钮 随机系统主题和随机颜色。 + +
+ + +
重置网站设置
+ +

+ 该网站会在浏览器缓存和本地存储中存储主题和颜色设置,以及下载的样本、表情符号和图标。 +

+

+ 您可以在浏览器的开发工具中检查缓存和存储的内容。如果您使用的是 Edge 或 Chrome 浏览器,可以进入 "应用程序 "选项卡,然后点击 "缓存存储 "或 "本地存储 "部分。 +
+ 在火狐浏览器中,您可以进入 "存储 "选项卡,然后单击 "缓存存储 "或 "本地存储 "部分。 +

+ +

+ 如果你觉得没有看到最新和最好的样本、表情符号或图标,或者你想清除存储的主题和颜色,请单击下面的按钮清除缓存并删除本地存储。 +

+

+ 别担心, 这将重置网站的存储数据。它不会清除浏览器中其他网站的任何缓存! +

+ +
+ + + 随机主题 + 重置设置 + + + +

+ @_status +

+
+
diff --git a/src/Web/WebAdmin.Shared/Components/SiteSettingsPanel.razor.cs b/src/Web/WebAdmin.Shared/Components/SiteSettingsPanel.razor.cs new file mode 100644 index 0000000000000000000000000000000000000000..e9f25a85dd9e007acfdc8ba4472302a38be293c5 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/SiteSettingsPanel.razor.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Logging; +using Microsoft.FluentUI.AspNetCore.Components.Extensions; +using Microsoft.FluentUI.AspNetCore.Components; +using WebAdmin.Shared.Infrastructure; + +namespace WebAdmin.Shared.Components; + +public partial class SiteSettingsPanel +{ + private string? _status; + private bool _popVisible; + private bool _ltr = true; + private FluentDesignTheme? _theme; + + [Inject] + public required ILogger Logger { get; set; } + + [Inject] + public required CacheStorageAccessor CacheStorageAccessor { get; set; } + + [Inject] + public required GlobalState GlobalState { get; set; } + + public DesignThemeModes Mode { get; set; } + + public OfficeColor? OfficeColor { get; set; } + + public LocalizationDirection? Direction { get; set; } + + private static IEnumerable AllModes => Enum.GetValues(); + + private static IEnumerable AllOfficeColors + { + get + { + return Enum.GetValues().Select(i => (OfficeColor?)i); + } + } + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + Direction = GlobalState.Dir; + _ltr = !Direction.HasValue || Direction.Value == LocalizationDirection.LeftToRight; + } + } + + protected void HandleDirectionChanged(bool isLeftToRight) + { + + _ltr = isLeftToRight; + Direction = isLeftToRight ? LocalizationDirection.LeftToRight : LocalizationDirection.RightToLeft; + } + + private async Task ResetSiteAsync() + { + var msg = "重置网站设置并清除缓存!"; + + await CacheStorageAccessor.RemoveAllAsync(); + _theme?.ClearLocalStorageAsync(); + + Logger.LogInformation(msg); + _status = msg; + + OfficeColor = Microsoft.FluentUI.AspNetCore.Components.OfficeColor.Default; + Mode = DesignThemeModes.System; + } + + private async Task RandomSiteAsync() + { + var msg = "随机网站主题颜色和模式!"; + + await CacheStorageAccessor.RemoveAllAsync(); + _theme?.ClearLocalStorageAsync(); + + Logger.LogInformation(msg); + _status = msg; + + OfficeColor = OfficeColorUtilities.GetRandom(); + var themeModes = Enum.GetValues(); + Mode = themeModes.ElementAt(Random.Shared.Next(themeModes.Length)); + } + + private static string? GetCustomColor(OfficeColor? color) + { + return color switch + { + null => OfficeColorUtilities.GetRandom(true).ToAttributeValue(), + Microsoft.FluentUI.AspNetCore.Components.OfficeColor.Default => "#036ac4", + _ => color.ToAttributeValue(), + }; + + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/TableOfContents.razor b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor new file mode 100644 index 0000000000000000000000000000000000000000..ddff6855b3fc2d76bcc8db4054e4f8836e7c1a22 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor @@ -0,0 +1,14 @@ +
+ + + + @GetTocItems(_anchors) + + + + + @if (ShowBackButton) + { + 返回顶部 + } +
\ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.cs b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.cs new file mode 100644 index 0000000000000000000000000000000000000000..5db072d8659f5b63c790e642708cb387f43c7e3f --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.cs @@ -0,0 +1,170 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace WebAdmin.Shared.Components; + +public partial class TableOfContents : IAsyncDisposable +{ + private Anchor[]? _anchors; + private bool _expanded = true; + + private IJSObjectReference _jsModule = default!; + + [Inject] protected IJSRuntime JSRuntime { get; set; } = default!; + + [Inject] protected NavigationManager NavigationManager { get; set; } = default!; + + /// + /// Gets or sets the heading for the ToC + /// Defaults to 'In this article' + /// + [Parameter] + public string Heading { get; set; } = "导航"; + + /// + /// Gets or sets a value indicating whether a 'Back to top' button should be rendered. + /// Defaults to true + /// + [Parameter] + public bool ShowBackButton { get; set; } = true; + + /// + /// Gets or sets the content to be rendered inside the component. + /// + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + try + { + // Unsubscribe from the event when our component is disposed + NavigationManager.LocationChanged -= LocationChanged; + + if (_jsModule is not null) await _jsModule.DisposeAsync(); + } + catch (Exception ex) when (ex is JSDisconnectedException || + ex is OperationCanceledException) + { + // The JSRuntime side may routinely be gone already if the reason we're disposing is that + // the client disconnected. This is not an error. + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _jsModule = await JSRuntime.InvokeAsync("import", + "./_content/WebAdmin.Shared/Components/TableOfContents.razor.js"); + var mobile = await _jsModule!.InvokeAsync("isDevice"); + + if (mobile) _expanded = false; + + await BackToTopAsync(); + await QueryDomAsync(); + } + } + + private async Task BackToTopAsync() + { + if (_jsModule is null) return; + await _jsModule.InvokeAsync("backToTop"); + } + + private async Task QueryDomAsync() + { + if (_jsModule is null) return; + + var foundAnchors = await _jsModule.InvokeAsync("queryDomForTocEntries"); + + if (AnchorsEqual(_anchors, foundAnchors)) return; + + _anchors = foundAnchors; + StateHasChanged(); + } + + private bool AnchorsEqual(Anchor[]? firstSet, Anchor[]? secondSet) => (firstSet ?? []) + .SequenceEqual(secondSet ?? []); + + protected override void OnInitialized() + { + // Subscribe to the event + NavigationManager.LocationChanged += LocationChanged; + } + + private async void LocationChanged(object? sender, LocationChangedEventArgs e) + { + try + { + await BackToTopAsync(); + await QueryDomAsync(); + } + catch (Exception) + { + // Already disposed + } + } + + [SuppressMessage("Style", "VSTHRD200:Use `Async` suffix for async methods", + Justification = "#vNext: To update in the next version")] + public async Task Refresh() + { + await QueryDomAsync(); + } + + private RenderFragment? GetTocItems(IEnumerable? items) + { + if (items is not null) + return builder => + { + var i = 0; + + builder.OpenElement(i++, "ul"); + foreach (var item in items) + { + builder.OpenElement(i++, "li"); + builder.OpenComponent(i++); + builder.AddAttribute(i++, "Href", item.Href); + builder.AddAttribute(i++, "Appearance", Appearance.Hypertext); + builder.AddAttribute(i++, "ChildContent", + (RenderFragment)(content => { content.AddContent(i++, item.Text); })); + builder.CloseComponent(); + if (item.Anchors is not null) builder.AddContent(i++, GetTocItems(item.Anchors)); + builder.CloseElement(); + } + + builder.CloseElement(); + }; + return builder => { builder.AddContent(0, ChildContent); }; + } + + private record Anchor(string Level, string Text, string Href, Anchor[] Anchors) + { + public virtual bool Equals(Anchor? other) + { + if (other is null) return false; + + if (Level != other.Level || + Text != other.Text || + Href != other.Href || + (Anchors?.Length ?? 0) != (other.Anchors?.Length ?? 0)) + return false; + + if (Anchors is not null && + Anchors.Length > 0) + for (var i = 0; i < Anchors.Length; i++) + if (!Anchors[i].Equals(other.Anchors![i])) + return false; + + return true; + } + + public override int GetHashCode() + => HashCode.Combine(Level, Text, Href); + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.css b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.css new file mode 100644 index 0000000000000000000000000000000000000000..f27538b1c6d06f5a7dac845e96c711810eaeceda --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.css @@ -0,0 +1,57 @@ +::deep ul { + list-style: none; + text-overflow: ellipsis; +} + +::deep ul:first-of-type { + padding-left: 1rem; +} + +[dir="rtl"] * ::deep ul:first-of-type { + padding-right: 1rem; +} + + +li { + text-overflow: ellipsis; +} + + +::deep fluent-anchor::part(control) { + color: var(--neutral-foreground-rest); + text-decoration: none; + overflow: hidden; + white-space: break-spaces; + line-height: var(--type-ramp-base-line-height); +} + +::deep fluent-anchor::part(control):focus { + outline: 1px dashed; + outline-offset: 3px; + border-radius: 0; +} + +::deep fluent-anchor::part(control):hover { + text-decoration: underline; +} + + +::deep fluent-button { + display: none; + position: fixed; + bottom: 45px; + right: 20px; + left: unset; + z-index: 99; + cursor: pointer; +} + +[dir="rtl"] * ::deep fluent-button { + display: none; + position: fixed; + bottom: 45px; + left: 20px; + right: unset; + z-index: 99; + cursor: pointer; +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.js b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.js new file mode 100644 index 0000000000000000000000000000000000000000..ea0e4bcfc2513c4484d696f619f8c33cd567fa00 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/TableOfContents.razor.js @@ -0,0 +1,74 @@ +export function queryDomForTocEntries() { + const article = document.getElementById('article'); + const headings = article.querySelectorAll("h2, h3, h4"); + + let tocArray = new Array() + let chapter = null; + let subchapter = null; + + for (let element of headings) { + if (!element.id) { + let anchorText = element.innerText; + let elementId = anchorText.replaceAll(" ", "-", "/", "\\", "#", "$", "@", ":", ",").toLowerCase(); + element.id = elementId; + } + if (element.innerText) { + let anchor = { + "level": element.nodeName, + "text": element.innerText, + "href": "#" + element.id, + "anchors": new Array() + }; + + if ("H3" === element.nodeName) { + if (chapter) { + subchapter = anchor; + chapter.anchors.push(subchapter); + + } + } else if ("H4" === element.nodeName) { + if (subchapter) { + subchapter.anchors.push(anchor); + } + } + else { + chapter = anchor; + tocArray.push(chapter); + + } + } + } + return tocArray; +} + +let backToTopButton = document.getElementById("backtotop"); + +// When the user scrolls down 20px from the top of the document, show the button +let bodycontent = document.getElementById('body-content'); +if (!bodycontent) { + bodycontent = document.body; +} + +bodycontent.onscroll = function () { + scrollFunction() +}; + +function scrollFunction() { + if (document.body.scrollTop > 20 || document.getElementById('body-content').scrollTop > 20 || document.documentElement.scrollTop > 20) { + backToTopButton.style.display = "flex"; + } else { + backToTopButton.style.display = "none"; + } +} + +// When the user clicks on the button, scroll to the top of the document +export function backToTop() { + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + document.getElementById('body-content').scrollTop = 0; +} + +// Very simple check to see if mobile or tablet is being used +export function isDevice() { + return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(navigator.userAgent); +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/UserProfile.razor b/src/Web/WebAdmin.Shared/Components/UserProfile.razor new file mode 100644 index 0000000000000000000000000000000000000000..bc665dd4a94cb1da0959823125c8502c1a549f47 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/UserProfile.razor @@ -0,0 +1,11 @@ +
+ +
\ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Components/UserProfile.razor.css b/src/Web/WebAdmin.Shared/Components/UserProfile.razor.css new file mode 100644 index 0000000000000000000000000000000000000000..35b0e5599e77775cf01db80e17f3e019c1aee3f4 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Components/UserProfile.razor.css @@ -0,0 +1,5 @@ +.profile { + padding-right: 16px; + margin-left: 0; + /*margin-right: 10px;*/ +} diff --git a/src/Web/WebAdmin.Shared/Configurations/AdminUiOptions.cs b/src/Web/WebAdmin.Shared/Configurations/AdminUiOptions.cs new file mode 100644 index 0000000000000000000000000000000000000000..5670308787262c285493f0ef347e11c7d4391a34 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Configurations/AdminUiOptions.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.Configuration; + +namespace WebAdmin.Shared.Configurations; + +/// +/// 管理后台配置 +/// +public class AdminUiOptions +{ + /// 管理后台的基础配置 + public AdminConfiguration Admin { get; } = new(); + + /// 全球化配置 + public CultureConfiguration Culture { get; set; } = new(); + + /// HTTP 托管环境配置 + public HttpConfiguration Http { get; set; } = new(); + + /// + /// 将从 appsettings 文件解析的配置应用到这些选项中。 + /// + /// 绑定到此实例的配置 + public void BindConfiguration(IConfiguration configuration) + { + configuration.GetSection(nameof(AdminConfiguration)).Bind(Admin); + configuration.GetSection(nameof(CultureConfiguration)).Bind(Culture); + configuration.GetSection(nameof(HttpConfiguration)).Bind(Http); + } +} + +/// +/// 管理后台的基础配置 +/// +public class AdminConfiguration +{ + /// Page Name + public string? PageTitle { get; set; } + + /// Favicon Uri + public string? FaviconUri { get; set; } + + /// Web Admin Redirect Uri + public string? AdminRedirectUri { get; set; } + + /// Scopes + public string[] Scopes { get; set; } = []; + + /// Administration Role Name + public string? AdministrationRole { get; set; } + + /// Require HttpsMetadata + public bool RequireHttpsMetadata { get; set; } + + /// Web Admin CookieName + public string? AdminCookieName { get; set; } + + /// Web Admin Cookie Expires UtcHours + public double AdminCookieExpiresUtcHours { get; set; } + + /// Token Validation ClaimName + public string? TokenValidationClaimName { get; set; } + + /// Token Validation ClaimRole + public string? TokenValidationClaimRole { get; set; } + + /// IdentityServer BaseUrl + public string? IdentityServerBaseUrl { get; set; } + + /// ClientId + public string? ClientId { get; set; } + + /// ClientSecret + public string? ClientSecret { get; set; } + + /// Oidc ResponseType + public string? OidcResponseType { get; set; } +} + +/// +/// 全球化配置 +/// +public class CultureConfiguration +{ + /// 可用的多语言文化 + public static readonly string[] AvailableCultures = ["zh-Hans", "en"]; + + /// 默认的文化 + public static readonly string DefaultRequestCulture = "zh-Hans"; + + /// 可用的多语言文化列表 + public List Cultures { get; set; } = []; + + /// 默认的文化 + public string DefaultCulture { get; set; } = DefaultRequestCulture; +} + +/// +/// Http 配置 +/// +public class HttpConfiguration +{ + /// BasePath + public string BasePath { get; set; } = ""; +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Configurations/ConfigurationConsts.cs b/src/Web/WebAdmin.Shared/Configurations/ConfigurationConsts.cs new file mode 100644 index 0000000000000000000000000000000000000000..080bf0fe2f86796d8bdba5395dd15aa10c4b4e35 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Configurations/ConfigurationConsts.cs @@ -0,0 +1,7 @@ +namespace WebAdmin.Shared.Configurations; + +public class ConfigurationConsts +{ + public const string ResourcesPath = "Resources"; + public const string HeaderColor = "#ffffff"; +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/AppVersionService.cs b/src/Web/WebAdmin.Shared/Infrastructure/AppVersionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..85294996812998b7cfdbeb2322f9ea9dce827a0b --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/AppVersionService.cs @@ -0,0 +1,27 @@ +using System.Reflection; + +namespace WebAdmin.Shared.Infrastructure; + +/// +/// AppVersion Service +/// +public class AppVersionService : IAppVersionService +{ + public string Version => GetVersionFromAssembly(); + + public static string GetVersionFromAssembly() + { + string strVersion = default!; + var versionAttribute = + Assembly.GetExecutingAssembly().GetCustomAttribute(); + if (versionAttribute == null) return strVersion; + var version = versionAttribute.InformationalVersion; + var plusIndex = version.IndexOf('+'); + if (plusIndex >= 0 && plusIndex + 9 < version.Length) + strVersion = version[..(plusIndex + 9)]; + else + strVersion = version; + + return strVersion; + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/CacheStorageAccessor.cs b/src/Web/WebAdmin.Shared/Infrastructure/CacheStorageAccessor.cs new file mode 100644 index 0000000000000000000000000000000000000000..3a1201828b82e70c7fb34d9923d87ce7e64bb92d --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/CacheStorageAccessor.cs @@ -0,0 +1,96 @@ +using Microsoft.FluentUI.AspNetCore.Components.Utilities; +using Microsoft.JSInterop; + +namespace WebAdmin.Shared.Infrastructure; + +public class CacheStorageAccessor(IJSRuntime js, IAppVersionService vs) : JSModule(js, "./_content/WebAdmin.Shared/js/CacheStorageAccessor.js") +{ + private readonly IAppVersionService _vs = vs; + private string? _currentCacheVersion = default; + + public async ValueTask PutAsync(HttpRequestMessage requestMessage, HttpResponseMessage responseMessage) + { + var requestMethod = requestMessage.Method.Method; + var requestBody = await GetRequestBodyAsync(requestMessage); + var responseBody = await responseMessage.Content.ReadAsStringAsync(); + + await InvokeVoidAsync("put", requestMessage.RequestUri!, requestMethod, requestBody, responseBody); + } + + public async ValueTask PutAndGetAsync(HttpRequestMessage requestMessage, HttpResponseMessage responseMessage) + { + var requestMethod = requestMessage.Method.Method; + var requestBody = await GetRequestBodyAsync(requestMessage); + var responseBody = await responseMessage.Content.ReadAsStringAsync(); + + await InvokeVoidAsync("put", requestMessage.RequestUri!, requestMethod, requestBody, responseBody); + + return responseBody; + } + + public async ValueTask GetAsync(HttpRequestMessage requestMessage) + { + if (_currentCacheVersion is null) + { + await InitializeCacheAsync(); + } + + var result = await InternalGetAsync(requestMessage); + + return result; + } + private async ValueTask InternalGetAsync(HttpRequestMessage requestMessage) + { + var requestMethod = requestMessage.Method.Method; + var requestBody = await GetRequestBodyAsync(requestMessage); + var result = await InvokeAsync("get", requestMessage.RequestUri!, requestMethod, requestBody); + + return result; + } + + public async ValueTask RemoveAsync(HttpRequestMessage requestMessage) + { + var requestMethod = requestMessage.Method.Method; + var requestBody = await GetRequestBodyAsync(requestMessage); + + await InvokeVoidAsync("remove", requestMessage.RequestUri!, requestMethod, requestBody); + } + + public async ValueTask RemoveAllAsync() + { + await InvokeVoidAsync("removeAll"); + } + private static async ValueTask GetRequestBodyAsync(HttpRequestMessage requestMessage) + { + var requestBody = string.Empty; + if (requestMessage.Content is not null) + { + requestBody = await requestMessage.Content.ReadAsStringAsync(); + } + + return requestBody; + } + + private async Task InitializeCacheAsync() + { + // last version cached is stored in appVersion + var requestMessage = new HttpRequestMessage(HttpMethod.Get, "appVersion"); + + // get the last version cached + var result = await InternalGetAsync(requestMessage); + if (!result.Equals(_vs.Version)) + { + // running newer version now, clear cache, and update version in cache + await RemoveAllAsync(); + var requestBody = await GetRequestBodyAsync(requestMessage); + await InvokeVoidAsync( + "put", + requestMessage.RequestUri!, + requestMessage.Method.Method, + requestBody, + _vs.Version); + } + // + _currentCacheVersion = _vs.Version; + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/CustomIcons.cs b/src/Web/WebAdmin.Shared/Infrastructure/CustomIcons.cs new file mode 100644 index 0000000000000000000000000000000000000000..8fdce66476627de0063a4bc6fd544b1f291932bb --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/CustomIcons.cs @@ -0,0 +1,13 @@ +using Microsoft.FluentUI.AspNetCore.Components; + +namespace WebAdmin.Shared.Infrastructure; + +public static class CustomIcons +{ + public static class Size20 + { + // The official SVGs from GitHub have a viewbox of 96x96, so we need to scale them down to 20x20 and center them within the 24x24 box to make them match the + // other icons we're using. We also need to remove the fill attribute from the SVGs so that we can color them with CSS. + public sealed class GitHub : Icon { public GitHub() : base("GitHub", IconVariant.Regular, IconSize.Size20, @"") { } } + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/HttpBasedStaticAssetService.cs b/src/Web/WebAdmin.Shared/Infrastructure/HttpBasedStaticAssetService.cs new file mode 100644 index 0000000000000000000000000000000000000000..cad8c843d8f3c42eaec753c28d734d3d4bbeb555 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/HttpBasedStaticAssetService.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Components; + +namespace WebAdmin.Shared.Infrastructure; + +public class HttpBasedStaticAssetService : IStaticAssetService +{ + private readonly HttpClient _httpClient; + private readonly CacheStorageAccessor _cacheStorageAccessor; + + public HttpBasedStaticAssetService(HttpClient httpClient, NavigationManager navigationManager, CacheStorageAccessor cacheStorageAccessor) + { + _httpClient = httpClient; + _httpClient.BaseAddress ??= new Uri(navigationManager.BaseUri); + _cacheStorageAccessor = cacheStorageAccessor; + } + + public async Task GetAsync(string assetUrl, bool useCache = true) + { + string? result = null; + + HttpRequestMessage? message = CreateMessage(assetUrl); + + if (useCache) + { + // Get the result from the cache + result = await _cacheStorageAccessor.GetAsync(message); + } + + if (string.IsNullOrEmpty(result)) + { + //It not in the cache (or cache not used), download the asset + HttpResponseMessage? response = await _httpClient.SendAsync(message); + + // If successful, store the response in the cache and get the result + if (response.IsSuccessStatusCode) + { + if (useCache) + { + // Store the response in the cache and get the result + result = await _cacheStorageAccessor.PutAndGetAsync(message, response); + } + else + { + result = await response.Content.ReadAsStringAsync(); + } + } + else + { + result = string.Empty; + } + } + + return result; + } + + private static HttpRequestMessage CreateMessage(string url) => new(HttpMethod.Get, url); +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/IAppVersionService.cs b/src/Web/WebAdmin.Shared/Infrastructure/IAppVersionService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e6e925bb31bafe1e5d25b0fc5b785f1ebf365477 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/IAppVersionService.cs @@ -0,0 +1,9 @@ +namespace WebAdmin.Shared.Infrastructure; + +/// +/// AppVersion Service +/// +public interface IAppVersionService +{ + string Version { get; } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/IStaticAssetService.cs b/src/Web/WebAdmin.Shared/Infrastructure/IStaticAssetService.cs new file mode 100644 index 0000000000000000000000000000000000000000..789ccca2fd256fe9b01adf34b86cbb2f8b9544c7 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/IStaticAssetService.cs @@ -0,0 +1,6 @@ +namespace WebAdmin.Shared.Infrastructure; + +public interface IStaticAssetService +{ + public Task GetAsync(string assetUrl, bool useCache = true); +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/NavProvider.cs b/src/Web/WebAdmin.Shared/Infrastructure/NavProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..d1c8b97fdce1dfb2fb540f0dadcaf921d39de365 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/NavProvider.cs @@ -0,0 +1,182 @@ +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.FluentUI.AspNetCore.Components; +using WebAdmin.Shared.Records; +using NavLink = WebAdmin.Shared.Records.NavLink; + +namespace WebAdmin.Shared.Infrastructure; + +public class NavProvider +{ + public NavProvider() + { + NavMenuItems = + [ + new NavLink( + href: "/", + match: NavLinkMatch.All, + icon: new Icons.Regular.Size20.Home(), + name: "Home" + ), + + new NavLink( + href: "/counter", + match: NavLinkMatch.All, + icon: new Icons.Regular.Size20.NumberSymbolSquare(), + name: "Counter" + ), + + + new NavLink( + href: "/weather", + match: NavLinkMatch.All, + icon: new Icons.Regular.Size20.PersonKey(), + name: "Weather" + ), + + // 会员管理 + new NavGroup( + icon: new Icons.Regular.Size20.Group(), + name: "CustomerMgr", + expanded: true, + gap: "10px", + children: + [ + new NavLink( + href: "/customer/list", + icon: new Icons.Regular.Size20.Accessibility(), + name: "CustomerList" + ), + new NavLink( + href: "/customer/level", + icon: new Icons.Regular.Size20.Trophy(), + name: "CustomerLevel" + ) + ]), + + // 订单管理 + new NavGroup( + icon: new Icons.Regular.Size20.ReceiptCube(), + name: "OrderMgr", + expanded: false, + gap: "10px", + children: + [ + new NavLink( + href: "/order/purchase", + icon: new Icons.Regular.Size20.ShoppingBag(), + name: "PurchaseOrder" + ), + new NavLink( + href: "/order/process", + icon: new Icons.Regular.Size20.BuildingFactory(), + name: "ProcessOrder" + ) + ]), + + // 支付管理 + new NavGroup( + icon: new Icons.Regular.Size20.CurrencyDollarEuro(), + name: "PaymentMgr", + expanded: false, + gap: "10px", + children: + [ + new NavLink( + href: "/payment/receiving-orders", + icon: new Icons.Regular.Size20.CreditCardClock(), + name: "ReceiveOrder" + ), + new NavLink( + href: "/payment/paid-orders", + icon: new Icons.Regular.Size20.WalletCreditCard(), + name: "PaidOrder" + ) + ]), + + // 物流管理 + new NavGroup( + icon: new Icons.Regular.Size20.VehicleTruckBag(), + name: "LogisticsMgr", + expanded: false, + gap: "10px", + children: + [ + new NavLink( + href: "/logistics/orders", + icon: new Icons.Regular.Size20.BoxMultipleSearch(), + name: "LogisticsOrder" + ), + new NavLink( + href: "/logistics/services", + icon: new Icons.Regular.Size20.Globe(), + name: "LogisticsService" + ), + new NavLink( + href: "/logistics/channels", + icon: new Icons.Regular.Size20.CompassNorthwest(), + name: "LogisticsChannel" + ) + ]), + + // 终端管理 + new NavGroup( + icon: new Icons.Regular.Size20.TabletLaptop(), + name: "DeviceMgr", + expanded: false, + gap: "10px", + children: + [ + new NavLink( + href: "/device/list", + icon: new Icons.Regular.Size20.DeviceMeetingRoom(), + name: "DeviceList" + ), + new NavLink( + href: "/device/types", + icon: new Icons.Regular.Size20.AppsList(), + name: "DeviceType" + ), + new NavLink( + href: "/device/models", + icon: new Icons.Regular.Size20.Cube(), + name: "DeviceModule" + ), + new NavLink( + href: "/device/maintenance-orders", + icon: new Icons.Regular.Size20.VirtualNetworkToolbox(), + name: "DeviceMaintain" + ), + new NavLink( + href: "/device/remote-controls", + icon: new Icons.Regular.Size20.DesktopCursor(), + name: "RemoveControl" + ), + new NavLink( + href: "/device/versions", + icon: new Icons.Regular.Size20.BranchFork(), + name: "VersionMgr" + ) + ]), + ]; + + FlattenedMenuItems = GetFlattenedMenuItems(NavMenuItems) + .ToList() + .AsReadOnly(); + } + + public IReadOnlyList NavMenuItems { get; init; } + + public IReadOnlyList FlattenedMenuItems { get; init; } + + private static IEnumerable GetFlattenedMenuItems(IEnumerable items) + { + foreach (var item in items) + { + yield return item; + + if (item is not NavGroup group || !group.Children.Any()) continue; + + foreach (var flattenedMenuItem in GetFlattenedMenuItems(group.Children)) yield return flattenedMenuItem; + } + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/OfficeColorNameMapper.cs b/src/Web/WebAdmin.Shared/Infrastructure/OfficeColorNameMapper.cs new file mode 100644 index 0000000000000000000000000000000000000000..cb6dbf085e91dfd2bee5d3d956f408714de6b024 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/OfficeColorNameMapper.cs @@ -0,0 +1,38 @@ +using Microsoft.FluentUI.AspNetCore.Components; + +namespace WebAdmin.Shared.Infrastructure; + +/// +public static class OfficeColorNameMapper +{ + /// + public static string Map(OfficeColor? color) => color switch + { + OfficeColor.Default => "源远流长", + OfficeColor.Access => "朱砂古韵", + OfficeColor.Booking => "碧波荡漾", + OfficeColor.Exchange => "晴空万里", + OfficeColor.Excel => "竹翠绿意", + OfficeColor.GroupMe => "青天碧海", + OfficeColor.Office => "朱门绯影", + OfficeColor.OneDrive => "浩瀚蓝天", + OfficeColor.OneNote => "紫气东来", + OfficeColor.Outlook => "高望碧空", + OfficeColor.Planner => "绿野策划", + OfficeColor.PowerApps => "暗紫深思", + OfficeColor.PowerBI => "金黄智慧", + OfficeColor.PowerPoint => "赤日炎炎", + OfficeColor.Project => "绿意盎然", + OfficeColor.Publisher => "碧泉出版", + OfficeColor.SharePoint => "分享蓝图", + OfficeColor.Skype => "蓝天通话", + OfficeColor.Stream => "红流媒体", + OfficeColor.Sway => "水绿摇曳", + OfficeColor.Teams => "深蓝合作", + OfficeColor.Visio => "视觉蓝图", + OfficeColor.Windows => "开窗见蓝", + OfficeColor.Word => "字海泛蓝", + OfficeColor.Yammer => "沟通蓝天", + _ => throw new ArgumentOutOfRangeException(nameof(color), color, null) + }; +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/ServerStaticAssetService.cs b/src/Web/WebAdmin.Shared/Infrastructure/ServerStaticAssetService.cs new file mode 100644 index 0000000000000000000000000000000000000000..881c641d2687c26d00ff22edba3a67a3cf31fbe4 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/ServerStaticAssetService.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components; + +namespace WebAdmin.Shared.Infrastructure; + +internal class ServerStaticAssetService : IStaticAssetService +{ + private readonly HttpClient _httpClient; + + public ServerStaticAssetService(HttpClient httpClient, NavigationManager navigationManager) + { + _httpClient = httpClient; + _httpClient.BaseAddress ??= new Uri(navigationManager.BaseUri); + } + public async Task GetAsync(string assetUrl, bool useCache = true) + { + var message = new HttpRequestMessage(HttpMethod.Get, assetUrl); + var response = await _httpClient.SendAsync(message); + + return await response.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Infrastructure/ServiceCollectionExtensions.cs b/src/Web/WebAdmin.Shared/Infrastructure/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..fb3be9969bba21a35ebcbd2110259466df25a7af --- /dev/null +++ b/src/Web/WebAdmin.Shared/Infrastructure/ServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace WebAdmin.Shared.Infrastructure; + +public static class ServiceCollectionExtensions +{ + /// + /// 为 Blazor 库添加 Web UI Web 组件所需的常用客户端服务 + /// + /// Service collection + public static IServiceCollection AddAdminUiClientServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient(); + services.AddSingleton(); + + return services; + } + + /// + /// 为 Blazor 库添加 Web UI 组件所需的通用服务器服务 + /// + /// Service collection + public static IServiceCollection AddAdminUiServerServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddHttpClient(); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Layout/MainLayout.razor b/src/Web/WebAdmin.Shared/Layout/MainLayout.razor new file mode 100644 index 0000000000000000000000000000000000000000..99ff0f98a4cc581a352938343bcbd70ada2dc02f --- /dev/null +++ b/src/Web/WebAdmin.Shared/Layout/MainLayout.razor @@ -0,0 +1,111 @@ +@inherits LayoutComponentBase + + + + + WebAdmin + + + + + + + + + + + + + +
+
+ + + + @Body + +
+ + + + + + + +
+
+
+ + github + + Copyright Ⓒ 2024 WebAdmin + +
+ + +@code +{ + + private const string MESSAGES_NOTIFICATION_CENTER = "NOTIFICATION_CENTER"; + private const string MESSAGES_TOP = "TOP"; + private const string MESSAGES_DIALOG = "DIALOG"; + private const string MESSAGES_CARD = "CARD"; + private const string JAVASCRIPT_FILE = "./_content/WebAdmin.Shared/Layout/MainLayout.razor.js"; + private string? _version; + private bool _mobile; + private string? _prevUri; + private TableOfContents? _toc; + private bool _menuChecked = true; + + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + + [Inject] + public IJSRuntime JSRuntime { get; set; } = default!; + + protected override void OnInitialized() + { + _version = AppVersionService.GetVersionFromAssembly(); + + _prevUri = NavigationManager.Uri; + NavigationManager.LocationChanged += LocationChanged; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var jsModule = await JSRuntime.InvokeAsync("import", JAVASCRIPT_FILE); + _mobile = await jsModule.InvokeAsync("isDevice"); + await jsModule.DisposeAsync(); + } + } + + public EventCallback OnRefreshTableOfContents => EventCallback.Factory.Create(this, RefreshTableOfContentsAsync); + + private async Task RefreshTableOfContentsAsync() + { + await _toc!.Refresh(); + } + + private void HandleChecked() + { + _menuChecked = !_menuChecked; + } + + private void LocationChanged(object? sender, LocationChangedEventArgs e) + { + + if (!e.IsNavigationIntercepted && new Uri(_prevUri!).AbsolutePath != new Uri(e.Location).AbsolutePath) + { + _prevUri = e.Location; + if (_mobile && _menuChecked == true) + { + _menuChecked = false; + StateHasChanged(); + } + } + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Layout/MainLayout.razor.js b/src/Web/WebAdmin.Shared/Layout/MainLayout.razor.js new file mode 100644 index 0000000000000000000000000000000000000000..76e0d31f9a0eab8f68da9d8ebb34ba8eab9cfa55 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Layout/MainLayout.razor.js @@ -0,0 +1,12 @@ +export function isDevice() { + return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(navigator.userAgent); +} + +export function isDarkMode() { + let matched = window.matchMedia("(prefers-color-scheme: dark)").matches; + + if (matched) + return true; + else + return false; +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Layout/NavMenu.razor b/src/Web/WebAdmin.Shared/Layout/NavMenu.razor new file mode 100644 index 0000000000000000000000000000000000000000..b27e2905172ed89339bcb04f5423307c4b4dc7db --- /dev/null +++ b/src/Web/WebAdmin.Shared/Layout/NavMenu.razor @@ -0,0 +1,21 @@ +@inject IStringLocalizer L +@inject NavProvider NavProvider + +@code { + private bool _expanded = true; +} + + \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Layout/NavMenu.razor.css b/src/Web/WebAdmin.Shared/Layout/NavMenu.razor.css new file mode 100644 index 0000000000000000000000000000000000000000..4e15395e091e5445136786385471f8b8bda9e738 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Layout/NavMenu.razor.css @@ -0,0 +1,105 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/src/Web/WebAdmin.Shared/Layout/NavMenuItem.razor b/src/Web/WebAdmin.Shared/Layout/NavMenuItem.razor new file mode 100644 index 0000000000000000000000000000000000000000..66345e390026e257404a12e14c2134e8fdaa7009 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Layout/NavMenuItem.razor @@ -0,0 +1,35 @@ +@using NavLink = WebAdmin.Shared.Records.NavLink +@inject IStringLocalizer L + +@switch (Value) +{ + case NavGroup group: + + +

@L[group.Name]

+
+ + @foreach (var item in group.Children) + { + + } + +
+ break; + case NavLink: + + @if (Value.Match is NavLinkMatch.All) + { +

@L[Value.Name]

+ } + else + { + @L[Value.Name] + } +
+ break; +} + +@code { + [Parameter] [EditorRequired] public required NavItem Value { get; set; } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Layout/NavMenuItem.razor.css b/src/Web/WebAdmin.Shared/Layout/NavMenuItem.razor.css new file mode 100644 index 0000000000000000000000000000000000000000..5f282702bb03ef11d7184d19c80927b47f919764 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Layout/NavMenuItem.razor.css @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Records/NavItem.cs b/src/Web/WebAdmin.Shared/Records/NavItem.cs new file mode 100644 index 0000000000000000000000000000000000000000..b43ab899d8c69a9f7e5996abd7580ee62ee717b3 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Records/NavItem.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.FluentUI.AspNetCore.Components; + +namespace WebAdmin.Shared.Records; + +public abstract record NavItem +{ + public string Name { get; init; } = string.Empty; + public string? Href { get; init; } + public NavLinkMatch Match { get; init; } = NavLinkMatch.Prefix; + public Icon Icon { get; init; } = new Icons.Regular.Size20.Document(); + public Color IconColor { get; set; } = Color.Accent; +} + +public record NavLink : NavItem +{ + public NavLink( + string? href, + Icon icon, + string name, + NavLinkMatch match = NavLinkMatch.Prefix, + Color iconColor = Color.Accent) + { + Href = href; + Icon = icon; + Name = name; + Match = match; + IconColor = iconColor; + } +} + +public record NavGroup : NavItem +{ + public bool Expanded { get; init; } + public string Gap { get; init; } + public IReadOnlyList Children { get; } + + public NavGroup(Icon icon, string name, bool expanded, string gap, List children) + { + Href = null; + Icon = icon; + Name = name; + Expanded = expanded; + Gap = gap; + Children = children.AsReadOnly(); + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.cs b/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.cs new file mode 100644 index 0000000000000000000000000000000000000000..4b0c41c03a270f7bb3d12cb517933471ce1c63e2 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.cs @@ -0,0 +1,5 @@ +namespace WebAdmin.Shared.Resources.Layout; + +public class NavMenu +{ +} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.en.resx b/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.en.resx new file mode 100644 index 0000000000000000000000000000000000000000..2034ad22c355302c15b7ea1b093d0e301ae4e678 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.en.resx @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Customer + + + Customer Level + + + Customer List + + + Order + + + Purchase Orders + + + Process Orders + + + Payment + + + Receiving Orders + + + Paid Orders + + + Logistics + + + Logistics Orders + + + Logistics Services + + + Logistics Channels + + + Device + + + Device List + + + Device Types + + + Device Modules + + + Maintenance Orders + + + Remote Control + + + Version + + \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.zh-Hans.resx b/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.zh-Hans.resx new file mode 100644 index 0000000000000000000000000000000000000000..708114e7447ad6990032554f09da5f4b12f3e412 --- /dev/null +++ b/src/Web/WebAdmin.Shared/Resources/Layout/NavMenu.zh-Hans.resx @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 会员管理 + + + 会员列表 + + + 会员等级 + + + 订单管理 + + + 采购订单 + + + 加工订单 + + + 支付管理 + + + 收款订单 + + + 付款订单 + + + 物流管理 + + + 物流订单 + + + 物流服务 + + + 物流渠道 + + + 终端管理 + + + 终端列表 + + + 型号列表 + + + 基础模块 + + + 维护订单 + + + 远程控制 + + + 版本管理 + + \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/WebAdmin.Shared.csproj b/src/Web/WebAdmin.Shared/WebAdmin.Shared.csproj new file mode 100644 index 0000000000000000000000000000000000000000..984f136caf056352e2deb11f405ff375b322cc49 --- /dev/null +++ b/src/Web/WebAdmin.Shared/WebAdmin.Shared.csproj @@ -0,0 +1,40 @@ + + + + false + false + false + + + + + + + + False + 6 + true + false + 612;618;1701;1702;8669;1591;1816 + + + + True + 1701;1702;8669;1591 + false + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/_App.razor b/src/Web/WebAdmin.Shared/_App.razor new file mode 100644 index 0000000000000000000000000000000000000000..6bf8f26b0516a31452ee406e9d353eeb57ba33e0 --- /dev/null +++ b/src/Web/WebAdmin.Shared/_App.razor @@ -0,0 +1,13 @@ + + + + + + + + 404 Not found 页面未找到 + +

抱歉,该地址没有任何信息.

+
+
+
\ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/_App.razor.cs b/src/Web/WebAdmin.Shared/_App.razor.cs new file mode 100644 index 0000000000000000000000000000000000000000..551f55fb0add3132fa786cf0cbd654c713b3ee3f --- /dev/null +++ b/src/Web/WebAdmin.Shared/_App.razor.cs @@ -0,0 +1,15 @@ +//namespace WebAdmin.Shared; + +//public partial class App +//{ + +// public static string PageTitle(string page) +// { +// return $"{page} - WebAdmin"; +// } + +// public const string MESSAGES_NOTIFICATION_CENTER = "NOTIFICATION_CENTER"; +// public const string MESSAGES_TOP = "TOP"; +// public const string MESSAGES_DIALOG = "DIALOG"; +// public const string MESSAGES_CARD = "CARD"; +//} \ No newline at end of file diff --git a/src/Web/WebAdmin.Shared/_Imports.razor b/src/Web/WebAdmin.Shared/_Imports.razor new file mode 100644 index 0000000000000000000000000000000000000000..17a871160a7bcf34179cafd8ed38199034d74e64 --- /dev/null +++ b/src/Web/WebAdmin.Shared/_Imports.razor @@ -0,0 +1,23 @@ +@using System.Net.Http +@using System.Net.Http.Json + +@using WebAdmin.Shared.Infrastructure +@using WebAdmin.Shared.Components +@using WebAdmin.Shared.Records +@using WebAdmin.Shared.Layout +@using WebAdmin.Shared +@using WebAdmin.Shared.Configurations + +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization + +@using Microsoft.Extensions.Logging; +@using Microsoft.Extensions.Localization + +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@using Microsoft.FluentUI.AspNetCore.Components.DesignTokens + +@using Microsoft.JSInterop diff --git a/src/Web/WebAdmin.Shared/wwwroot/css/app.css b/src/Web/WebAdmin.Shared/wwwroot/css/app.css new file mode 100644 index 0000000000000000000000000000000000000000..1de6cdbeb559abfcaffd94102c6f8bf8551a8ae0 --- /dev/null +++ b/src/Web/WebAdmin.Shared/wwwroot/css/app.css @@ -0,0 +1,479 @@ +@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; + +body { + height: 100%; + overflow: hidden; +} + +.siteheader { + border-bottom: calc(var(--stroke-width) * 2px) solid var(--accent-fill-rest); + margin-bottom: 0 !important; +} + + .siteheader .logo { + width: 108px; + height: 23px; + grid-column: 1; + } + + .siteheader .search { + display: flex; + align-items: center; + padding-right: 20px; + } + + .siteheader .links { + padding-right: 10px; + display: flex; + align-items: center; + } + + .siteheader .notifications { + display: flex; + align-items: center; + } + + .siteheader .settings { + padding-right: 6px; + display: flex; + align-items: center; + margin-left: 0; + margin-right: 10px; + } + +[dir="rtl"] .siteheader .settings { + padding: 0 0 0 6px; + margin-left: 10px; + margin-right: 0; +} + +[dir="rtl"] .siteheader .search { + padding-left: 20px; + padding-right: 0; +} + +[dir="rtl"] .siteheader .links { + padding-left: 10px; +} + +.search-result-icon { + vertical-align: middle; +} + +.body-stack { + flex-direction: row; +} + +.footer { + display: flex !important; + flex-direction: row !important; + background: var(--neutral-layer-4); + color: var(--neutral-foreground-rest) !important; + padding: 10px 10px; + margin-top: 0px !important; +} + + .footer .version a { + color: var(--neutral-foreground-rest); + text-decoration: none; + } + + .footer .version a:focus { + outline: 1px dashed; + outline-offset: 3px; + } + + .footer .version a:hover { + text-decoration: underline; + } + + +nav.sitenav { + background-color: var(--neutral-layer-1); + padding: 1.5rem 1rem; + height: calc(100dvh - 90px); + width: 18rem; + overflow-y: auto; +} + +nav h2 { + font-size: var(--type-ramp-plus-1-font-size); + line-height: var(--type-ramp-plus-1-line-height); + padding: 15px 0; + margin: 0; + pointer-events: none; +} + +nav h3 { + font-size: var(--type-ramp-base-font-size); + line-height: var(--type-ramp-minus-1-line-height); + padding: 10px 0; + margin: 0; + pointer-events: none; +} + + +nav fluent-anchor { + width: 100%; + color: var(--fill-color); +} + + nav fluent-anchor::part(control) { + justify-content: start; + background: var(--accent-fill-rest); + } + + +.fluent-nav-link.notactive .fluent-nav-text { + font-weight: 600 !important; +} + +.content { + display: flex; + background-color: var(--neutral-layer-1); +} + +article { + padding: 1.5rem 1rem; + border-right: 1px solid var(--neutral-stroke-divider-rest); + margin: 0 0; + overflow-x: hidden; + transition: all 300ms ease-in-out; + width: calc(100% - 18rem); +} + +aside { + padding: 1.5rem 1rem; + top: 0px; + height: 100vh; + position: sticky; + width: 18rem; +} + +#navmenu-toggle { + display: none; +} + +.navmenu-icon { + display: none; +} + +#navmenu-toggle:checked > nav { + width: 0px; +} + +[dir="rtl"] #navmenu-toggle:checked ~ nav { + right: 0px; +} + +#color { + margin-right: 10px; + margin-left: 0; +} + +[dir="rtl"] #color { + margin-left: 10px; + margin-right: 0; +} + + +label { + color: var(--neutral-foreground-rest); + cursor: pointer; +} + +.shell, .sourceCode { + background: var(--neutral-stroke-layer-rest); + padding: 7px; +} + +code { + background: var(--neutral-stroke-layer-rest); +} + +.demopanel { + border: 1px dashed var(--accent-fill-rest); + padding: 5px; +} + +.highlighted-row { + background-color: var(--neutral-fill-secondary-hover); +} + +kbd { + padding: 0.10rem 0.25rem; + font-size: 0.875em; + color: var(--neutral-foreground-rest); + background-color: var(--neutral-fill-secondary-rest); + border-radius: 0.25rem; + border: 1px solid var(--accent-fill-rest); +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + margin: 20px 0; + color: var(--neutral-foreground-rest); +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + border: 1px dashed var(--error); + background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNDgnIGhlaWdodD0nNDgnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZycgeG1sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnIG92ZXJmbG93PSdoaWRkZW4nIGZpbGw9JyNCQzJGMzInPjxwYXRoIGQ9J00yNCA0YTIwIDIwIDAgMSAwIDAgNDAgMjAgMjAgMCAwIDAgMC00MFptMCA5Yy42OSAwIDEuMjUuNTYgMS4yNSAxLjI1djEyLjVhMS4yNSAxLjI1IDAgMSAxLTIuNSAwdi0xMi41YzAtLjY5LjU2LTEuMjUgMS4yNS0xLjI1Wm0wIDIxYTIgMiAwIDEgMSAwLTQgMiAyIDAgMCAxIDAgNFonPjwvcGF0aD48L3N2Zz4=") no-repeat 1rem/1.8rem; + padding: 1rem 1rem 1rem 3.7rem; +} + + .blazor-error-boundary::before { + content: "An error has occurred: " + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: var( --neutral-fill-layer-rest); + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +@media (max-width: 767px) { + + .siteheader { + grid-area: header; + grid-template-columns: 150px 1fr; + align-items: center; + justify-content: flex-start; + } + + .siteheader .search { + display: none; + } + + .siteheader .logo { + width: 160px; + height: 23px; + /*padding: 0 25px;*/ + } + + + .body-stack { + flex-direction: column !important; + } + + nav.sitenav { + width: 100%; + height: calc(100dvh - 50px); + } + + .navmenu { + width: 100%; + } + + #navmenu-toggle { + appearance: none; + } + + .navmenu-icon { + cursor: pointer; + z-index: 10; + display: block; + position: absolute; + top: 15px; + left: unset; + right: 20px; + width: 20px; + height: 20px; + border: none; + } + + [dir="rtl"] .navmenu-icon { + left: 20px; + right: unset + } + + #navmenu-toggle ~ nav { + display: none; + } + + #navmenu-toggle:checked ~ nav { + display: block; + } + + #navmenu-toggle ~ article { + display: block; + } + + #navmenu-toggle:checked ~ article { + display: none; + } + + .content { + flex-direction: column; + } + + article { + padding-top: 0px; + width: 100%; + } + + aside { + padding: 1em 0.75em; + width: 100%; + } + + .footer { + display: grid; + grid-template-columns: 10px auto 10px; + } + + .footer .version { + grid-column: 2; + justify-self: start; + } + + .footer .copy { + grid-column: 2; + grid-row: 2; + justify-self: end; + } + + + @media screen and (max-width: 767px) and (orientation: landscape) { + + nav { + padding: 25px 40px; + } + + nav ul { + margin: 0 0; + } + } +} + +@media (min-width: 768px) and (max-width: 1024px) { + fluent-select::part(control) { + width: 150px; + } + + aside { + padding: 1.5em 0.75em 1em 0.75em; + width: 12rem; + } + + article { + padding-top: 0px; + width: 100%; + } +} + + +/* Surface Duo specific styling */ +@media (horizontal-viewport-segments: 2) { + + .siteheader { + grid-area: header; + display: grid; + grid-template-columns: 150px calc(env(viewport-segment-width 0 0) - 160px) 1fr; + grid-template-rows: 1fr; + align-items: center; + justify-content: flex-start; + padding: 12px 0; + background-color: var(--neutral-layer-4); + } + + .siteheader a { + padding: 0px 15px; + color: var(--neutral-foreground-rest); + } + + .siteheader .logo { + grid-column: 1; + width: 108px; + height: 23px; + /*padding: 0 30px;*/ + } + + main { + display: grid; + grid-template-columns: env(viewport-segment-width 0 0) 1fr; + grid-template-rows: repeat(0, 1fr); + } + + nav { + grid-column: 1; + width: env(viewport-segment-width 0 0) !important; + } + + .content { + display: grid; + grid-template-columns: auto; + } + + aside { + grid-area: 2 / 2 / 3 / 3; + padding: 1.5em 0.75em 1em 0.75em; + margin-inline-start: calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0)); /* hinge width */ + margin-inline-end: calc(100% - env(viewport-segment-left 1 0)); + width: auto; + } + + article { + grid-area: 1 / 2 / 2 / 3; + padding-top: 0px; + margin-inline-start: calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0)); /* hinge width */ + margin-inline-end: calc(100% - env(viewport-segment-left 1 0)); + width: auto; + } +} + + + +@media (min-width: 800px) { + .fluent-dialog-main { + --dialog-width: 340px; + } +} + + +@media (max-width: 480px) { + .fluent-dialog-main { + --dialog-width: 100vw; + } +} diff --git a/src/Web/WebAdmin.Shared/wwwroot/js/CacheStorageAccessor.js b/src/Web/WebAdmin.Shared/wwwroot/js/CacheStorageAccessor.js new file mode 100644 index 0000000000000000000000000000000000000000..507beee1a138bad9d4e16be60f705f2903135a43 --- /dev/null +++ b/src/Web/WebAdmin.Shared/wwwroot/js/CacheStorageAccessor.js @@ -0,0 +1,91 @@ +async function openCacheStorage() { + try { + return await window.caches.open("WebAdmin"); + } catch (err) { + return undefined; + } +} + +function createRequest(url, method, body = "") { + const requestInit = { + method: method + }; + + if (body != "") { + requestInit.body = body; + } + + const request = new Request(url, requestInit); + + return request; +} + +export async function put(url, method, body = "", responseString) { + const CACHING_DURATION = 7 * 24 * 3600; + + const expires = new Date(); + expires.setSeconds(expires.getSeconds() + CACHING_DURATION); + + const cachedResponseFields = { + headers: { 'fluent-cache-expires': expires.toUTCString() }, + }; + + const cache = await openCacheStorage(); + if (cache != null) { + + const request = createRequest(url, method, body); + const response = new Response(responseString, cachedResponseFields); + + await cache.put(request, response); + } +} + +export async function get(url, method, body = "") { + const cache = await openCacheStorage(); + if (cache == null) { + return ""; + } + + const request = createRequest(url, method, body); + const response = await cache.match(request); + + if (response == null) { + return ""; + } else { + const expirationDate = Date.parse(response.headers.get("cache-expires")); + const now = new Date(); + // Check it is not already expired and return from the cache + if (expirationDate > now) { + const result = await response.text(); + + return result; + } + } + + return ""; +} + +export async function remove(url, method, body = "") { + const cache = await openCacheStorage(); + + if (cache != null) { + const request = createRequest(url, method, body); + await cache.delete(request); + } +} + +export async function removeAll() { + const cache = await openCacheStorage(); + + if (cache != null) { + cache.keys().then(function(names) { + for (let name of names) + cache.delete(name); + }); + //let requests = await cache.keys(); + + //for (let request in requests) { + // await cache.delete(request); + //} + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin/Components/App.razor b/src/Web/WebAdmin/Components/App.razor new file mode 100644 index 0000000000000000000000000000000000000000..c825426786b8a4bb396e5fd7c99c65093cb74d8e --- /dev/null +++ b/src/Web/WebAdmin/Components/App.razor @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Web/WebAdmin/Components/Layout/MainLayout.razor b/src/Web/WebAdmin/Components/Layout/MainLayout.razor new file mode 100644 index 0000000000000000000000000000000000000000..653dbdd0694b4ba7810180cea3dce7052f8562e7 --- /dev/null +++ b/src/Web/WebAdmin/Components/Layout/MainLayout.razor @@ -0,0 +1,26 @@ +@inherits LayoutComponentBase + + + + WebAdmin + + + + +
+ @Body +
+
+
+ + Documentation and demos + + About Blazor + +
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/src/Web/WebAdmin/Components/Pages/Error.razor b/src/Web/WebAdmin/Components/Pages/Error.razor new file mode 100644 index 0000000000000000000000000000000000000000..576cc2d2f4db1df9d16532432880ea6e0bfbc001 --- /dev/null +++ b/src/Web/WebAdmin/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/Web/WebAdmin/Components/Pages/Home.razor b/src/Web/WebAdmin/Components/Pages/Home.razor new file mode 100644 index 0000000000000000000000000000000000000000..50caea5c113fb44addbaac92e370f91a67303a6a --- /dev/null +++ b/src/Web/WebAdmin/Components/Pages/Home.razor @@ -0,0 +1,123 @@ +@page "/" +@inject IStringLocalizer L +@inject AdminConfiguration AdminConfiguration +@inject IToastService ToastService +@inject NavigationManager NavigationManager + +@L["PageTitle"] - @AdminConfiguration.PageTitle + +

Hello, world!

+ +Welcome to new Blazor Web App Admin + + + +

MessageBar

+ + + 当你听到一些人对于精致的概念模型侃侃而谈,请保持清醒。 + + + + 物换星移 泥牛入海
黑暗好像 一颗巨石 按在胸口
独脚大盗 + 百万富翁 摸爬滚打
黑暗好像 一颗巨石 按在胸口 +
+ + + 可以照亮最黑暗的夜,也可以燃尽一切过往。
+ 在时间的长河里,我们都是匆匆的过客,留下的只有脚印和故事。
+ 一颗心被封存太久,就会生锈,忘记了如何跳动。
+ 命运之轮不停转动,我们不过是其中的一粒尘埃,飘忽不定。
+
+ + + 不会苦一辈子,但总有苦的一段时间。
+ 当我们站得太高时,忘记了脚下的路有多远。
+ 每个人的心里都有一座孤岛,时常有潮水上涨,也有退潮时刻。
+
+ + + 都有下坡的一天。
+ 当我们把昨天抛在身后,明天的路也会变得更加宽广。
+ 有些事,可以预见未来,但无法阻止发生。
+
+ + + 不是一个终点,它在于持续的努力而非偶然的机遇。 + +
+ +

Toast

+ + + + + 显示成功 + + + + 显示警告 + + + + 显示错误 + + + + 显示信息 + + + + 显示进度 + + + + 显示上传 + + + + 显示下载 + + + + 展示活动 + + + + 显示提及 + + + + 显示自定义 + + + + + 无图标 + + + (this, HandleTopAction)))"> + 有 Action + + + + 自定义超时 + + + + + 长时间 成功 + + + + +
+ +@code { + + private void HandleTopAction(ToastResult result) + { + Console.WriteLine("Toast clicked"); + } + +} \ No newline at end of file diff --git a/src/Web/WebAdmin/Components/Pages/Weather.razor b/src/Web/WebAdmin/Components/Pages/Weather.razor new file mode 100644 index 0000000000000000000000000000000000000000..d1e493bab53a5bc0552c9b6d6448e515d96284c9 --- /dev/null +++ b/src/Web/WebAdmin/Components/Pages/Weather.razor @@ -0,0 +1,65 @@ +@page "/weather" +@inject IStringLocalizer L +@inject AdminConfiguration AdminConfiguration +@attribute [StreamRendering] + +@L["PageTitle"] - @AdminConfiguration.PageTitle + +

Weather

+ +

该组件用于显示数据。

+ + +

DataGrid

+ + + + + + + + + + +
+ + + + +@code { + private IQueryable? forecasts; + + protected override async Task OnInitializedAsync() + { + await OnQueryAsync(); + + StateHasChanged(); + } + public async Task OnRefreshAsync() + { + await OnQueryAsync(); + + return true; + } + private async Task OnQueryAsync() + { + await Task.Delay(Random.Shared.Next(400, 800)); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).AsQueryable(); + } + + private class WeatherForecast + { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} diff --git a/src/Web/WebAdmin/Components/Routes.razor b/src/Web/WebAdmin/Components/Routes.razor new file mode 100644 index 0000000000000000000000000000000000000000..c00618981e49174924dacedfaa2fbe78b431007f --- /dev/null +++ b/src/Web/WebAdmin/Components/Routes.razor @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Web/WebAdmin/Components/_Imports.razor b/src/Web/WebAdmin/Components/_Imports.razor new file mode 100644 index 0000000000000000000000000000000000000000..4e386191fc2c77a4f35c1b0154a9a3f1ff2b1693 --- /dev/null +++ b/src/Web/WebAdmin/Components/_Imports.razor @@ -0,0 +1,15 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.Localization +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.JSInterop +@using WebAdmin +@using WebAdmin.Client +@using WebAdmin.Client.Layout +@using WebAdmin.Components +@using WebAdmin.Shared.Configurations diff --git a/src/Web/WebAdmin/Controllers/CultureController.cs b/src/Web/WebAdmin/Controllers/CultureController.cs new file mode 100644 index 0000000000000000000000000000000000000000..06644bd1a613fa4bf537fbab47f8f64e7fab9d89 --- /dev/null +++ b/src/Web/WebAdmin/Controllers/CultureController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc; + +[Route("[controller]/[action]")] +public class CultureController : Controller +{ + public IActionResult SetCulture(string culture, string redirectUri) + { + if (culture != null) + { + HttpContext.Response.Cookies.Append( + CookieRequestCultureProvider.DefaultCookieName, + CookieRequestCultureProvider.MakeCookieValue( + new RequestCulture(culture, culture))); + } + + return LocalRedirect(redirectUri); + } +} \ No newline at end of file diff --git a/src/Web/WebAdmin/Program.cs b/src/Web/WebAdmin/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..7f7b80f6da0099916f70cd6f952c18ac045e0369 --- /dev/null +++ b/src/Web/WebAdmin/Program.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using Microsoft.FluentUI.AspNetCore.Components; +using WebAdmin.Components; +using WebAdmin.Shared.Configurations; +using WebAdmin.Shared.Infrastructure; + +var builder = WebApplication.CreateBuilder(args); + +var adminUiOptions = new AdminUiOptions(); + +adminUiOptions.BindConfiguration(builder.Configuration); +builder.Services.AddSingleton(adminUiOptions); +builder.Services.AddSingleton(adminUiOptions.Admin); +builder.Services.AddSingleton(adminUiOptions.Culture); +builder.Services.AddSingleton(adminUiOptions.Http); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddLocalization(opts => opts.ResourcesPath = ConfigurationConsts.ResourcesPath); +builder.Services.AddAdminUiServerServices(); +builder.Services.AddFluentUIComponents(); +builder.Services.AddControllers(); +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.MapControllers(); +app.UseRequestLocalization(options => +{ + var cultureConfiguration = adminUiOptions.Culture; + var supportedCultureCodes = + (cultureConfiguration?.Cultures?.Count > 0 + ? cultureConfiguration.Cultures.Intersect(CultureConfiguration.AvailableCultures) + : CultureConfiguration.AvailableCultures).ToArray(); + if (!supportedCultureCodes.Any()) + supportedCultureCodes = CultureConfiguration.AvailableCultures; + var defaultCultureCode = string.IsNullOrEmpty(cultureConfiguration?.DefaultCulture) + ? CultureConfiguration.DefaultRequestCulture + : cultureConfiguration?.DefaultCulture; + if (!supportedCultureCodes.Contains(defaultCultureCode)) + defaultCultureCode = supportedCultureCodes.FirstOrDefault(); + + options.AddSupportedCultures(supportedCultureCodes) + .AddSupportedUICultures(supportedCultureCodes) + .SetDefaultCulture(defaultCultureCode!); +}); +app.UseStaticFiles(); +app.UseAntiforgery(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(WebAdmin.Client._Imports).Assembly); +app.Run(); diff --git a/src/Web/WebAdmin/Properties/launchSettings.json b/src/Web/WebAdmin/Properties/launchSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..8e35d658144e16adf860744dda883c3dc11804f3 --- /dev/null +++ b/src/Web/WebAdmin/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7273;http://localhost:5172", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/src/Web/WebAdmin/Resources/Components/Pages/CustomerList.en.resx b/src/Web/WebAdmin/Resources/Components/Pages/CustomerList.en.resx new file mode 100644 index 0000000000000000000000000000000000000000..585e2a16c913f5c343fab3dec260d6b5dc1336e9 --- /dev/null +++ b/src/Web/WebAdmin/Resources/Components/Pages/CustomerList.en.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Customer List + + \ No newline at end of file diff --git a/src/Web/WebAdmin/Resources/Components/Pages/CustomerList.zh-Hans.resx b/src/Web/WebAdmin/Resources/Components/Pages/CustomerList.zh-Hans.resx new file mode 100644 index 0000000000000000000000000000000000000000..439fcda38cbfaaa70d7642319041db32069a8baf --- /dev/null +++ b/src/Web/WebAdmin/Resources/Components/Pages/CustomerList.zh-Hans.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 会员列表 + + \ No newline at end of file diff --git a/src/Web/WebAdmin/Resources/Components/Pages/Home.en.resx b/src/Web/WebAdmin/Resources/Components/Pages/Home.en.resx new file mode 100644 index 0000000000000000000000000000000000000000..d2a88d150f0a1d5ca3c3e446512de8f2d59230f9 --- /dev/null +++ b/src/Web/WebAdmin/Resources/Components/Pages/Home.en.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Home + + \ No newline at end of file diff --git a/src/Web/WebAdmin/Resources/Components/Pages/Home.zh-Hans.resx b/src/Web/WebAdmin/Resources/Components/Pages/Home.zh-Hans.resx new file mode 100644 index 0000000000000000000000000000000000000000..6aff071cb35e0e98d858ffbebceb973a544655a6 --- /dev/null +++ b/src/Web/WebAdmin/Resources/Components/Pages/Home.zh-Hans.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 首页 + + \ No newline at end of file diff --git a/src/Web/WebAdmin/Resources/Components/Pages/Weather.en.resx b/src/Web/WebAdmin/Resources/Components/Pages/Weather.en.resx new file mode 100644 index 0000000000000000000000000000000000000000..ce9675e8de0739018758c0a2cf44731807509157 --- /dev/null +++ b/src/Web/WebAdmin/Resources/Components/Pages/Weather.en.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Weather + + \ No newline at end of file diff --git a/src/Web/WebAdmin/Resources/Components/Pages/Weather.zh-Hans.resx b/src/Web/WebAdmin/Resources/Components/Pages/Weather.zh-Hans.resx new file mode 100644 index 0000000000000000000000000000000000000000..f3223f758b89b69c6f4df03e34a49d305dfed023 --- /dev/null +++ b/src/Web/WebAdmin/Resources/Components/Pages/Weather.zh-Hans.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 天气 + + \ No newline at end of file diff --git a/src/Web/WebAdmin/WebAdmin.csproj b/src/Web/WebAdmin/WebAdmin.csproj new file mode 100644 index 0000000000000000000000000000000000000000..3c46fc8d39ecf6b3251411e7bfb170c33d2eaed3 --- /dev/null +++ b/src/Web/WebAdmin/WebAdmin.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + Designer + + + diff --git a/src/Web/WebAdmin/WebAdmin.xml b/src/Web/WebAdmin/WebAdmin.xml new file mode 100644 index 0000000000000000000000000000000000000000..5be91c255c32a201399114afebdf1807cda8ed09 --- /dev/null +++ b/src/Web/WebAdmin/WebAdmin.xml @@ -0,0 +1,8 @@ + + + + WebAdmin + + + + diff --git a/src/Web/WebAdmin/appsettings.Development.json b/src/Web/WebAdmin/appsettings.Development.json new file mode 100644 index 0000000000000000000000000000000000000000..01daaa3b7154d27cb2a005600a43a291192a53e7 --- /dev/null +++ b/src/Web/WebAdmin/appsettings.Development.json @@ -0,0 +1,49 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + + "DetailedErrors": true, + + "AllowedHosts": "*", + + "ConnectionStrings": { + "Catalog": "Server=192.168.8.112;Port=5432;Database=dapr_catalog;User Id=dapr;Password=Local@Db;Pooling=true;MaxPoolSize=100;", + "Identity": "Server=192.168.8.112;Port=5432;Database=dapr_identity;User Id=dapr;Password=Local@Db;Pooling=true;MaxPoolSize=100;", + "Ordering": "Server=192.168.8.112;Port=5432;Database=dapr_ordering;User Id=dapr;Password=Local@Db;Pooling=true;MaxPoolSize=100;" + }, + + "AdminConfiguration": { + "PageTitle": "管理后台", + "FaviconUri": "~/favicon.ico", + "AdminRedirectUri": "https://localhost:44303/signin-oidc", + "IdentityServerBaseUrl": "https://localhost:44310", + "AdminCookieName": "IdentityServerAdmin", + "AdminCookieExpiresUtcHours": 12, + "RequireHttpsMetadata": false, + "TokenValidationClaimName": "name", + "TokenValidationClaimRole": "role", + "ClientId": "web_admin", + "ClientSecret": "3E220EE1-C134-4A0C-9ADF-73E303AA9FA8", + "OidcResponseType": "code", + "Scopes": [ + "openid", + "profile", + "email", + "roles" + ], + "AdministrationRole": "Administrator" + }, + + "CultureConfiguration": { + "Cultures": [ "en", "zh-Hans" ], + "DefaultCulture": "zh-Hans" + }, + + "HttpConfiguration": { + "BasePath": "" + } +} diff --git a/src/Web/WebAdmin/appsettings.json b/src/Web/WebAdmin/appsettings.json new file mode 100644 index 0000000000000000000000000000000000000000..10f68b8c8b4f796baf8ddeee7551b6a52b9437cc --- /dev/null +++ b/src/Web/WebAdmin/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Web/WebAdmin/wwwroot/app.css b/src/Web/WebAdmin/wwwroot/app.css new file mode 100644 index 0000000000000000000000000000000000000000..50016b29019d47bb67d5587dd4743645a5c77505 --- /dev/null +++ b/src/Web/WebAdmin/wwwroot/app.css @@ -0,0 +1,177 @@ +@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; + +body { + --body-font: "Segoe UI Variable", "Segoe UI", sans-serif; + font-family: var(--body-font); + font-size: var(--type-ramp-base-font-size); + line-height: var(--type-ramp-base-line-height); + margin: 0; +} + +.navmenu-icon { + display: none; +} + +.main { + min-height: calc(100dvh - 86px); + color: var(--neutral-foreground-rest); + align-items: stretch !important; +} + +.body-content { + align-self: stretch; + height: unset !important; + display: flex; +} + +.content { + padding: 0.5rem 1.5rem; + align-self: stretch !important; + width: 100%; +} + +.manage { + width: 100dvw; +} + +footer { + background: var(--neutral-layer-4); + color: var(--neutral-foreground-rest); + align-items: center; + padding: 10px 10px; +} + + footer a { + color: var(--neutral-foreground-rest); + text-decoration: none; + } + + footer a:focus { + outline: 1px dashed; + outline-offset: 3px; + } + + footer a:hover { + text-decoration: underline; + } + +.alert { + border: 1px dashed var(--accent-fill-rest); + padding: 5px; +} + + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + margin: 20px 0; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::before { + content: "An error has occurred. " + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +@media (max-width: 600px) { + .main { + flex-direction: column !important; + row-gap: 0 !important; + } + + nav.sitenav { + width: 100%; + height: 100%; + } + + #main-menu { + width: 100% !important; + } + + #main-menu > div:first-child:is(.expander) { + display: none; + } + + .navmenu { + width: 100%; + } + + #navmenu-toggle { + appearance: none; + } + + #navmenu-toggle ~ nav { + display: none; + } + + #navmenu-toggle:checked ~ nav { + display: block; + } + + .navmenu-icon { + cursor: pointer; + z-index: 10; + display: block; + position: absolute; + top: 15px; + right: 20px; + width: 20px; + height: 20px; + border: none; + } +} diff --git a/src/Web/WebAdmin/wwwroot/favicon.ico b/src/Web/WebAdmin/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e189d8e579e552ab2b3c82a949bccbc5ea00f3ed Binary files /dev/null and b/src/Web/WebAdmin/wwwroot/favicon.ico differ diff --git a/test/DaprTool.AbstractionsTest/Base/DbDependencySetupFixture.cs b/test/DaprTool.AbstractionsTest/Base/DbDependencySetupFixture.cs index 6ad1e79bcc7cee0e9040a18c67fc7a2124947b0f..87b212a3905e75b2a0d0c5ce726800d48861218a 100644 --- a/test/DaprTool.AbstractionsTest/Base/DbDependencySetupFixture.cs +++ b/test/DaprTool.AbstractionsTest/Base/DbDependencySetupFixture.cs @@ -11,7 +11,7 @@ public class DbDependencySetupFixture : DependencySetupFixture { // 注册 业务数据库 ServiceCollection.AddLogging(); - ServiceCollection.AddAppDataConnection(Configuration); + ServiceCollection.AddOrderAppDataConnection(Configuration); } } @@ -25,7 +25,7 @@ public class ObserverDependencySetupFixture : DependencySetupFixture // 注册 MediatR ServiceCollection.AddLogging(); ServiceCollection.AddAppMediators(); - ServiceCollection.AddAppDataConnection(Configuration); + ServiceCollection.AddOrderAppDataConnection(Configuration); } }