diff --git a/src/BootstrapBlazor.Server/Components/Layout/HomeLayout.razor b/src/BootstrapBlazor.Server/Components/Layout/HomeLayout.razor
index d38bedbe44c..139cca04eb4 100644
--- a/src/BootstrapBlazor.Server/Components/Layout/HomeLayout.razor
+++ b/src/BootstrapBlazor.Server/Components/Layout/HomeLayout.razor
@@ -64,6 +64,9 @@
@Localizer["Footer"]
diff --git a/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitor.cs b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitor.cs
new file mode 100644
index 00000000000..5150d24aa88
--- /dev/null
+++ b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitor.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 客户端链接组件
+///
+[BootstrapModuleAutoLoader(ModuleName = "net", JSObjectReference = true)]
+public class NetworkMonitor : BootstrapModuleComponentBase
+{
+ ///
+ /// Gets or sets the callback function that is invoked when the network state changes.
+ ///
+ [Parameter]
+ public Func
? OnNetworkStateChanged { get; set; }
+
+ ///
+ /// Gets or sets the list of indicators used for display info.
+ ///
+ [Parameter]
+ public List? Indicators { get; set; }
+
+ private NetworkMonitorState _state = new();
+
+ ///
+ ///
+ ///
+ ///
+ protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, new
+ {
+ Invoke = Interop,
+ OnlineStateChangedCallback = nameof(TriggerOnlineStateChanged),
+ OnNetworkStateChangedCallback = nameof(TriggerNetworkStateChanged),
+ Indicators
+ });
+
+ ///
+ /// JSInvoke 回调方法
+ ///
+ ///
+ [JSInvokable]
+ public async Task TriggerOnlineStateChanged(bool online)
+ {
+ _state.IsOnline = online;
+ if (OnNetworkStateChanged != null)
+ {
+ await OnNetworkStateChanged(_state);
+ }
+ }
+
+ ///
+ /// JSInvoke 回调方法
+ ///
+ ///
+ [JSInvokable]
+ public async Task TriggerNetworkStateChanged(NetworkMonitorState state)
+ {
+ // 网络状态变化回调方法
+ _state = state;
+ _state.IsOnline = true;
+ if (OnNetworkStateChanged != null)
+ {
+ await OnNetworkStateChanged(_state);
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor
new file mode 100644
index 00000000000..0cf96a79e14
--- /dev/null
+++ b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor
@@ -0,0 +1,26 @@
+@namespace BootstrapBlazor.Components
+@inherits IdComponentBase
+
+
+
+
+
+
+
+
+
NetworkType:
+
@_state.NetworkType
+
+
+
Downlink:
+
@_state.Downlink Mbps
+
+
+
RTT:
+
@_state.RTT ms
+
+
+
+
+
+
diff --git a/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.cs b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.cs
new file mode 100644
index 00000000000..5020815fbc8
--- /dev/null
+++ b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+using Microsoft.Extensions.Localization;
+
+namespace BootstrapBlazor.Components;
+
+///
+/// Represents a network monitor indicator with customizable tooltip settings.
+///
+/// This component allows you to configure the text, placement, and trigger behavior of a tooltip that
+/// appears when interacting with the network monitor indicator. The tooltip can be customized to provide additional
+/// information to users.
+public partial class NetworkMonitorIndicator
+{
+ ///
+ /// 获得/设置 Popover 弹窗标题 默认为 null
+ ///
+ [Parameter]
+ public string? Title { get; set; }
+
+ ///
+ /// 获得/设置 Popover 显示位置 默认为 Top
+ ///
+ [Parameter]
+ public Placement PopoverPlacement { get; set; } = Placement.Top;
+
+ ///
+ /// 获得/设置 Popover 触发方式 默认为 hover focus
+ ///
+ [Parameter]
+ [NotNull]
+ public string? Trigger { get; set; }
+
+ [Inject, NotNull]
+ private IStringLocalizer? Localizer { get; set; }
+
+ private NetworkMonitorState _state = new();
+ private readonly List _indicators = [];
+ private string _networkTypeString = "";
+ private string _downlinkString = "";
+ private string _rttString = "";
+
+ private string? ClassString => CssBuilder.Default("bb-nt-indicator")
+ .AddClass("bb-nt-indicator-4g", _state.NetworkType == "4g")
+ .AddClass("bb-nt-indicator-3g", _state.NetworkType == "3g")
+ .AddClass("bb-nt-indicator-2g", _state.NetworkType == "2g")
+ .AddClassFromAttributes(AdditionalAttributes)
+ .Build();
+
+ ///
+ ///
+ ///
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+
+ _indicators.Add(Id);
+ }
+
+ ///
+ ///
+ ///
+ protected override void OnParametersSet()
+ {
+ base.OnParametersSet();
+
+ Trigger ??= "hover focus";
+ Title ??= Localizer["Title"];
+ _networkTypeString = Localizer["NetworkType"];
+ _downlinkString = Localizer["Downlink"];
+ _rttString = Localizer["RTT"];
+ }
+
+ private Task OnNetworkStateChanged(NetworkMonitorState state)
+ {
+ _state = state;
+ StateHasChanged();
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.scss b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.scss
new file mode 100644
index 00000000000..9d6546064c8
--- /dev/null
+++ b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorIndicator.razor.scss
@@ -0,0 +1,52 @@
+.bb-nt-indicator {
+ --bb-nt-indicator-width: .5rem;
+ --bb-nt-indicator-border-radius: 50%;
+ --bb-nt-indicator-bg: var(--bs-secondary);
+ background: var(--bb-nt-indicator-bg);
+ cursor: pointer;
+ width: var(--bb-nt-indicator-width);
+ height: var(--bb-nt-indicator-width);
+ border-radius: var(--bb-nt-indicator-border-radius);
+ display: inline-block;
+
+ &.bb-nt-indicator-4g {
+ background-color: var(--bs-success);
+ }
+
+ &.bb-nt-indicator-3g {
+ background-color: var(--bs-warning);
+ }
+
+ &.bb-nt-indicator-2g {
+ background-color: var(--bs-danger);
+ }
+
+ &.offline {
+ background-color: var(--bs-secondary);
+ }
+}
+
+[data-bs-toggle="popover"]:has(.offline) {
+ pointer-events: none;
+}
+
+.bb-nt-main {
+ .bb-nt-item {
+ display: flex;
+ align-items: center;
+
+ > span {
+ width: 120px;
+ }
+
+ > div {
+ flex: 1;
+ min-width: 0;
+ width: 1%;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: .5rem;
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorState.cs b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorState.cs
new file mode 100644
index 00000000000..f6c4afdcbf9
--- /dev/null
+++ b/src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitorState.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 网络状态信息类
+///
+public class NetworkMonitorState
+{
+ ///
+ /// Gets or sets a value indicating whether the network is online
+ ///
+ public bool IsOnline { get; set; }
+
+ ///
+ /// Gets or sets the current network type
+ ///
+ public string? NetworkType { get; set; }
+
+ ///
+ /// Gets or sets the downlink speed in megabits per second (Mbps).
+ ///
+ public double? Downlink { get; set; }
+
+ ///
+ /// Gets or sets the round-trip time (RTT) in milliseconds.
+ ///
+ public int RTT { get; set; }
+}
diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json
index bab4144d7ae..f68bd3852cb 100644
--- a/src/BootstrapBlazor/Locales/en.json
+++ b/src/BootstrapBlazor/Locales/en.json
@@ -389,5 +389,11 @@
},
"BootstrapBlazor.Components.ValidateBase": {
"DefaultRequiredErrorMessage": "{0} is required."
+ },
+ "BootstrapBlazor.Components.NetworkMonitorIndicator": {
+ "NTitle": "Network",
+ "NetworkType": "NetworkType",
+ "Downlink": "Downlink",
+ "RTT": "RTT"
}
}
diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json
index 043e283ba57..88d10375a7a 100644
--- a/src/BootstrapBlazor/Locales/zh.json
+++ b/src/BootstrapBlazor/Locales/zh.json
@@ -389,5 +389,11 @@
},
"BootstrapBlazor.Components.ValidateBase": {
"DefaultRequiredErrorMessage": "{0}是必填项"
+ },
+ "BootstrapBlazor.Components.NetworkMonitorIndicator": {
+ "Title": "网络状态",
+ "NetworkType": "网络类型",
+ "Downlink": "下载速度",
+ "RTT": "响应时间"
}
}
diff --git a/src/BootstrapBlazor/wwwroot/modules/net.js b/src/BootstrapBlazor/wwwroot/modules/net.js
new file mode 100644
index 00000000000..a7748f7c1ae
--- /dev/null
+++ b/src/BootstrapBlazor/wwwroot/modules/net.js
@@ -0,0 +1,58 @@
+import Data from "./data.js"
+import EventHandler from "./event-handler.js";
+
+export function init(id, options) {
+ const { invoke, onlineStateChangedCallback, onNetworkStateChangedCallback, indicators } = options;
+ const updateState = nt => {
+ const { downlink, effectiveType, rtt } = nt;
+ invoke.invokeMethodAsync(onNetworkStateChangedCallback, {
+ downlink, networkType: effectiveType, rTT: rtt
+ });
+ }
+ navigator.connection.onchange = e => {
+ updateState(e.target);
+ }
+
+ const onlineStateChanged = () => {
+ if (Array.isArray(indicators)) {
+ indicators.forEach(indicator => {
+ const el = document.getElementById(indicator);
+ if (el) {
+ el.classList.remove('offline');
+ }
+ });
+ }
+ invoke.invokeMethodAsync(onlineStateChangedCallback, true);
+ }
+ const offlineStateChanged = () => {
+ if (Array.isArray(indicators)) {
+ indicators.forEach(indicator => {
+ const el = document.getElementById(indicator);
+ if (el) {
+ el.classList.add('offline');
+ }
+ });
+ }
+ invoke.invokeMethodAsync(onlineStateChangedCallback, false);
+ }
+ EventHandler.on(window, 'online', onlineStateChanged);
+ EventHandler.on(window, 'offline', offlineStateChanged);
+
+ Data.set(id, {
+ onlineStateChanged,
+ offlineStateChanged
+ });
+
+ updateState(navigator.connection);
+}
+
+export async function dispose(id) {
+ const nt = Data.get(id);
+ Data.remove(id);
+
+ if (nt) {
+ const { onlineStateChanged, offlineStateChanged } = nt;
+ EventHandler.off(window, 'online', onlineStateChanged);
+ EventHandler.off(window, 'offline', offlineStateChanged);
+ }
+}
diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss
index da15073286f..bcbd0818c41 100644
--- a/src/BootstrapBlazor/wwwroot/scss/components.scss
+++ b/src/BootstrapBlazor/wwwroot/scss/components.scss
@@ -69,6 +69,7 @@
@import "../../Components/Menu/Menu.razor.scss";
@import "../../Components/Message/Message.razor.scss";
@import "../../Components/Modal/Modal.razor.scss";
+@import "../../Components/NetworkMonitor/NetworkMonitorIndicator.razor.scss";
@import "../../Components/Pagination/Pagination.razor.scss";
@import "../../Components/Popover/Popover.razor.scss";
@import "../../Components/QueryBuilder/QueryBuilder.razor.scss";
diff --git a/test/UnitTest/Components/DisplayTest.cs b/test/UnitTest/Components/DisplayTest.cs
index 35dd8e71166..6504484b12c 100644
--- a/test/UnitTest/Components/DisplayTest.cs
+++ b/test/UnitTest/Components/DisplayTest.cs
@@ -56,7 +56,7 @@ public async Task LookupService_Ok()
{
pb.Add(a => a.LookupService, new MockLookupService());
});
- await Task.Delay(50);
+ await Task.Delay(100);
Assert.Contains("Test1,Test2", cut.Markup);
cut.SetParametersAndRender(pb =>
@@ -64,7 +64,7 @@ public async Task LookupService_Ok()
pb.Add(a => a.LookupServiceKey, null);
pb.Add(a => a.Lookup, new List { new("v1", "Test3"), new("v2", "Test4") });
});
- await Task.Delay(50);
+ await Task.Delay(100);
Assert.Contains("Test3,Test4", cut.Markup);
}
diff --git a/test/UnitTest/Components/NetworkMonitorIndicatorTest.cs b/test/UnitTest/Components/NetworkMonitorIndicatorTest.cs
new file mode 100644
index 00000000000..ece9ca562eb
--- /dev/null
+++ b/test/UnitTest/Components/NetworkMonitorIndicatorTest.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace UnitTest.Components;
+
+public class NetworkMonitorIndicatorTest : BootstrapBlazorTestBase
+{
+ [Fact]
+ public async Task NetworkMonitorIndicator_Ok()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.PopoverPlacement, Placement.Top);
+ });
+
+ var com = cut.FindComponent();
+ await cut.InvokeAsync(() => com.Instance.TriggerNetworkStateChanged(new NetworkMonitorState
+ {
+ IsOnline = false,
+ NetworkType = "4g",
+ Downlink = 10.0,
+ RTT = 50
+ }));
+ Assert.DoesNotContain("offline", cut.Markup);
+
+ await cut.InvokeAsync(() => com.Instance.TriggerOnlineStateChanged(true));
+ Assert.DoesNotContain("offline", cut.Markup);
+ }
+
+ [Fact]
+ public async Task NetworkMonitor_Ok()
+ {
+ NetworkMonitorState? state = null;
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.Add(a => a.OnNetworkStateChanged, v =>
+ {
+ state = v;
+ return Task.CompletedTask;
+ });
+ });
+
+ await cut.InvokeAsync(() => cut.Instance.TriggerOnlineStateChanged(false));
+ Assert.NotNull(state);
+ Assert.False(state.IsOnline);
+ }
+}