diff --git a/src/BootstrapBlazor.Server/Components/App.razor b/src/BootstrapBlazor.Server/Components/App.razor index 14fc245376e..08f502ccaef 100644 --- a/src/BootstrapBlazor.Server/Components/App.razor +++ b/src/BootstrapBlazor.Server/Components/App.razor @@ -23,7 +23,6 @@ - @Localizer["Title"] diff --git a/src/BootstrapBlazor.Server/Components/Components/Header.razor.cs b/src/BootstrapBlazor.Server/Components/Components/Header.razor.cs index e6b4ca0fe85..cf617bae761 100644 --- a/src/BootstrapBlazor.Server/Components/Components/Header.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Components/Header.razor.cs @@ -57,5 +57,5 @@ protected override void OnInitialized() _versionString = $"v{PackageVersionService.Version}"; } - private Task OnThemeChangedAsync(string themeName) => InvokeVoidAsync("updateTheme", themeName); + private Task OnThemeChangedAsync(ThemeValue themeName) => InvokeVoidAsync("updateTheme", themeName); } diff --git a/src/BootstrapBlazor.Server/Controllers/Api/LoginController.cs b/src/BootstrapBlazor.Server/Controllers/Api/LoginController.cs index 65dcbb0d749..9c0c1711d02 100644 --- a/src/BootstrapBlazor.Server/Controllers/Api/LoginController.cs +++ b/src/BootstrapBlazor.Server/Controllers/Api/LoginController.cs @@ -9,7 +9,7 @@ namespace BootstrapBlazor.Server.Controllers.Api; /// -/// +/// 登录控制器 /// [Route("api/[controller]")] [AllowAnonymous] @@ -17,22 +17,10 @@ namespace BootstrapBlazor.Server.Controllers.Api; public class LoginController : ControllerBase { /// - /// + /// 认证方法 /// /// /// [HttpPost] - public IActionResult Post(User user) - { - IActionResult? response; - if (user.UserName == "admin" && user.Password == "123456") - { - response = new JsonResult(new { Code = 200, Message = "登录成功" }); - } - else - { - response = new JsonResult(new { Code = 500, Message = "用户名或密码错误" }); - } - return response; - } + public IActionResult Post(User user) => user is { UserName: "admin", Password: "123456" } ? new JsonResult(new { Code = 200, Message = "登录成功" }) : new JsonResult(new { Code = 500, Message = "用户名或密码错误" }); } diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs index f5c57537be0..9c5c428a7f4 100644 --- a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs +++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs @@ -69,7 +69,19 @@ public partial class ThemeProvider /// 获得/设置 主题切换回调方法 /// [Parameter] - public Func? OnThemeChangedAsync { get; set; } + public Func? OnThemeChangedAsync { get; set; } + + /// + /// 主题类型 + /// + [Parameter] + public ThemeValue ThemeValue { get; set; } = ThemeValue.UseLocalStorage; + + /// + /// 主题类型改变回调方法 + /// + [Parameter] + public EventCallback ThemeValueChanged { get; set; } [Inject, NotNull] private IIconTheme? IconTheme { get; set; } @@ -107,7 +119,7 @@ protected override void OnParametersSet() /// /// /// - protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, OnThemeChangedAsync != null ? nameof(OnThemeChanged) : null); + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, ThemeValue, nameof(OnThemeChanged)); /// /// JavaScript 回调方法 @@ -115,8 +127,13 @@ protected override void OnParametersSet() /// /// [JSInvokable] - public async Task OnThemeChanged(string name) + public async Task OnThemeChanged(ThemeValue name) { + if (ThemeValueChanged.HasDelegate) + { + await ThemeValueChanged.InvokeAsync(name); + } + if (OnThemeChangedAsync != null) { await OnThemeChangedAsync(name); diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js index e221e4c36a8..6f22cb48456 100644 --- a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js +++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js @@ -1,37 +1,56 @@ -import { getAutoThemeValue, getPreferredTheme, setActiveTheme, switchTheme } from "../../modules/utility.js" +import { getPreferredTheme, setTheme, switchTheme } from "../../modules/utility.js" import EventHandler from "../../modules/event-handler.js" +import Data from "../../modules/data.js" -export function init(id, invoke, callback) { +export function init(id, invoke, themeValue, callback) { const el = document.getElementById(id); - if (el) { - const currentTheme = getPreferredTheme(); - const activeItem = el.querySelector(`.dropdown-item[data-bb-theme-value="${currentTheme}"]`); - if (activeItem) { - setActiveTheme(el, activeItem); - } + if (el === null) { + return; + } + + const theme = { el }; + Data.set(id, theme); + + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + EventHandler.on(darkModeMediaQuery, 'change', () => changeTheme(id)); + theme.mediaQueryList = darkModeMediaQuery; - const items = el.querySelectorAll('.dropdown-item'); - items.forEach(item => { - EventHandler.on(item, 'click', () => { - setActiveTheme(el, item); - - let theme = item.getAttribute('data-bb-theme-value'); - if (theme === 'auto') { - theme = getAutoThemeValue(); - } - switchTheme(theme, window.innerWidth, 0); - if (callback) { - invoke.invokeMethodAsync(callback, theme); - } - }); - }); + let currentTheme = themeValue; + if (currentTheme === 'useLocalStorage') { + currentTheme = getPreferredTheme(); } + setTheme(currentTheme, true); + theme.currentTheme = currentTheme; + + EventHandler.on(el, 'click', '.dropdown-item', e => { + const activeTheme = e.delegateTarget.getAttribute('data-bb-theme-value'); + theme.currentTheme = activeTheme; + switchTheme(activeTheme, window.innerWidth, 0); + if (callback) { + invoke.invokeMethodAsync(callback, activeTheme); + } + }); } export function dispose(id) { - const el = document.getElementById(id); - const items = el.querySelectorAll('.dropdown-item'); - items.forEach(item => { - EventHandler.off(item, 'click'); - }); + const theme = Data.get(id); + if (theme === null) { + return; + } + Data.remove(id); + + const { el, darkModeMediaQuery } = theme; + EventHandler.off(el, 'click'); + EventHandler.off(darkModeMediaQuery, 'change'); +} + +const changeTheme = id => { + const theme = Data.get(id); + if (theme === null) { + return; + } + + if (theme.currentTheme === 'auto') { + switchTheme('auto', window.innerWidth, 0); + } } diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeValue.cs b/src/BootstrapBlazor/Components/ThemeProvider/ThemeValue.cs new file mode 100644 index 00000000000..56b3a992e6b --- /dev/null +++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeValue.cs @@ -0,0 +1,34 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +using BootstrapBlazor.Core.Converter; + +namespace BootstrapBlazor.Components; + +/// +/// 主题选项 +/// +[JsonEnumConverter(true)] +public enum ThemeValue +{ + /// + /// 自动 + /// + Auto, + + /// + /// 明亮主题 + /// + Light, + + /// + /// 暗黑主题 + /// + Dark, + + /// + /// 使用本地保存选项 + /// + UseLocalStorage, +} diff --git a/src/BootstrapBlazor/Converter/JsonEnumConverter.cs b/src/BootstrapBlazor/Converter/JsonEnumConverter.cs index 0fb3461aa72..4e7564cfc86 100644 --- a/src/BootstrapBlazor/Converter/JsonEnumConverter.cs +++ b/src/BootstrapBlazor/Converter/JsonEnumConverter.cs @@ -12,34 +12,26 @@ namespace BootstrapBlazor.Core.Converter; /// public class JsonEnumConverter : JsonConverterAttribute { - /// - /// 构造函数 - /// - public JsonEnumConverter() : base() - { - - } - /// /// 构造函数 /// /// Optional naming policy for writing enum values. /// True to allow undefined enum values. When true, if an enum value isn't defined it will output as a number rather than a string. - public JsonEnumConverter(bool camelCase, bool allowIntegerValues = true) : this() + public JsonEnumConverter(bool camelCase = false, bool allowIntegerValues = true) { - CamelCase = camelCase; - AllowIntegerValues = allowIntegerValues; + _camelCase = camelCase; + _allowIntegerValues = allowIntegerValues; } /// /// naming policy for writing enum values /// - public bool CamelCase { get; private set; } + private readonly bool _camelCase; /// /// True to allow undefined enum values. When true, if an enum value isn't defined it will output as a number rather than a string /// - public bool AllowIntegerValues { get; private set; } = true; + private readonly bool _allowIntegerValues; /// /// @@ -48,9 +40,9 @@ public JsonEnumConverter(bool camelCase, bool allowIntegerValues = true) : this( /// public override JsonConverter? CreateConverter(Type typeToConvert) { - var converter = CamelCase - ? new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, AllowIntegerValues) - : new JsonStringEnumConverter(null, AllowIntegerValues); + var converter = _camelCase + ? new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, _allowIntegerValues) + : new JsonStringEnumConverter(null, _allowIntegerValues); return converter; } } diff --git a/src/BootstrapBlazor/wwwroot/modules/utility.js b/src/BootstrapBlazor/wwwroot/modules/utility.js index 33948b6a754..79ef5674607 100644 --- a/src/BootstrapBlazor/wwwroot/modules/utility.js +++ b/src/BootstrapBlazor/wwwroot/modules/utility.js @@ -716,7 +716,9 @@ export function getTheme() { } export function saveTheme(theme) { - localStorage.setItem('theme', theme) + if (localStorage) { + localStorage.setItem('theme', theme); + } } export function getAutoThemeValue() { diff --git a/test/UnitTest/Components/ThemeProviderTest.cs b/test/UnitTest/Components/ThemeProviderTest.cs index 84c0a923474..bf9816d584a 100644 --- a/test/UnitTest/Components/ThemeProviderTest.cs +++ b/test/UnitTest/Components/ThemeProviderTest.cs @@ -21,17 +21,29 @@ public void ThemeProvider_Ok() [Fact] public async Task OnThemeChanged_Ok() { - var name = ""; + var v = ThemeValue.Auto; var cut = Context.RenderComponent(pb => { - pb.Add(a => a.OnThemeChangedAsync, t => + pb.Add(a => a.OnThemeChangedAsync, val => { - name = t; + v = val; return Task.CompletedTask; }); - pb.Add(a => a.Alignment, Alignment.Center); }); - await cut.Instance.OnThemeChanged("dark"); - Assert.Equal("dark", name); + await cut.Instance.OnThemeChanged(ThemeValue.Dark); + Assert.Equal(ThemeValue.Dark, v); + } + + [Fact] + public async Task ThemeValueChanged_Ok() + { + var v = ThemeValue.Auto; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.ThemeValue, ThemeValue.Light); + pb.Add(a => a.ThemeValueChanged, EventCallback.Factory.Create(this, val => v = val)); + }); + await cut.Instance.OnThemeChanged(ThemeValue.Dark); + Assert.Equal(ThemeValue.Dark, v); } }