!775 feat(#I2A037): update layout support tab

* Merge branch 'dev' into dev-layout
* doc: Menu 示例文件更新
* feat: 修复首次加载时寻找菜单显示文本功能
* feat: Tab 组件更新逻辑与地址栏进行联动
* feat: Menu 组件支持与 Tab 联动
* feat: Layout 组件内置 多 Tab 功能
* WIP: 临时增加默认 Index 文本
* style: 增加 Tab 在 Layout-main 中的样式
* fix: 修复 NavLink 匹配规则
* refactor: 新增 MenuTabBoundleOptions 类
* refactor: 移动 Options 配置类位置
* feat: 增加一个默认 Tab 标签页显示文本
* feat: 增加拦截地址栏进行多 Tab 处理逻辑
* style: 更新 tab 样式
* feat: Tab 组件增加 AutoNavagator 属性是否自动与地址栏联动
This commit is contained in:
Argo 2020-12-19 00:31:07 +08:00
parent dba9ea1207
commit 5a2627b19f
15 changed files with 259 additions and 32 deletions

View File

@ -5,30 +5,30 @@
<h4>为页面和功能提供导航的菜单列表。</h4>
<Block Title="顶栏" Introduction="适用广泛的基础用法。" CodeFile="menu.1.html">
<Menu Items="@Items" OnClick="@OnClickMenu" />
<Menu Items="@Items" DisableNavigation="true" OnClick="@OnClickMenu" />
<Logger @ref="Trace" class="mt-3" />
</Block>
<Block Title="带图标的顶栏菜单" Introduction="适用简单的网站应用,通过设置菜单项 <code>MenuItem</code> 的 <code>Icon</code> 属性设置菜单图标" CodeFile="menu.2.html">
<Menu Items="@IconItems" />
<Menu Items="@IconItems" DisableNavigation="true" />
</Block>
<Block Title="侧栏" Introduction="适用于左右结构布局的网站,通过设置 <code>IsVertical</code> 更改导航菜单为侧栏" CodeFile="menu.3.html">
<div style="width:220px;">
<Menu Items="@SideMenuItems" IsVertical="true" OnClick="@OnClickSideMenu" style="border-right: 1px solid #e6e6e6;" />
<Menu Items="@SideMenuItems" DisableNavigation="true" IsVertical="true" OnClick="@OnClickSideMenu" style="border-right: 1px solid #e6e6e6;" />
</div>
<Logger @ref="TraceSideMenu" class="mt-3" />
</Block>
<Block Title="带图标的侧栏菜单" Introduction="通过设置菜单项 <code>MenuItem</code> 的 <code>Icon</code> 属性设置菜单图标" CodeFile="menu.3.html">
<div style="width:220px;">
<Menu Items="@IconSideMenuItems" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
<Menu Items="@IconSideMenuItems" DisableNavigation="true" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
</div>
</Block>
<Block Title="手风琴效果的侧栏" Introduction="通过设置 <code>IsAccordion</code> 属性设置手风琴特效侧栏菜单" CodeFile="menu.4.html">
<div style="width:220px;">
<Menu Items="@IconSideMenuItems" IsVertical="true" IsAccordion="true" style="border-right: 1px solid #e6e6e6;" />
<Menu Items="@IconSideMenuItems" DisableNavigation="true" IsVertical="true" IsAccordion="true" style="border-right: 1px solid #e6e6e6;" />
</div>
</Block>
@ -60,14 +60,14 @@
<Block Title="带挂件的菜单" Introduction="通过设置 <code>MenuItem</code> 的 <code>Component</code> 属性设置自定义组件到菜单中" CodeFile="menu.6.html">
<div style="width:220px;">
<Menu Items="@WidgetIconSideMenuItems" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
<Menu Items="@WidgetIconSideMenuItems" DisableNavigation="true" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
</div>
</Block>
<Block Title="自定义节点收缩" Introduction="通过设置 <code>MenuItem</code> 的 <code>IsCollapsed</code> 属性设置节点是否收起" CodeFile="menu.7.html">
<p>本例中 <b>权限设置</b> 节点为展开状态,其余节点为收起状态</p>
<div style="width:220px;">
<Menu Items="@CollapsedIconSideMenuItems" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
<Menu Items="@CollapsedIconSideMenuItems" DisableNavigation="true" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
</div>
</Block>
@ -76,7 +76,7 @@
<Menu Items="@DisabledMenuItems" />
<p class="mt-3"><b>侧栏的禁用示例</b></p>
<div style="width:220px;">
<Menu Items="@DisabledMenuItems" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
<Menu Items="@DisabledMenuItems" DisableNavigation="true" IsVertical="true" style="border-right: 1px solid #e6e6e6;" />
</div>
</Block>

View File

@ -8,7 +8,16 @@
@Header
</header>
<main class="@MainClassString">
@Main
@if (UseTabSet)
{
<CascadingValue Value="this" IsFixed="true">
<Tab ClickTabToNavigator="true" ShowExtendButtons="true"></Tab>
</CascadingValue>
}
else
{
@Main
}
</main>
@if (ShowFooter)
{
@ -45,7 +54,16 @@
@Header
</header>
<main class="@MainClassString">
@Main
@if (UseTabSet)
{
<CascadingValue Value="this" IsFixed="true">
<Tab ClickTabToNavigator="true" ShowExtendButtons="true"></Tab>
</CascadingValue>
}
else
{
@Main
}
</main>
@if (ShowFooter)
{
@ -81,7 +99,16 @@
}
</aside>
<main class="@MainClassString">
@Main
@if (UseTabSet)
{
<CascadingValue Value="this" IsFixed="true">
<Tab ClickTabToNavigator="true" ShowExtendButtons="true"></Tab>
</CascadingValue>
}
else
{
@Main
}
</main>
</section>
@if (ShowFooter)

View File

@ -141,6 +141,12 @@ namespace BootstrapBlazor.Components
[Parameter]
public IEnumerable<MenuItem>? Menus { get; set; }
/// <summary>
/// 获得/设置 是否右侧使用 Tab 组件 默认为 false 不使用
/// </summary>
[Parameter]
public bool UseTabSet { get; set; }
/// <summary>
/// 获得/设置 是否固定 Footer 组件
/// </summary>

View File

@ -8,8 +8,10 @@
// **********************************
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
@ -64,15 +66,18 @@ namespace BootstrapBlazor.Components
/// 获得/设置 菜单项点击回调委托
/// </summary>
[Parameter]
public Func<MenuItem, Task> OnClick { get; set; } = _ => Task.CompletedTask;
public Func<MenuItem, Task>? OnClick { get; set; }
#nullable disable
/// <summary>
/// 获得/设置 NavigationManager 实例
/// </summary>
[Inject]
private NavigationManager Navigator { get; set; }
#nullable restore
[NotNull]
private NavigationManager? Navigator { get; set; }
[Inject]
[NotNull]
private MenuTabBoundleOptions? Options { get; set; }
/// <summary>
/// OnInitialized 方法
@ -82,7 +87,15 @@ namespace BootstrapBlazor.Components
base.OnInitialized();
var item = FindMenuItem(Items, Navigator.ToBaseRelativePath(Navigator.Uri));
CascadingSetActive(item);
if (DisableNavigation)
{
CascadingSetActive(item);
}
else
{
Options.TabItemText = item?.Text;
}
}
/// <summary>
@ -122,9 +135,11 @@ namespace BootstrapBlazor.Components
{
if (!item.IsDisabled)
{
Options.TabItemText = item.Text;
// 回调委托
await OnClick(item);
if (!item.Items.Any())
if (OnClick != null) await OnClick(item);
if (DisableNavigation)
{
CascadingSetActive(item);
StateHasChanged();

View File

@ -1,7 +1,7 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapComponentBase
<NavLink @attributes="@AdditionalAttributes" class="@ClassString" href="@GetHrefString" @onclick="@OnClickLink" Match="NavLinkMatch.All">
<NavLink @attributes="@AdditionalAttributes" class="@ClassString" href="@GetHrefString" @onclick="@OnClickLink" Match="@GetMatch()" data-match="@GetMatch()">
@if (!string.IsNullOrEmpty(Item.Icon))
{
<i class="@Item.Icon"></i>

View File

@ -8,6 +8,7 @@
// **********************************
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@ -26,7 +27,7 @@ namespace BootstrapBlazor.Components
.AddClassFromAttributes(AdditionalAttributes)
.Build();
private string? GetHrefString => (DisableNavigation || Item.IsDisabled) ? null : (Item.Items.Any() ? "#" : Item.Url?.TrimStart('/'));
private string? GetHrefString => (DisableNavigation || Item.IsDisabled) ? null : (Item.Items.Any() ? "#" : Item.Url);
/// <summary>
/// 获得/设置 是否禁止导航 默认为 false 允许导航
@ -51,5 +52,7 @@ namespace BootstrapBlazor.Components
{
if (OnClick != null) await OnClick(Item);
}
private NavLinkMatch GetMatch() => string.IsNullOrEmpty(Item.Url) ? NavLinkMatch.All : NavLinkMatch.Prefix;
}
}

View File

@ -31,7 +31,7 @@
}
@foreach (var item in Items)
{
<div role="tab" tabindex="-1" class="@GetClassString(item.IsActive)" @onclick="@(e =>OnClickTabItem(item))">
<div role="tab" tabindex="-1" class="@GetClassString(item.IsActive)" @onclick="@(e => OnClickTabItem(item))">
@if (!string.IsNullOrEmpty(item.Icon))
{
<i class="@GetIconClassString(item.Icon)"></i>

View File

@ -8,9 +8,13 @@
// **********************************
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace BootstrapBlazor.Components
@ -20,6 +24,8 @@ namespace BootstrapBlazor.Components
/// </summary>
public sealed partial class Tab
{
static ConcurrentDictionary<string, Type> RouteTable { get; set; } = new ConcurrentDictionary<string, Type>();
/// <summary>
///
/// </summary>
@ -104,6 +110,13 @@ namespace BootstrapBlazor.Components
[Parameter]
public int Height { get; set; }
/// <summary>
/// 获得/设置 默认首页 Tab 显示文本 默认为 Index
/// </summary>
[Parameter]
[NotNull]
public string? DefaultIndexText { get; set; } = "Index";
/// <summary>
/// 获得/设置 组件标签显示位置 默认显示在 Top 位置
/// </summary>
@ -122,26 +135,104 @@ namespace BootstrapBlazor.Components
[Parameter]
public bool ShowExtendButtons { get; set; }
/// <summary>
/// 获得/设置 点击 TabItem 时是否自动导航 默认为 false 不导航
/// </summary>
[Parameter]
public bool ClickTabToNavigator { get; set; }
/// <summary>
/// 获得/设置 TabItems 模板
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// 获得/设置 Gets or sets a collection of additional assemblies that should be searched for components that can match URIs.
/// </summary>
[Parameter]
public IEnumerable<Assembly>? AdditionalAssemblies { get; set; }
/// <summary>
/// 获得/设置 点击 TabItem 时回调方法
/// </summary>
[Parameter]
public Func<TabItem, Task>? OnClickTab { get; set; }
[Inject]
[NotNull]
private NavigationManager? Navigator { get; set; }
[Inject]
[NotNull]
private MenuTabBoundleOptions? Options { get; set; }
/// <summary>
/// OnInitialized 方法
/// OnInitializedAsync 方法
/// </summary>
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
base.OnInitialized();
await base.OnInitializedAsync();
if (ShowExtendButtons) IsBorderCard = true;
await InitRouteTable();
}
private Task InitRouteTable() => Task.Run(() =>
{
if (ClickTabToNavigator)
{
var apps = AdditionalAssemblies == null ? new[] { Assembly.GetEntryAssembly()! } : new[] { Assembly.GetEntryAssembly()! }.Concat(AdditionalAssemblies);
var componentTypes = apps.SelectMany(a => a.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t)));
foreach (var componentType in componentTypes)
{
var routeAttributes = componentType.GetCustomAttributes<RouteAttribute>(false);
foreach (var template in routeAttributes.Select(t => t.Template))
{
RouteTable.TryAdd(template.Trim('/'), componentType);
}
}
Navigator.LocationChanged += Navigator_LocationChanged;
InvokeAsync(() => AddTabByUrl(Navigator.ToBaseRelativePath(Navigator.Uri)));
}
});
private void Navigator_LocationChanged(object? sender, LocationChangedEventArgs e)
{
var requestUrl = Navigator.ToBaseRelativePath(e.Location);
var tab = Items.FirstOrDefault(tab => tab.Url == requestUrl);
if (tab != null)
{
ActiveTab(tab);
}
else
{
AddTabByUrl(requestUrl);
}
}
private void AddTabByUrl(string url)
{
if (RouteTable.TryGetValue(url, out var comp))
{
var item = new TabItem();
var parameters = new Dictionary<string, object>
{
[nameof(TabItem.Text)] = Options.TabItemText ?? string.Empty,
[nameof(TabItem.Url)] = url,
[nameof(TabItem.IsActive)] = true,
[nameof(TabItem.ChildContent)] = new RenderFragment(builder =>
{
builder.OpenComponent(0, comp);
builder.CloseComponent();
})
};
var _ = item.SetParametersAsync(ParameterView.FromDictionary(parameters));
Add(item);
}
}
/// <summary>
@ -165,8 +256,15 @@ namespace BootstrapBlazor.Components
private async Task OnClickTabItem(TabItem item)
{
Items.ToList().ForEach(i => i.SetActive(false));
item.SetActive(true);
if (OnClickTab != null) await OnClickTab(item);
if (ClickTabToNavigator)
{
Navigator.NavigateTo(item.Url ?? "");
}
else
{
item.SetActive(true);
}
}
/// <summary>
@ -180,11 +278,19 @@ namespace BootstrapBlazor.Components
var index = _items.IndexOf(item);
if (index > -1)
{
item.SetActive(false);
index--;
if (index < 0) index = _items.Count - 1;
if (!ClickTabToNavigator) item.SetActive(false);
item = Items.ElementAt(index);
item.SetActive(true);
if (ClickTabToNavigator)
{
Navigator.NavigateTo(item.Url!);
}
else
{
item.SetActive(true);
}
}
}
}
@ -200,11 +306,20 @@ namespace BootstrapBlazor.Components
var index = _items.IndexOf(item);
if (index < _items.Count)
{
item.SetActive(false);
if (!ClickTabToNavigator) item.SetActive(false);
index++;
if (index + 1 > _items.Count) index = 0;
item = Items.ElementAt(index);
item.SetActive(true);
if (ClickTabToNavigator)
{
Navigator.NavigateTo(item.Url!);
}
else
{
item.SetActive(true);
}
}
}
}
@ -307,5 +422,19 @@ namespace BootstrapBlazor.Components
StateHasChanged();
return Task.CompletedTask;
}
/// <summary>
/// Dispose 方法
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (ClickTabToNavigator) Navigator.LocationChanged -= Navigator_LocationChanged;
}
base.Dispose(disposing);
}
}
}

View File

@ -23,6 +23,12 @@ namespace BootstrapBlazor.Components
[Parameter]
public string? Text { get; set; }
/// <summary>
/// 获得/设置 请求地址
/// </summary>
[Parameter]
public string? Url { get; set; }
/// <summary>
/// 获得/设置 当前状态是否激活
/// </summary>

View File

@ -38,6 +38,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<PopoverService>();
services.AddScoped<ToastService>();
services.AddScoped<SwalService>();
services.AddScoped<MenuTabBoundleOptions>();
services.AddSingleton<IConfigureOptions<BootstrapBlazorOptions>, ConfigureOptions<BootstrapBlazorOptions>>();
services.Configure<BootstrapBlazorOptions>(options =>
{

View File

@ -0,0 +1,22 @@
// **********************************
// 框架名称BootstrapBlazor
// 框架作者Argo Zhang
// 开源地址:
// Gitee : https://gitee.com/LongbowEnterprise/BootstrapBlazor
// GitHub: https://github.com/ArgoZhang/BootstrapBlazor
// 开源协议Apache-2.0 (https://gitee.com/LongbowEnterprise/BootstrapBlazor/blob/dev/LICENSE)
// **********************************
namespace BootstrapBlazor.Components
{
/// <summary>
/// 菜单与标签捆绑配置类
/// </summary>
internal class MenuTabBoundleOptions
{
/// <summary>
/// 获得/设置 Tab 标签文本
/// </summary>
public string? TabItemText { get; set; }
}
}

File diff suppressed because one or more lines are too long

View File

@ -3076,6 +3076,7 @@ input:disabled,
cursor: pointer;
align-items: center;
position: relative;
transition: color .3s linear, background-color .3s linear;
}
.tabs-item:hover {
@ -3202,9 +3203,16 @@ input:disabled,
.tabs-border-card .tabs-header .tabs-item {
border: 1px solid transparent;
color: #909399;
}
.tabs-border-card .tabs-header .tabs-item:not(:hover):not(.is-active) {
color: #909399;
}
.tabs-border-card .tabs-header .tabs-item:hover {
background-color: #e9ecef;
}
.tabs-border-card.tabs-top .tabs-header {
border-bottom: 1px solid #e4e7ed;
}
@ -3267,6 +3275,16 @@ input:disabled,
margin-right: 1rem;
margin-left: 0rem;
}
.layout-main > .tabs.tabs-top.tabs-border-card {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border: none;
overflow: auto;
}
/*end tabs*/
/*transfer*/

File diff suppressed because one or more lines are too long