diff --git a/src/BootstrapBlazor.Server/Components/Components/Header.razor b/src/BootstrapBlazor.Server/Components/Components/Header.razor
index f54206d314c..e550c49293b 100644
--- a/src/BootstrapBlazor.Server/Components/Components/Header.razor
+++ b/src/BootstrapBlazor.Server/Components/Components/Header.razor
@@ -41,9 +41,6 @@
@DownloadText
@_versionString
-
-
-
-
+
diff --git a/src/BootstrapBlazor.Server/Components/Components/Header.razor.css b/src/BootstrapBlazor.Server/Components/Components/Header.razor.css
index 87b876895b4..41e576bee06 100644
--- a/src/BootstrapBlazor.Server/Components/Components/Header.razor.css
+++ b/src/BootstrapBlazor.Server/Components/Components/Header.razor.css
@@ -46,6 +46,18 @@
white-space: nowrap;
}
+::deep .bb-theme-mode .dropdown-toggle {
+ color: var(--bs-navbar-color);
+}
+
+::deep .bb-theme-mode .dropdown-menu li {
+ padding: 0 4px;
+}
+
+ ::deep .bb-theme-mode .dropdown-menu li button {
+ border-radius: var(--bs-border-radius);
+ }
+
@media (min-width: 768px) {
.navbar-header {
position: sticky;
diff --git a/src/BootstrapBlazor.Server/Components/Components/Header.razor.js b/src/BootstrapBlazor.Server/Components/Components/Header.razor.js
index 75ecc64f202..7ff62f2fdc6 100644
--- a/src/BootstrapBlazor.Server/Components/Components/Header.razor.js
+++ b/src/BootstrapBlazor.Server/Components/Components/Header.razor.js
@@ -1,5 +1,4 @@
-import { getPreferredTheme, setTheme } from "../../_content/BootstrapBlazor/modules/theme.js"
-import EventHandler from "../../_content/BootstrapBlazor/modules/event-handler.js"
+import EventHandler from "../../_content/BootstrapBlazor/modules/event-handler.js"
export function init() {
const scrollTop = () => (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop
@@ -9,27 +8,12 @@ export function init() {
const currentScrollTop = scrollTop()
if (currentScrollTop > prevScrollTop) {
items.forEach(item => item.classList.add('hide'))
- } else {
+ }
+ else {
items.forEach(item => item.classList.remove('hide'))
}
prevScrollTop = currentScrollTop
})
-
- const themeElements = document.querySelectorAll('.icon-theme');
- if (themeElements) {
- themeElements.forEach(el => {
- EventHandler.on(el, 'click', e => {
- let theme = getPreferredTheme();
- if (theme === 'dark') {
- theme = 'light';
- }
- else {
- theme = 'dark';
- }
- setTheme(theme);
- });
- });
- }
}
export function dispose() {
diff --git a/src/BootstrapBlazor.Server/wwwroot/lib/theme.js b/src/BootstrapBlazor.Server/wwwroot/lib/theme.js
index a49a799de70..dedda5164c0 100644
--- a/src/BootstrapBlazor.Server/wwwroot/lib/theme.js
+++ b/src/BootstrapBlazor.Server/wwwroot/lib/theme.js
@@ -1,3 +1,3 @@
-import { getPreferredTheme, setTheme } from "../../_content/BootstrapBlazor/modules/theme.js"
+import { getPreferredTheme, setTheme } from "../../_content/BootstrapBlazor/modules/utility.js"
-setTheme(getPreferredTheme())
+setTheme(getPreferredTheme(), false)
diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj
index 57ea9b6f887..edfd31e7a67 100644
--- a/src/BootstrapBlazor/BootstrapBlazor.csproj
+++ b/src/BootstrapBlazor/BootstrapBlazor.csproj
@@ -1,7 +1,7 @@
- 8.5.2
+ 8.5.3
diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor
new file mode 100644
index 00000000000..e471eab25bd
--- /dev/null
+++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor
@@ -0,0 +1,32 @@
+@namespace BootstrapBlazor.Components
+@inherits BootstrapModuleComponentBase
+@attribute [BootstrapModuleAutoLoader]
+
+
+
+
+
diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs
new file mode 100644
index 00000000000..ddf8b80d715
--- /dev/null
+++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.cs
@@ -0,0 +1,82 @@
+// 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 Microsoft.Extensions.Localization;
+
+namespace BootstrapBlazor.Components;
+
+///
+/// ThemeProvider 组件
+///
+public partial class ThemeProvider
+{
+ ///
+ /// 获得/设置 自动模式图标 默认 null
+ ///
+ [Parameter]
+ public string? AutoModeIcon { get; set; }
+
+ ///
+ /// 获得/设置 自动模式文本 默认 null 未设置使用本地化资源
+ ///
+ [Parameter]
+ public string? AutoModeText { get; set; }
+
+ ///
+ /// 获得/设置 暗黑模式图标 默认 null
+ ///
+ [Parameter]
+ public string? DarkModeIcon { get; set; }
+
+ ///
+ /// 获得/设置 暗黑模式文本 默认 null 未设置使用本地化资源
+ ///
+ [Parameter]
+ public string? DarkModeText { get; set; }
+
+ ///
+ /// 获得/设置 明亮模式图标 默认 null
+ ///
+ [Parameter]
+ public string? LightModeIcon { get; set; }
+
+ ///
+ /// 获得/设置 明亮模式文本 默认 null 未设置使用本地化资源
+ ///
+ [Parameter]
+ public string? LightModeText { get; set; }
+
+ ///
+ /// 获得/设置 当前选中模式图标 默认 null
+ ///
+ [Parameter]
+ public string? ActiveIcon { get; set; }
+
+ [Inject, NotNull]
+ private IIconTheme? IconTheme { get; set; }
+
+ [Inject, NotNull]
+ private IStringLocalizer? Localizer { get; set; }
+
+ private string? ClassString => CssBuilder.Default("dropdown bb-theme-mode")
+ .AddClassFromAttributes(AdditionalAttributes)
+ .Build();
+
+ ///
+ ///
+ ///
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+
+ AutoModeIcon ??= IconTheme.GetIconByKey(ComponentIcons.ThemeProviderAutoModeIcon);
+ DarkModeIcon ??= IconTheme.GetIconByKey(ComponentIcons.ThemeProviderDarkModeIcon);
+ LightModeIcon ??= IconTheme.GetIconByKey(ComponentIcons.ThemeProviderLightModeIcon);
+ ActiveIcon ??= IconTheme.GetIconByKey(ComponentIcons.ThemeProviderActiveModeIcon);
+
+ AutoModeText ??= Localizer[nameof(AutoModeText)];
+ DarkModeText ??= Localizer[nameof(DarkModeText)];
+ LightModeText ??= Localizer[nameof(LightModeText)];
+ }
+}
diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js
new file mode 100644
index 00000000000..090107e4913
--- /dev/null
+++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js
@@ -0,0 +1,35 @@
+import { getAutoThemeValue, getPreferredTheme, setActiveTheme, setTheme, saveTheme } from "../../modules/utility.js"
+import EventHandler from "../../modules/event-handler.js"
+
+export function init(id) {
+ 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);
+ }
+
+ 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();
+ }
+ setTheme(theme, false);
+ saveTheme(theme);
+ });
+ });
+ }
+}
+
+export function dispose(id) {
+ const el = document.getElementById(id);
+ const items = el.querySelectorAll('.dropdown-item');
+ items.forEach(item => {
+ EventHandler.off(item, 'click');
+ });
+}
diff --git a/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.scss b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.scss
new file mode 100644
index 00000000000..40e8677f986
--- /dev/null
+++ b/src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.scss
@@ -0,0 +1,33 @@
+.bb-theme-mode {
+ --bb-theme-mode-width: 128px;
+ --bb-theme-mode-label-width: 40px;
+ --bb-theme-mode-icon-width: 14px;
+ --bb-theme-mode-item-margin-top: 2px;
+
+ .bb-theme-mode-active {
+ width: var(--bb-theme-mode-icon-width);
+ display: inline-block;
+ }
+
+ .dropdown-menu {
+ --bs-dropdown-min-width: var(--bb-theme-mode-width);
+
+ li:not(:first-child) {
+ margin-top: var(--bb-theme-mode-item-margin-top);
+ }
+
+ .dropdown-item {
+ span {
+ width: var(--bb-theme-mode-label-width);
+ }
+
+ i {
+ width: var(--bb-theme-mode-icon-width);
+ }
+
+ &:not(.active) .bb-theme-mode-check {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Enums/ComponentIcons.cs b/src/BootstrapBlazor/Enums/ComponentIcons.cs
index e56483dbd4a..5bd161aa5ad 100644
--- a/src/BootstrapBlazor/Enums/ComponentIcons.cs
+++ b/src/BootstrapBlazor/Enums/ComponentIcons.cs
@@ -782,5 +782,25 @@ public enum ComponentIcons
///
/// QueryBuilder 组件 移除按钮图标
///
- QueryBuilderRemoveIcon
+ QueryBuilderRemoveIcon,
+
+ ///
+ /// ThemeProvider 组件 自动模式图标
+ ///
+ ThemeProviderAutoModeIcon,
+
+ ///
+ /// ThemeProvider 组件 暗黑模式图标
+ ///
+ ThemeProviderDarkModeIcon,
+
+ ///
+ /// ThemeProvider 组件 明亮模式图标
+ ///
+ ThemeProviderLightModeIcon,
+
+ ///
+ /// ThemeProvider 组件 明亮模式图标
+ ///
+ ThemeProviderActiveModeIcon
}
diff --git a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
index 7687f036e82..5ffc71db1e9 100644
--- a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
+++ b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
@@ -43,6 +43,9 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
// Table 导出服务
services.TryAddScoped();
+ // 主题服务
+ services.TryAddScoped();
+
// IP 地理位置定位服务
services.TryAddSingleton();
services.AddSingleton();
diff --git a/src/BootstrapBlazor/Extensions/JSModuleExtensions.cs b/src/BootstrapBlazor/Extensions/JSModuleExtensions.cs
index 09e196220ed..10b9437f066 100644
--- a/src/BootstrapBlazor/Extensions/JSModuleExtensions.cs
+++ b/src/BootstrapBlazor/Extensions/JSModuleExtensions.cs
@@ -185,4 +185,19 @@ public static string GetTypeModuleName(this Type type)
///
/// Returns a formatted element ID
public static ValueTask GetHtml(this JSModule module, string? id = null, string? selector = null) => module.InvokeAsync("getHtml", new { id, selector });
+
+ ///
+ /// 设置主题方法
+ ///
+ /// An instance of
+ /// theme name
+ ///
+ public static ValueTask SetTheme(this JSModule module, string themeName) => module.InvokeVoidAsync("setTheme", themeName, true);
+
+ ///
+ /// 设置主题方法
+ ///
+ /// An instance of
+ ///
+ public static ValueTask GetTheme(this JSModule module) => module.InvokeAsync("getTheme");
}
diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json
index 9303cc5391b..21d20f2dd2e 100644
--- a/src/BootstrapBlazor/Locales/en.json
+++ b/src/BootstrapBlazor/Locales/en.json
@@ -356,5 +356,10 @@
"BootstrapBlazor.Components.ClockPicker": {
"AMText": "AM",
"PMText": "PM"
+ },
+ "BootstrapBlazor.Components.ThemeProvider": {
+ "AutoModeText": "Auto",
+ "DarkModeText": "Dark",
+ "LightModeText": "Light"
}
}
diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json
index 6580d5041b0..9cb887b99b7 100644
--- a/src/BootstrapBlazor/Locales/zh.json
+++ b/src/BootstrapBlazor/Locales/zh.json
@@ -356,5 +356,10 @@
"BootstrapBlazor.Components.ClockPicker": {
"AMText": "上午",
"PMText": "下午"
+ },
+ "BootstrapBlazor.Components.ThemeProvider": {
+ "AutoModeText": "自动",
+ "DarkModeText": "暗黑",
+ "LightModeText": "明亮"
}
}
diff --git a/src/BootstrapBlazor/Options/IconThemeOptions.cs b/src/BootstrapBlazor/Options/IconThemeOptions.cs
index ce2409459f6..4e18869fd96 100644
--- a/src/BootstrapBlazor/Options/IconThemeOptions.cs
+++ b/src/BootstrapBlazor/Options/IconThemeOptions.cs
@@ -234,5 +234,10 @@ public IconThemeOptions()
{ ComponentIcons.QueryBuilderPlusIcon, "fa-solid fa-plus" },
{ ComponentIcons.QueryBuilderMinusIcon, "fa-solid fa-minus" },
{ ComponentIcons.QueryBuilderRemoveIcon, "fa-solid fa-xmark" },
+
+ { ComponentIcons.ThemeProviderAutoModeIcon, "fa-solid fa-circle-half-stroke" },
+ { ComponentIcons.ThemeProviderLightModeIcon, "fa-solid fa-sun" },
+ { ComponentIcons.ThemeProviderDarkModeIcon, "fa-solid fa-moon" },
+ { ComponentIcons.ThemeProviderActiveModeIcon, "fa-solid fa-check" }
};
}
diff --git a/src/BootstrapBlazor/Services/DefaultThemeProvider.cs b/src/BootstrapBlazor/Services/DefaultThemeProvider.cs
new file mode 100644
index 00000000000..b8afdaf06dc
--- /dev/null
+++ b/src/BootstrapBlazor/Services/DefaultThemeProvider.cs
@@ -0,0 +1,28 @@
+// 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/
+
+
+namespace BootstrapBlazor.Components;
+
+class DefaultThemeProvider(IJSRuntime jsRuntime) : IThemeProvider
+{
+ ///
+ ///
+ ///
+ ///
+ public async ValueTask SetThemeAsync(string themeName)
+ {
+ var module = await jsRuntime.LoadUtility();
+ await module.SetTheme(themeName);
+ }
+
+ ///
+ ///
+ ///
+ public async ValueTask GetThemeAsync()
+ {
+ var module = await jsRuntime.LoadUtility();
+ return await module.GetTheme();
+ }
+}
diff --git a/src/BootstrapBlazor/Services/IThemeProvider.cs b/src/BootstrapBlazor/Services/IThemeProvider.cs
new file mode 100644
index 00000000000..e05eb659c5f
--- /dev/null
+++ b/src/BootstrapBlazor/Services/IThemeProvider.cs
@@ -0,0 +1,22 @@
+// 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/
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 主题提供器接口
+///
+public interface IThemeProvider
+{
+ ///
+ /// 设置主题方法
+ ///
+ ///
+ ValueTask SetThemeAsync(string themeName);
+
+ ///
+ /// 获得当前主题方法
+ ///
+ ValueTask GetThemeAsync();
+}
diff --git a/src/BootstrapBlazor/wwwroot/modules/theme.js b/src/BootstrapBlazor/wwwroot/modules/theme.js
deleted file mode 100644
index bedfef5580c..00000000000
--- a/src/BootstrapBlazor/wwwroot/modules/theme.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const getStoredTheme = () => localStorage.getItem('theme')
-const setStoredTheme = theme => localStorage.setItem('theme', theme)
-
-export function getPreferredTheme() {
- const storedTheme = getStoredTheme()
- if (storedTheme) {
- return storedTheme
- }
-
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
-}
-
-export function setTheme(theme) {
- if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- document.documentElement.setAttribute('data-bs-theme', 'dark')
- } else {
- document.documentElement.setAttribute('data-bs-theme', theme);
- }
- setStoredTheme(theme);
-}
diff --git a/src/BootstrapBlazor/wwwroot/modules/utility.js b/src/BootstrapBlazor/wwwroot/modules/utility.js
index bc00d3d30fd..e640dc2dfb1 100644
--- a/src/BootstrapBlazor/wwwroot/modules/utility.js
+++ b/src/BootstrapBlazor/wwwroot/modules/utility.js
@@ -674,6 +674,67 @@ export function getHtml(options) {
return html;
}
+
+export function getPreferredTheme() {
+ const storedTheme = getTheme()
+ if (storedTheme) {
+ return storedTheme
+ }
+
+ return getAutoThemeValue();
+}
+
+export function getTheme() {
+ return localStorage.getItem('theme') || document.documentElement.getAttribute('data-bs-theme') || 'light';
+}
+
+export function saveTheme(theme) {
+ localStorage.setItem('theme', theme)
+}
+
+export function getAutoThemeValue() {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+}
+
+export function setTheme(theme, sync) {
+ if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ document.documentElement.setAttribute('data-bs-theme', 'dark')
+ }
+ else {
+ document.documentElement.setAttribute('data-bs-theme', theme);
+ }
+
+ if (sync === true) {
+ const providers = document.querySelectorAll('.bb-theme-mode');
+ providers.forEach(p => {
+ const activeItem = p.querySelector(`.dropdown-item[data-bb-theme-value="${theme}"]`);
+ setActiveTheme(p, activeItem)
+ })
+ saveTheme(theme);
+ }
+}
+
+export function setActiveTheme(el, activeItem) {
+ const currentTheme = el.querySelector('.active');
+ if (currentTheme) {
+ currentTheme.classList.remove('active');
+ }
+
+ if (activeItem) {
+ activeItem.classList.add('active');
+ const iconItem = activeItem.querySelector('[data-bb-theme-icon]');
+ if (iconItem) {
+ const icon = iconItem.getAttribute('data-bb-theme-icon');
+ if (icon) {
+ const toggleIcon = el.querySelector('.bb-theme-mode-active');
+ if (toggleIcon) {
+ toggleIcon.outerHTML = ``;
+ }
+ }
+ }
+ }
+}
+
export {
autoAdd,
autoRemove,
diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss
index 2ef89a3288e..c108cc95fad 100644
--- a/src/BootstrapBlazor/wwwroot/scss/components.scss
+++ b/src/BootstrapBlazor/wwwroot/scss/components.scss
@@ -90,6 +90,7 @@
@import "../../Components/Table/Table.razor.scss";
@import "../../Components/Table/TableAdvancedSortDialog.razor.scss";
@import "../../Components/Tag/Tag.razor.scss";
+@import "../../Components/ThemeProvider/ThemeProvider.razor.scss";
@import "../../Components/Timeline/Timeline.razor.scss";
@import "../../Components/Timer/Timer.razor.scss";
@import "../../Components/TimePicker/TimePicker.razor.scss";
diff --git a/test/UnitTest/Components/ThemeProviderTest.cs b/test/UnitTest/Components/ThemeProviderTest.cs
new file mode 100644
index 00000000000..8f22f423308
--- /dev/null
+++ b/test/UnitTest/Components/ThemeProviderTest.cs
@@ -0,0 +1,15 @@
+// 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/
+
+namespace UnitTest.Components;
+
+public class ThemeProviderTest : BootstrapBlazorTestBase
+{
+ [Fact]
+ public void ThemeProvider_Ok()
+ {
+ var cut = Context.RenderComponent();
+ cut.Contains("dropdown bb-theme-mode");
+ }
+}
diff --git a/test/UnitTest/Services/ThemeProviderTest.cs b/test/UnitTest/Services/ThemeProviderTest.cs
new file mode 100644
index 00000000000..de29419dd44
--- /dev/null
+++ b/test/UnitTest/Services/ThemeProviderTest.cs
@@ -0,0 +1,26 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+
+namespace UnitTest.Services;
+
+public class ThemeProviderTest : BootstrapBlazorTestBase
+{
+ [Fact]
+ public async Task SetTheme_Ok()
+ {
+ var themeProviderService = Context.Services.GetRequiredService();
+ await themeProviderService.SetThemeAsync("light");
+ }
+
+ [Fact]
+ public async Task GetTheme_Ok()
+ {
+ Context.JSInterop.Setup("getTheme").SetResult("light");
+ var themeProviderService = Context.Services.GetRequiredService();
+ var theme = await themeProviderService.GetThemeAsync();
+ Assert.Equal("light", theme);
+ }
+}