Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/BootstrapBlazor.Server/Components/Layout/HomeLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
<div class="ms-1">BB @VersionService.Version</div>
<FooterCounter></FooterCounter>
<CacheCounter></CacheCounter>
<div class="ms-2">
<NetworkMonitorIndicator PopoverPlacement="Placement.Bottom"></NetworkMonitorIndicator>
</div>
</div>
<div class="d-flex flex-fill align-items-center justify-content-center">
<a class="d-none d-md-block me-3" href="@WebsiteOption.CurrentValue.GiteeRepositoryUrl" target="_blank">@Localizer["Footer"]</a>
Expand Down
69 changes: 69 additions & 0 deletions src/BootstrapBlazor/Components/NetworkMonitor/NetworkMonitor.cs
Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

namespace BootstrapBlazor.Components;

/// <summary>
/// 客户端链接组件
/// </summary>
[BootstrapModuleAutoLoader(ModuleName = "net", JSObjectReference = true)]
public class NetworkMonitor : BootstrapModuleComponentBase
{
/// <summary>
/// Gets or sets the callback function that is invoked when the network state changes.
/// </summary>
[Parameter]
public Func<NetworkMonitorState, Task>? OnNetworkStateChanged { get; set; }

/// <summary>
/// Gets or sets the list of indicators used for display info.
/// </summary>
[Parameter]
public List<string>? Indicators { get; set; }

private NetworkMonitorState _state = new();

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, new
{
Invoke = Interop,
OnlineStateChangedCallback = nameof(TriggerOnlineStateChanged),
OnNetworkStateChangedCallback = nameof(TriggerNetworkStateChanged),
Indicators
});

/// <summary>
/// JSInvoke 回调方法
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task TriggerOnlineStateChanged(bool online)
{
_state.IsOnline = online;
if (OnNetworkStateChanged != null)
{
await OnNetworkStateChanged(_state);
}
}

/// <summary>
/// JSInvoke 回调方法
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task TriggerNetworkStateChanged(NetworkMonitorState state)
{
// 网络状态变化回调方法
_state = state;
_state.IsOnline = true;
if (OnNetworkStateChanged != null)
{
await OnNetworkStateChanged(_state);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@namespace BootstrapBlazor.Components
@inherits IdComponentBase

<Popover Title="@Title" Trigger="@Trigger" Placement="@PopoverPlacement">
<ChildContent>
<span @attributes="@AdditionalAttributes" id="@Id" tabindex="0" class="@ClassString"></span>
</ChildContent>
<Template>
<div class="bb-nt-main">
<div class="bb-nt-item">
<span>NetworkType:</span>
<div>@_state.NetworkType</div>
</div>
<div class="bb-nt-item">
<span>Downlink:</span>
<div>@_state.Downlink M/s</div>
</div>
<div class="bb-nt-item">
<span>RTT:</span>
<div>@_state.RTT ms</div>
</div>
</div>
</Template>
</Popover>

<NetworkMonitor OnNetworkStateChanged="OnNetworkStateChanged" Indicators="@_indicators"></NetworkMonitor>
Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

using Microsoft.Extensions.Localization;

namespace BootstrapBlazor.Components;

/// <summary>
/// Represents a network monitor indicator with customizable tooltip settings.
/// </summary>
/// <remarks>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.</remarks>
public partial class NetworkMonitorIndicator
{
/// <summary>
/// 获得/设置 Popover 弹窗标题 默认为 null
/// </summary>
[Parameter]
public string? Title { get; set; }

/// <summary>
/// 获得/设置 Popover 显示位置 默认为 Top
/// </summary>
[Parameter]
public Placement PopoverPlacement { get; set; } = Placement.Top;

/// <summary>
/// 获得/设置 Popover 触发方式 默认为 hover focus
/// </summary>
[Parameter]
[NotNull]
public string? Trigger { get; set; }

[Inject, NotNull]
private IStringLocalizer<NetworkMonitorIndicator>? Localizer { get; set; }

private NetworkMonitorState _state = new();
private readonly List<string> _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();

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

_indicators.Add(Id);
}

/// <summary>
/// <inheritdoc/>
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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([email protected]) Website: https://www.blazor.zone

namespace BootstrapBlazor.Components;

/// <summary>
/// 网络状态信息类
/// </summary>
public class NetworkMonitorState
{
/// <summary>
/// Gets or sets a value indicating whether the network is online
/// </summary>
public bool IsOnline { get; set; }

/// <summary>
/// Gets or sets the current network type
/// </summary>
public string? NetworkType { get; set; }

/// <summary>
/// Gets or sets the downlink speed in megabits per second (Mbps).
/// </summary>
public double? Downlink { get; set; }

/// <summary>
/// Gets or sets the round-trip time (RTT) in milliseconds.
/// </summary>
public int RTT { get; set; }
}
6 changes: 6 additions & 0 deletions src/BootstrapBlazor/Locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,5 +389,11 @@
},
"BootstrapBlazor.Components.ValidateBase": {
"DefaultRequiredErrorMessage": "{0} is required."
},
"BootstrapBlazor.Components.NetworkMonitorIndicator": {
"NTitle": "Network",
"NetworkType": "NetworkType",
"Downlink": "Downlink",
"RTT": "RTT"
}
}
6 changes: 6 additions & 0 deletions src/BootstrapBlazor/Locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,5 +389,11 @@
},
"BootstrapBlazor.Components.ValidateBase": {
"DefaultRequiredErrorMessage": "{0}是必填项"
},
"BootstrapBlazor.Components.NetworkMonitorIndicator": {
"Title": "网络状态",
"NetworkType": "网络类型",
"Downlink": "下载速度",
"RTT": "响应时间"
}
}
58 changes: 58 additions & 0 deletions src/BootstrapBlazor/wwwroot/modules/net.js
Original file line number Diff line number Diff line change
@@ -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) {
var nt = Data.get(id);
Data.remove(id);

if (nt) {
const { onlineStateChanged, offlineStateChanged } = nt;
EventHandler.off(window, 'online', onlineStateChanged);
EventHandler.off(window, 'offline', offlineStateChanged);
}
}
1 change: 1 addition & 0 deletions src/BootstrapBlazor/wwwroot/scss/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions test/UnitTest/Components/DisplayTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ 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 =>
{
pb.Add(a => a.LookupServiceKey, null);
pb.Add(a => a.Lookup, new List<SelectedItem> { new("v1", "Test3"), new("v2", "Test4") });
});
await Task.Delay(50);
await Task.Delay(100);
Assert.Contains("Test3,Test4", cut.Markup);
}

Expand Down
Loading
Loading