Skip to content
1 change: 0 additions & 1 deletion src/BootstrapBlazor.Server/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
<Link Href="_content/BootstrapBlazor/css/motronic.min.css" />
<Link Href="BootstrapBlazor.Server.styles.css" />
<Link Href="css/site.css" />
<ThemeLoader></ThemeLoader>
<HeadOutlet @rendermode="new InteractiveServerRenderMode(false)" />
<title>@Localizer["Title"]</title>
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
18 changes: 3 additions & 15 deletions src/BootstrapBlazor.Server/Controllers/Api/LoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,18 @@
namespace BootstrapBlazor.Server.Controllers.Api;

/// <summary>
///
/// 登录控制器
/// </summary>
[Route("api/[controller]")]
[AllowAnonymous]
[ApiController]
public class LoginController : ControllerBase
{
/// <summary>
///
/// 认证方法
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[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 = "用户名或密码错误" });
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,19 @@ public partial class ThemeProvider
/// 获得/设置 主题切换回调方法
/// </summary>
[Parameter]
public Func<string, Task>? OnThemeChangedAsync { get; set; }
public Func<ThemeValue, Task>? OnThemeChangedAsync { get; set; }

/// <summary>
/// 主题类型
/// </summary>
[Parameter]
public ThemeValue ThemeValue { get; set; } = ThemeValue.UseLocalStorage;

/// <summary>
/// 主题类型改变回调方法
/// </summary>
[Parameter]
public EventCallback<ThemeValue> ThemeValueChanged { get; set; }

[Inject, NotNull]
private IIconTheme? IconTheme { get; set; }
Expand Down Expand Up @@ -107,16 +119,21 @@ protected override void OnParametersSet()
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, OnThemeChangedAsync != null ? nameof(OnThemeChanged) : null);
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, ThemeValue, nameof(OnThemeChanged));

/// <summary>
/// JavaScript 回调方法
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[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);
Expand Down
75 changes: 47 additions & 28 deletions src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
34 changes: 34 additions & 0 deletions src/BootstrapBlazor/Components/ThemeProvider/ThemeValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Argo Zhang ([email protected]). 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;

/// <summary>
/// 主题选项
/// </summary>
[JsonEnumConverter(true)]
public enum ThemeValue
{
/// <summary>
/// 自动
/// </summary>
Auto,

/// <summary>
/// 明亮主题
/// </summary>
Light,

/// <summary>
/// 暗黑主题
/// </summary>
Dark,

/// <summary>
/// 使用本地保存选项
/// </summary>
UseLocalStorage,
}
24 changes: 8 additions & 16 deletions src/BootstrapBlazor/Converter/JsonEnumConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,26 @@ namespace BootstrapBlazor.Core.Converter;
/// </summary>
public class JsonEnumConverter : JsonConverterAttribute
{
/// <summary>
/// 构造函数
/// </summary>
public JsonEnumConverter() : base()
{

}

/// <summary>
/// 构造函数
/// </summary>
/// <param name="camelCase">Optional naming policy for writing enum values.</param>
/// <param name="allowIntegerValues">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.</param>
public JsonEnumConverter(bool camelCase, bool allowIntegerValues = true) : this()
public JsonEnumConverter(bool camelCase = false, bool allowIntegerValues = true)
{
CamelCase = camelCase;
AllowIntegerValues = allowIntegerValues;
_camelCase = camelCase;
_allowIntegerValues = allowIntegerValues;
}

/// <summary>
/// naming policy for writing enum values
/// </summary>
public bool CamelCase { get; private set; }
private readonly bool _camelCase;

/// <summary>
/// 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
/// </summary>
public bool AllowIntegerValues { get; private set; } = true;
private readonly bool _allowIntegerValues;

/// <summary>
/// <inheritdoc/>
Expand All @@ -48,9 +40,9 @@ public JsonEnumConverter(bool camelCase, bool allowIntegerValues = true) : this(
/// <returns></returns>
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;
}
}
4 changes: 3 additions & 1 deletion src/BootstrapBlazor/wwwroot/modules/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,9 @@ export function getTheme() {
}

export function saveTheme(theme) {
localStorage.setItem('theme', theme)
if (localStorage) {
localStorage.setItem('theme', theme);
}
}

export function getAutoThemeValue() {
Expand Down
24 changes: 18 additions & 6 deletions test/UnitTest/Components/ThemeProviderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,29 @@ public void ThemeProvider_Ok()
[Fact]
public async Task OnThemeChanged_Ok()
{
var name = "";
var v = ThemeValue.Auto;
var cut = Context.RenderComponent<ThemeProvider>(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<ThemeProvider>(pb =>
{
pb.Add(a => a.ThemeValue, ThemeValue.Light);
pb.Add(a => a.ThemeValueChanged, EventCallback.Factory.Create<ThemeValue>(this, val => v = val));
});
await cut.Instance.OnThemeChanged(ThemeValue.Dark);
Assert.Equal(ThemeValue.Dark, v);
}
}