Merge branch 'dev-blazor' into publish

# Conflicts:
#	src/admin/Bootstrap.Admin/appsettings.json
This commit is contained in:
Argo Zhang 2019-12-10 22:15:39 +08:00
commit 954b7556a7
No known key found for this signature in database
GPG Key ID: 152E398953DDF19F
104 changed files with 4621 additions and 33 deletions

View File

@ -0,0 +1,10 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(NotFoundLayout)" />
</CascadingAuthenticationState>
</NotFound>
</Router>

View File

@ -0,0 +1,49 @@
using Bootstrap.Admin.Extensions;
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// AdminLayout 布局类
/// </summary>
public class AdminLayoutComponentBase : LayoutComponentBase
{
/// <summary>
///
/// </summary>
public TabSet? TabSet { get; set; }
/// <summary>
///
/// </summary>
[CascadingParameter(Name = "Default")]
public DefaultLayout RootLayout { get; protected set; } = new DefaultLayout();
/// <summary>
///
/// </summary>
public string UserName { get; set; } = "";
/// <summary>
///
/// </summary>
public string DisplayName { get; set; } = "";
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override void OnAfterRender(bool firstRender)
{
var requestUrl = RootLayout.NavigationManager?.Uri ?? "";
var path = new Uri(requestUrl).PathAndQuery;
var menus = DataAccess.MenuHelper.RetrieveAllMenus(RootLayout.UserName);
var menu = menus.FirstOrDefault(menu => path.Contains(menu.Url.ToBlazorMenuUrl()));
if (menu != null) TabSet?.AddTab(menu);
}
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 弹窗组件基类
/// </summary>
public class AlertBase : ModalBase
{
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? AlertBody { get; set; }
/// <summary>
/// 获得/设置 弹窗 Footer 代码块
/// </summary>
[Parameter]
public RenderFragment? AlertFooter { get; set; }
/// <summary>
/// 获得/设置 是否自动关闭 默认为 true
/// </summary>
[Parameter]
public bool AutoClose { get; set; } = true;
/// <summary>
/// 获得/设置 自动关闭时长 默认 1500 毫秒
/// </summary>
[Parameter]
public int Interval { get; set; } = 1500;
/// <summary>
/// 控件渲染完毕后回调方法
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (_show)
{
_show = false;
Toggle();
}
}
private bool _show;
/// <summary>
///
/// </summary>
/// <param name="title"></param>
public void Show(string title)
{
Title = title;
_show = true;
StateHasChanged();
}
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// BootstrapAdminDataAnnotationsValidator 验证组件
/// </summary>
public class BootstrapAdminDataAnnotationsValidator : ComponentBase
{
/// <summary>
/// 获得/设置 当前编辑数据上下文
/// </summary>
[CascadingParameter]
EditContext? CurrentEditContext { get; set; }
/// <summary>
/// 获得/设置 当前编辑窗体上下文
/// </summary>
[CascadingParameter]
public LgbEditFormBase? EditForm { get; set; }
/// <summary>
/// 初始化方法
/// </summary>
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} inside an EditForm.");
}
if (EditForm == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading parameter of type {nameof(LgbEditFormBase)}. For example, you can use {nameof(BootstrapAdminDataAnnotationsValidator)} inside an EditForm.");
}
CurrentEditContext.AddBootstrapAdminDataAnnotationsValidation(EditForm);
}
}
}

View File

@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public static class BootstrapAdminEditContextDataAnnotationsExtensions
{
private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> _propertyInfoCache = new ConcurrentDictionary<(Type, string), PropertyInfo>();
/// <summary>
///
/// </summary>
/// <param name="editContext">The <see cref="EditContext"/>.</param>
/// <param name="editForm"></param>
public static EditContext AddBootstrapAdminDataAnnotationsValidation(this EditContext editContext, LgbEditFormBase editForm)
{
if (editContext == null)
{
throw new ArgumentNullException(nameof(editContext));
}
var messages = new ValidationMessageStore(editContext);
editContext.OnValidationRequested +=
(sender, eventArgs) => ValidateModel(sender as EditContext, messages, editForm);
editContext.OnFieldChanged +=
(sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier, editForm);
return editContext;
}
private static void ValidateModel(EditContext? editContext, ValidationMessageStore messages, LgbEditFormBase editForm)
{
if (editContext != null)
{
var validationContext = new ValidationContext(editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
editForm.ValidateObject(editContext.Model, validationContext, validationResults);
messages.Clear();
foreach (var validationResult in validationResults)
{
if (!validationResult.MemberNames.Any())
{
messages.Add(new FieldIdentifier(editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage);
continue;
}
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
}
private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier, LgbEditFormBase editForm)
{
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
editForm.ValidateProperty(propertyValue, validationContext, results);
messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage));
editContext.NotifyValidationStateChanged();
}
}
#nullable disable
internal static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, out PropertyInfo propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
#nullable restore
}
}

View File

@ -0,0 +1,30 @@
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class BootstrapComponentBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Inject]
public NavigationManager? NavigationManager { get; set; }
/// <summary>
///
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
/// <summary>
///
/// </summary>
[CascadingParameter(Name = "Admin")]
protected AdminLayout Layout { get; set; } = new AdminLayout();
}
}

View File

@ -0,0 +1,49 @@
namespace Bootstrap.Admin.Components
{
/// <summary>
/// CheckBox 组件状态枚举值
/// </summary>
public enum CheckBoxState
{
/// <summary>
/// 未选中
/// </summary>
UnChecked,
/// <summary>
/// 选中
/// </summary>
Checked,
/// <summary>
/// 混合模式
/// </summary>
Mixed
}
/// <summary>
///
/// </summary>
public static class CheckBoxStateExtensions
{
/// <summary>
///
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
public static string ToCss(this CheckBoxState state)
{
var ret = "false";
switch (state)
{
case CheckBoxState.Checked:
ret = "true";
break;
case CheckBoxState.Mixed:
ret = "mixed";
break;
case CheckBoxState.UnChecked:
break;
}
return ret;
}
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Components;
using System;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// Checkbox 组件基类
/// </summary>
/// <typeparam name="TItem"></typeparam>
public class CheckboxBase<TItem> : ComponentBase
{
#nullable disable
/// <summary>
/// 获得/设置 数据绑定项
/// </summary>
[Parameter]
public TItem Item { get; set; }
#nullable restore
/// <summary>
/// 获得/设置 显示文本
/// </summary>
[Parameter]
public string Text { get; set; } = "";
/// <summary>
/// 获得/设置 是否被选中
/// </summary>
protected bool Checked { get; set; }
/// <summary>
/// 勾选回调方法
/// </summary>
[Parameter]
public Action<TItem, bool> ToggleCallback { get; set; } = new Action<TItem, bool>((v, c) => { });
/// <summary>
///
/// </summary>
[Parameter]
public Func<TItem, CheckBoxState> SetCheckCallback { get; set; } = new Func<TItem, CheckBoxState>(item => CheckBoxState.UnChecked);
/// <summary>
///
/// </summary>
protected override void OnParametersSet()
{
State = SetCheckCallback(Item);
Checked = State == CheckBoxState.Checked;
}
/// <summary>
///
/// </summary>
[Parameter]
public CheckBoxState State { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
protected string RenderStateCss()
{
var ret = "form-checkbox";
switch (State)
{
case CheckBoxState.Mixed:
ret = "form-checkbox is-indeterminate";
break;
case CheckBoxState.Checked:
ret = "form-checkbox is-checked";
break;
case CheckBoxState.UnChecked:
break;
}
return ret;
}
}
}

View File

@ -0,0 +1,67 @@
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public enum Color
{
/// <summary>
///
/// </summary>
Primary,
/// <summary>
///
/// </summary>
Secondary,
/// <summary>
///
/// </summary>
Success,
/// <summary>
///
/// </summary>
Danger,
/// <summary>
///
/// </summary>
Warning,
/// <summary>
///
/// </summary>
Info,
/// <summary>
///
/// </summary>
Light,
/// <summary>
///
/// </summary>
Dark,
/// <summary>
///
/// </summary>
White,
/// <summary>
///
/// </summary>
Transparent
}
/// <summary>
///
/// </summary>
public static class ColorExtensions
{
/// <summary>
///
/// </summary>
/// <param name="color"></param>
/// <param name="prefix"></param>
/// <returns></returns>
public static string ToCss(this Color color, string prefix = "")
{
string ret = color.ToString().ToLowerInvariant();
return string.IsNullOrEmpty(prefix) ? ret : $"{prefix}-{ret}";
}
}
}

View File

@ -0,0 +1,106 @@
using Bootstrap.Admin.Extensions;
using Bootstrap.Admin.Models;
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.JSInterop;
using System;
using System.Net;
using System.Threading.Tasks;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class DefaultLayoutComponentBase : LayoutComponentBase
{
/// <summary>
///
/// </summary>
[Inject]
public AuthenticationStateProvider AuthenticationStateProvider { get; set; } = new ServerAuthenticationStateProvider();
/// <summary>
///
/// </summary>
[Inject]
public NavigationManager? NavigationManager { get; set; }
/// <summary>
///
/// </summary>
[Inject]
public IJSRuntime? JSRuntime { get; set; }
/// <summary>
///
/// </summary>
public NavigatorBarModel Model { get; set; } = new NavigatorBarModel("");
/// <summary>
///
/// </summary>
public string UserName { get; set; } = "";
/// <summary>
///
/// </summary>
public string DisplayName { get; set; } = "";
/// <summary>
///
/// </summary>
protected bool IsAdmin { get; set; }
/// <summary>
/// 获得/设置 系统首页
/// </summary>
public string HomeUrl { get; protected set; } = "/Pages";
/// <summary>
/// 获得/设置 当前请求路径
/// </summary>
protected string RequestUrl { get; set; } = "";
/// <summary>
/// OnInitializedAsync 方法
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (!state.User.Identity.IsAuthenticated)
{
NavigationManager?.NavigateTo("/Account/Login?returnUrl=" + WebUtility.UrlEncode(new Uri(NavigationManager.Uri).PathAndQuery));
}
else
{
RequestUrl = new UriBuilder(NavigationManager?.Uri ?? "").Path;
Model = new NavigatorBarModel(state.User.Identity.Name, RequestUrl.ToMvcMenuUrl());
IsAdmin = state.User.IsInRole("Administrators");
UserName = state.User.Identity.Name ?? "";
DisplayName = Model.DisplayName;
}
}
/// <summary>
/// 设置参数方法
/// </summary>
protected override void OnParametersSet()
{
RequestUrl = new UriBuilder(NavigationManager?.Uri ?? "").Path;
Model = new NavigatorBarModel(UserName, RequestUrl.ToMvcMenuUrl());
}
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
if (firstRender) JSRuntime.InitDocument();
}
}
}

View File

@ -0,0 +1,69 @@
using Bootstrap.Security;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 字典表维护组件
/// </summary>
public class DictsBase : ComponentBase
{
/// <summary>
///
/// </summary>
protected BootstrapDict QueryModel { get; set; } = new BootstrapDict() { Define = -1 };
/// <summary>
///
/// </summary>
protected List<SelectedItem> DefineItems { get; set; } = new List<SelectedItem>(new SelectedItem[] { new SelectedItem() { Text = "系统使用", Value = "0" }, new SelectedItem() { Text = "自定义", Value = "1" } });
/// <summary>
///
/// </summary>
protected List<SelectedItem> QueryDefine { get; set; } = new List<SelectedItem>(new SelectedItem[] { new SelectedItem() { Text = "全部", Value = "-1", Active = true }, new SelectedItem() { Text = "系统使用", Value = "0" }, new SelectedItem() { Text = "自定义", Value = "1" } });
/// <summary>
///
/// </summary>
/// <param name="pageIndex"></param>
/// <param name="pageItems"></param>
protected QueryData<BootstrapDict> Query(int pageIndex, int pageItems)
{
var data = DataAccess.DictHelper.RetrieveDicts();
if (QueryModel.Define != -1) data = data.Where(d => d.Define == QueryModel.Define);
if (QueryModel.Name != "") data = data.Where(d => d.Name == QueryModel.Name);
if (QueryModel.Category != "") data = data.Where(d => d.Category == QueryModel.Category);
var totalCount = data.Count();
var items = data.Skip((pageIndex - 1) * pageItems).Take(pageItems);
return new QueryData<BootstrapDict>() { Items = items, TotalCount = totalCount, PageIndex = pageIndex, PageItems = pageItems };
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected BootstrapDict Add()
{
return new BootstrapDict();
}
/// <summary>
///
/// </summary>
protected bool Save(BootstrapDict dict) => DataAccess.DictHelper.Save(dict);
/// <summary>
///
/// </summary>
protected bool Delete(IEnumerable<BootstrapDict> items) => DataAccess.DictHelper.Delete(items.Select(item => item.Id ?? ""));
/// <summary>
///
/// </summary>
/// <returns></returns>
protected override bool ShouldRender() => false;
}
}

View File

@ -0,0 +1,147 @@
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 可编辑页面组件包含查询与数据表格
/// </summary>
public class EditPageBase<TItem> : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
#nullable disable
/// <summary>
///
/// </summary>
[Parameter]
public TItem QueryModel { get; set; }
#nullable restore
/// <summary>
/// 查询模板
/// </summary>
[Parameter]
public RenderFragment<TItem>? QueryBody { get; set; }
/// <summary>
/// 查询按钮回调方法
/// </summary>
[Parameter]
public Func<int, int, QueryData<TItem>>? OnQuery { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment<TItem>? TableHeader { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment<TItem>? RowTemplate { get; set; }
/// <summary>
/// 按钮模板
/// </summary>
[Parameter]
public RenderFragment<TItem>? ButtonTemplate { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? TableFooter { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment<TItem>? EditTemplate { get; set; }
/// <summary>
///
/// </summary>
protected Table<TItem>? Table { get; set; }
/// <summary>
/// 新建按钮回调方法
/// </summary>
[Parameter]
public Func<TItem> OnAdd { get; set; } = () => throw new InvalidOperationException($"The property {nameof(OnAdd)} can't be set to Null");
/// <summary>
/// 保存按钮回调方法
/// </summary>
[Parameter]
public Func<TItem, bool> OnSave { get; set; } = item => false;
/// <summary>
/// 删除按钮回调方法
/// </summary>
[Parameter]
public Func<IEnumerable<TItem>, bool> OnDelete { get; set; } = item => false;
/// <summary>
/// 组件初始化方法
/// </summary>
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(Id)) throw new InvalidOperationException($"The property {nameof(Id)} can't be set to Null");
}
/// <summary>
/// 数据表格内删除按钮方法
/// </summary>
/// <param name="item"></param>
protected void Delete(TItem item)
{
if (Table != null)
{
Table.SelectedItems.Clear();
Table.SelectedItems.Add(item);
Table.Delete();
}
}
/// <summary>
///
/// </summary>
protected void Edit(TItem item)
{
if (Table != null)
{
Table.SelectedItems.Clear();
Table.SelectedItems.Add(item);
Table.Edit();
}
}
/// <summary>
///
/// </summary>
protected void Query()
{
// 查询控件按钮触发此事件
if (OnQuery != null && Table != null)
{
Table.Query(OnQuery.Invoke(1, Table.PageItems));
}
}
/// <summary>
///
/// </summary>
/// <param name="pageIndex"></param>
/// <param name="pageItems"></param>
/// <returns></returns>
protected QueryData<TItem> QueryData(int pageIndex, int pageItems) => OnQuery?.Invoke(pageIndex, pageItems) ?? new QueryData<TItem>();
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class HeaderBase : BootstrapComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public string UserName { get; set; } = "未设置";
/// <summary>
///
/// </summary>
[Parameter]
public string DisplayName { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public EventCallback<string>? DisplayNameChanged { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public bool IsAdmin { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public string Title { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public string Icon { get; set; } = "";
/// <summary>
/// 更新登录用户显示名称方法
/// </summary>
/// <param name="displayName"></param>
public void UpdateDisplayName(string displayName)
{
StateHasChanged();
}
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// IValidComponent 接口
/// </summary>
public interface IValidateComponent
{
/// <summary>
/// 数据验证方法
/// </summary>
/// <param name="propertyValue"></param>
/// <param name="context"></param>
/// <param name="results"></param>
void ValidateProperty(object? propertyValue, ValidationContext context, List<ValidationResult> results);
/// <summary>
/// 显示或者隐藏提示信息方法
/// </summary>
/// <param name="results"></param>
/// <param name="validProperty"></param>
void ToggleMessage(IEnumerable<ValidationResult> results, bool validProperty);
}
}

View File

@ -0,0 +1,116 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// LgbEditForm 组件
/// </summary>
public class LgbEditFormBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created <c>form</c> element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
/// <summary>
/// Specifies the top-level model object for the form. An edit context will
/// be constructed for this model. If using this parameter, do not also supply
/// a value for <see cref="EditContext"/>.
/// </summary>
[Parameter]
public object? Model { get; set; }
/// <summary>
/// Specifies the content to be rendered inside this
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// A callback that will be invoked when the form is submitted.
/// If using this parameter, you are responsible for triggering any validation
/// manually, e.g., by calling <see cref="EditContext.Validate"/>.
/// </summary>
[Parameter] public EventCallback<EditContext> OnSubmit { get; set; }
/// <summary>
/// A callback that will be invoked when the form is submitted and the
/// <see cref="EditContext"/> is determined to be valid.
/// </summary>
[Parameter] public EventCallback<EditContext> OnValidSubmit { get; set; }
/// <summary>
/// A callback that will be invoked when the form is submitted and the
/// <see cref="EditContext"/> is determined to be invalid.
/// </summary>
[Parameter] public EventCallback<EditContext> OnInvalidSubmit { get; set; }
/// <summary>
/// 验证组件缓存 静态全局提高性能
/// </summary>
private static ConcurrentDictionary<(LgbEditFormBase EditForm, Type ModelType, string FieldName), IValidateComponent> _validatorCache = new ConcurrentDictionary<(LgbEditFormBase, Type, string), IValidateComponent>();
/// <summary>
/// 添加数据验证组件到 EditForm 中
/// </summary>
/// <param name="key"></param>
/// <param name="comp"></param>
public void AddValidator((LgbEditFormBase EditForm, Type ModelType, string FieldName) key, IValidateComponent comp) => _validatorCache.AddOrUpdate(key, k => comp, (k, c) => c = comp);
/// <summary>
/// EditModel 数据模型验证方法
/// </summary>
/// <param name="model"></param>
/// <param name="context"></param>
/// <param name="results"></param>
public void ValidateObject(object model, ValidationContext context, List<ValidationResult> results)
{
// 遍历所有可验证组件进行数据验证
foreach (var key in _validatorCache)
{
if (key.Key.EditForm == this && key.Key.ModelType == context.ObjectType)
{
if (BootstrapAdminEditContextDataAnnotationsExtensions.TryGetValidatableProperty(new FieldIdentifier(model, key.Key.FieldName), out var propertyInfo))
{
// 设置其关联属性字段
var propertyValue = propertyInfo.GetValue(model);
context.MemberName = propertyInfo.Name;
var validator = _validatorCache[key.Key];
// 数据验证
validator.ValidateProperty(propertyValue, context, results);
validator.ToggleMessage(results, false);
}
}
}
}
/// <summary>
/// 字段验证方法
/// </summary>
/// <param name="propertyValue"></param>
/// <param name="context"></param>
/// <param name="results"></param>
public void ValidateProperty(object? propertyValue, ValidationContext context, List<ValidationResult> results)
{
if (_validatorCache.TryGetValue((this, context.ObjectType, context.MemberName), out var validator))
{
validator.ValidateProperty(propertyValue, context, results);
validator.ToggleMessage(results, true);
}
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// LgbInputText 组件
/// </summary>
public class LgbInputTextBase : ValidateInputBase<string>
{
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="result"></param>
/// <param name="validationErrorMessage"></param>
/// <returns></returns>
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = "";
return true;
}
/// <summary>
///
/// </summary>
protected int? MaxLength
{
get
{
if (Rules.Count == 0 &&
AdditionalAttributes != null &&
AdditionalAttributes.TryGetValue("maxlength", out var maxlength) &&
int.TryParse(Convert.ToString(maxlength), out int ml))
{
return ml;
}
return (Rules.FirstOrDefault(r => r is StringLengthValidator) as StringLengthValidator)?.Length;
}
}
}
}

View File

@ -0,0 +1,45 @@
using Bootstrap.Admin.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Linq.Expressions;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class LgbTableHeader<TItem> : ComponentBase
{
#nullable disable
/// <summary>
///
/// </summary>
[Parameter] public TItem Value { get; set; }
#nullable restore
/// <summary>
///
/// </summary>
[Parameter] public EventCallback<TItem> ValueChanged { get; set; }
/// <summary>
///
/// </summary>
[Parameter] public Expression<Func<TItem>>? ValueExpression { get; set; }
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var filed = FieldIdentifier.Create(ValueExpression);
var text = filed.GetDisplayName();
builder.OpenElement(0, "th");
builder.AddContent(1, text);
builder.CloseElement();
}
}
}

View File

@ -0,0 +1,133 @@
using Bootstrap.Admin.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 模态框组件类
/// </summary>
public class ModalBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? ModalBody { get; set; }
/// <summary>
/// 获得/设置 弹窗 Footer 代码块
/// </summary>
[Parameter]
public RenderFragment? ModalFooter { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public string Title { get; set; } = "未设置";
/// <summary>
/// 获得/设置 是否允许点击后台关闭弹窗 默认为 false
/// </summary>
[Parameter]
public bool Backdrop { get; set; }
/// <summary>
/// 获得/设置 弹窗大小
/// </summary>
[Parameter]
public ModalSize Size { get; set; }
/// <summary>
/// 获得/设置 是否垂直居中 默认为 true
/// </summary>
[Parameter]
public bool IsCentered { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示 Footer 默认为 true
/// </summary>
[Parameter]
public bool ShowFooter { get; set; } = true;
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
JSRuntime.InitModal();
}
}
/// <summary>
/// 输出窗口大小样式
/// </summary>
/// <returns></returns>
protected string RenderModalSize()
{
var ret = "";
switch (Size)
{
case ModalSize.Default:
break;
case ModalSize.Small:
ret = "modal-sm";
break;
case ModalSize.Large:
ret = "modal-lg";
break;
case ModalSize.ExtraLarge:
ret = "modal-xl";
break;
}
return ret;
}
/// <summary>
///
/// </summary>
public void Toggle()
{
JSRuntime.ToggleModal($"#{Id}");
}
}
/// <summary>
/// 弹窗大小
/// </summary>
public enum ModalSize
{
/// <summary>
///
/// </summary>
Default,
/// <summary>
///
/// </summary>
Small,
/// <summary>
///
/// </summary>
Large,
/// <summary>
///
/// </summary>
ExtraLarge,
}
}

View File

@ -0,0 +1,17 @@
using Bootstrap.Security;
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class NavItemBase : BootstrapComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public BootstrapMenu Menu { get; set; } = new BootstrapMenu();
}
}

View File

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class PageContent : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public string Name { get; set; } = "";
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var name = Name.SpanSplit("/").LastOrDefault();
if (!string.IsNullOrEmpty(name))
{
var t = Type.GetType($"Bootstrap.Admin.Pages.Admin.{name}");
if (t != null)
{
builder.OpenComponent(0, t);
builder.CloseComponent();
}
}
}
}
}

View File

@ -0,0 +1,23 @@
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class PageContentAttributes
{
/// <summary>
/// 获得/设置 页面 ID
/// </summary>
public string? Id { get; set; }
/// <summary>
/// 获得/设置 页面名称
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// 获得/设置 是否显示
/// </summary>
public bool Active { get; set; }
}
}

View File

@ -0,0 +1,117 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 分页组件基类
/// </summary>
public class PaginationBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public int TotalCount { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public int PageIndex { get; set; } = 1;
/// <summary>
///
/// </summary>
[Parameter]
public int PageItems { get; set; }
/// <summary>
/// 获得/设置 页码总数
/// </summary>
public int PageCount
{
get
{
return (int)Math.Ceiling(TotalCount * 1.0 / PageItems); ;
}
}
/// <summary>
///
/// </summary>
[Parameter]
public Action<int, int> OnPageClick { get; set; } = new Action<int, int>((pageIndex, pageItems) => { });
/// <summary>
///
/// </summary>
[Parameter]
public Action<int> OnPageItemsChange { get; set; } = new Action<int>((pageItems) => { });
/// <summary>
///
/// </summary>
protected void MovePrev()
{
if (PageIndex > 1) OnPageClick(PageIndex - 1, PageItems);
else OnPageClick(PageCount, PageItems);
}
/// <summary>
///
/// </summary>
protected void MoveNext()
{
if (PageIndex < PageCount) OnPageClick(PageIndex + 1, PageItems);
else OnPageClick(1, PageItems);
}
/// <summary>
///
/// </summary>
/// <param name="pageItems"></param>
protected void ClickItem(int pageItems)
{
PageItems = pageItems;
OnPageItemsChange(PageItems);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected IEnumerable<int> GetPages()
{
var pages = new List<int>() { 20, 40, 80, 100, 200 };
var ret = new List<int>();
for (int i = 0; i < pages.Count; i++)
{
ret.Add(pages[i]);
if (pages[i] >= TotalCount) break;
}
return ret;
}
/// <summary>
///
/// </summary>
protected int StartPageIndex { get; set; }
/// <summary>
///
/// </summary>
protected int EndPageIndex { get; set; }
/// <summary>
///
/// </summary>
protected override void OnParametersSet()
{
// 计算 分页开始页码与结束页码
StartPageIndex = Math.Max(1, PageIndex - 4);
EndPageIndex = Math.Min(PageCount, PageIndex + 5);
}
}
}

View File

@ -0,0 +1,84 @@
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 组件位置枚举类型
/// </summary>
public enum Placement
{
/// <summary>
///
/// </summary>
Default,
/// <summary>
///
/// </summary>
Top,
/// <summary>
///
/// </summary>
TopLeft,
/// <summary>
///
/// </summary>
TopRight,
/// <summary>
///
/// </summary>
Bottom,
/// <summary>
///
/// </summary>
BottomLeft,
/// <summary>
///
/// </summary>
BottomRight,
/// <summary>
///
/// </summary>
Center
}
/// <summary>
///
/// </summary>
public static class PlacementExtensions
{
/// <summary>
///
/// </summary>
/// <param name="placement"></param>
/// <param name="prefix"></param>
/// <returns></returns>
public static string ToCss(this Placement placement, string prefix = "")
{
string ret = "";
switch (placement)
{
case Placement.Center:
ret = "center";
break;
case Placement.Top:
ret = "top";
break;
case Placement.TopLeft:
ret = "top-left";
break;
case Placement.TopRight:
ret = "top-right";
break;
case Placement.Bottom:
ret = "bottom";
break;
case Placement.BottomLeft:
ret = "bottom-left";
break;
case Placement.BottomRight:
case Placement.Default:
ret = "bottom-right";
break;
}
return string.IsNullOrEmpty(prefix) ? ret : $"{prefix}-{ret}";
}
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Components;
using System;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 查询组件
/// </summary>
public class QueryBase<TItem> : ComponentBase
{
private readonly string _defaultTitle = "查询条件";
private readonly string _defaultText = "查询";
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
/// 查询组件标题 默认为 查询条件
/// </summary>
[Parameter]
public string Title { get; set; } = "";
/// <summary>
/// 查询按钮显示文字 默认为 查询
/// </summary>
[Parameter]
public string Text { get; set; } = "";
/// <summary>
/// 查询组件模板
/// </summary>
[Parameter]
public RenderFragment<TItem>? ChildContent { get; set; }
#nullable disable
/// <summary>
///
/// </summary>
[Parameter]
public TItem QueryModel { get; set; }
#nullable restore
/// <summary>
/// 查询按钮回调方法
/// </summary>
[Parameter]
public Action? OnQuery { get; set; }
/// <summary>
/// 参数设置方法
/// </summary>
protected override void OnParametersSet()
{
if (string.IsNullOrEmpty(Title)) Title = _defaultTitle;
if (string.IsNullOrEmpty(Text)) Text = _defaultText;
}
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class QueryData<T>
{
/// <summary>
///
/// </summary>
public IEnumerable<T> Items { get; set; } = new T[0];
/// <summary>
///
/// </summary>
public int TotalCount { get; set; }
/// <summary>
///
/// </summary>
public int PageIndex { get; set; } = 1;
/// <summary>
///
/// </summary>
public int PageItems { get; set; } = 20;
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class QueryInputTextBase<TItem> : LgbInputTextBase
{
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class RequiredValidator : ValidatorComponentBase
{
/// <summary>
///
/// </summary>
public RequiredValidator()
{
ErrorMessage = "这是必填字段";
}
/// <summary>
/// 获得/设置 是否允许空字符串 默认 false 不允许
/// </summary>
[Parameter]
public bool AllowEmptyString { get; set; }
/// <summary>
///
/// </summary>
/// <param name="propertyValue"></param>
/// <param name="context"></param>
/// <param name="results"></param>
public override void Validate(object? propertyValue, ValidationContext context, List<ValidationResult> results)
{
var val = propertyValue?.ToString() ?? "";
if (!AllowEmptyString && val == string.Empty)
{
results.Add(new ValidationResult(ErrorMessage, new string[] { context.MemberName }));
}
}
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class SectionBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public bool ShowBackground { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public int LockScreenPeriod { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public string ShowCardTitle { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// Select 组件基类
/// </summary>
public class SelectBase<TItem> : ValidateInputBase<TItem>
{
/// <summary>
/// 当前选择项实例
/// </summary>
public SelectedItem SelectedItem { get; set; } = new SelectedItem();
/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
[Parameter]
public List<SelectedItem> Items { get; set; } = new List<SelectedItem>();
/// <summary>
///
/// </summary>
protected override void OnParametersSet()
{
Items.ForEach(t =>
{
t.Active = t.Value == Value?.ToString();
if (t.Active) SelectedItem = t;
});
}
/// <summary>
///
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();
if (!SelectedItem.Active)
{
SelectedItem = Items.FirstOrDefault(item => item.Active) ?? Items.First();
}
}
/// <summary>
///
/// </summary>
[Parameter]
public Action<SelectedItem>? SelectedItemChanged { get; set; }
/// <summary>
///
/// </summary>
public void ItemClickCallback(SelectedItem item)
{
SelectedItem = item;
CurrentValueAsString = item.Value;
}
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Components;
using System;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class SelectItemBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public SelectedItem Item { get; set; } = new SelectedItem();
/// <summary>
///
/// </summary>
[Parameter]
public Action<SelectedItem> ItemClickCallback { get; set; } = new Action<SelectedItem>(SelectedItem => { });
}
}

View File

@ -0,0 +1,23 @@
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 选项类
/// </summary>
public class SelectedItem
{
/// <summary>
/// 获得/设置 显示名称
/// </summary>
public string Text { get; set; } = "";
/// <summary>
/// 获得/设置 选项值
/// </summary>
public string Value { get; set; } = "";
/// <summary>
/// 获得/设置 是否选中
/// </summary>
public bool Active { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using Bootstrap.Admin.Models;
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 侧边栏组件
/// </summary>
public class SideBarBase : BootstrapComponentBase
{
/// <summary>
/// 获得/设置 侧边栏绑定 Model 实例
/// </summary>
[Parameter]
public NavigatorBarModel Model { get; set; } = new NavigatorBarModel("");
/// <summary>
/// 侧边栏绑定 Model 改变事件
/// </summary>
[Parameter]
public EventCallback<NavigatorBarModel> ModelChanged { get; set; }
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class StringLengthValidator : ValidatorComponentBase
{
private int _length = 50;
/// <summary>
///
/// </summary>
[Parameter]
public int Length
{
get { return _length; }
set
{
_length = value;
ErrorMessage = $"不可为空,{_length}字以内";
}
}
/// <summary>
///
/// </summary>
/// <param name="propertyValue"></param>
/// <param name="context"></param>
/// <param name="results"></param>
public override void Validate(object? propertyValue, ValidationContext context, List<ValidationResult> results)
{
var val = propertyValue?.ToString() ?? "";
if (val.Length > Length) results.Add(new ValidationResult(ErrorMessage, new string[] { context.MemberName }));
}
}
}

View File

@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 数据绑定提交弹窗组件
/// </summary>
public class SubmitModalBase<TItem> : ModalBase
{
#nullable disable
/// <summary>
/// 获得/设置 弹窗绑定数据实体
/// </summary>
[Parameter]
public TItem Model { get; set; }
#nullable restore
/// <summary>
///
/// </summary>
[Parameter]
public EventCallback<TItem> ModelChanged { get; set; }
/// <summary>
/// A callback that will be invoked when the form is submitted.
/// If using this parameter, you are responsible for triggering any validation
/// manually, e.g., by calling <see cref="EditContext.Validate"/>.
/// </summary>
[Parameter] public EventCallback<EditContext> OnSubmit { get; set; }
/// <summary>
/// A callback that will be invoked when the form is submitted and the
/// <see cref="EditContext"/> is determined to be valid.
/// </summary>
[Parameter] public EventCallback<EditContext> OnValidSubmit { get; set; }
/// <summary>
/// A callback that will be invoked when the form is submitted and the
/// <see cref="EditContext"/> is determined to be invalid.
/// </summary>
[Parameter] public EventCallback<EditContext> OnInvalidSubmit { get; set; }
}
}

View File

@ -0,0 +1,81 @@
using Bootstrap.Admin.Extensions;
using Bootstrap.Security;
using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class TabBase : BootstrapComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public string Title { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public string? Id { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public string Url { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public string Icon { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public bool Active { get; set; }
private bool closeTab;
/// <summary>
///
/// </summary>
protected async Task CloseTab()
{
closeTab = true;
if (Layout.TabSet != null) await Layout.TabSet.CloseTab(Id);
}
/// <summary>
///
/// </summary>
protected void ClickTab()
{
if (!closeTab) NavigationManager?.NavigateTo(Url);
else closeTab = false;
}
/// <summary>
///
/// </summary>
/// <param name="menu"></param>
public void Init(BootstrapMenu menu)
{
Title = menu.Name;
Url = menu.Url.ToBlazorMenuUrl();
Icon = menu.Icon;
Id = menu.Id;
Active = true;
}
/// <summary>
///
/// </summary>
/// <param name="val"></param>
public void SetActive(bool val) => Active = val;
}
}

View File

@ -0,0 +1,150 @@
using Bootstrap.Admin.Extensions;
using Bootstrap.Admin.Shared;
using Bootstrap.Security;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// TabSet 组件类
/// </summary>
public class TabSetBase : BootstrapComponentBase
{
/// <summary>
/// 页面集合
/// </summary>
protected List<PageContentAttributes> Pages { get; set; } = new List<PageContentAttributes>();
/// <summary>
/// Tab 集合
/// </summary>
protected List<Tab> Tabs { get; set; } = new List<Tab>();
private string? curId;
/// <summary>
/// 页面呈现后回调方法
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
if (!string.IsNullOrEmpty(curId))
{
// Add Tab 后设置 active 状态
JSRuntime.ActiveMenu(curId);
curId = "";
}
}
/// <summary>
/// 添加 Tab 由侧边栏菜单调用
/// </summary>
/// <param name="menu"></param>
public void AddTab(BootstrapMenu menu)
{
var tab = Tabs.FirstOrDefault(tb => tb.Id == menu.Id);
if (tab == null)
{
tab = new Tab();
tab.Init(menu);
Tabs.ForEach(t => t.SetActive(false));
Tabs.Add(tab);
Pages.ForEach(p => p.Active = false);
Pages.Add(new PageContentAttributes() { Id = menu.Id, Name = menu.Url, Active = true });
}
else
{
Tabs.ForEach(t => t.SetActive(t.Id == tab.Id));
Pages.ForEach(p => p.Active = p.Id == tab.Id);
}
curId = tab.Id;
StateHasChanged();
}
/// <summary>
/// 关闭指定标签页方法
/// </summary>
/// <param name="tabId"></param>
/// <returns></returns>
public async Task CloseTab(string? tabId)
{
if (Tabs.Count == 1)
{
CloseAllTab();
return;
}
if (!string.IsNullOrEmpty(tabId))
{
var tab = Tabs.FirstOrDefault(tb => tb.Id == tabId);
if (tab != null)
{
// 移除 PageContent
var page = Pages.FirstOrDefault(p => p.Id == tab.Id);
if (page != null) Pages.Remove(page);
// 移除 Tab 返回下一个 TabId
tabId = await JSRuntime.RemoveTabAsync(tab.Id);
Tabs.Remove(tab);
tab = Tabs.FirstOrDefault(t => t.Id == tabId);
tab.SetActive(true);
page = Pages.FirstOrDefault(p => p.Id == tabId);
if (page != null) page.Active = true;
StateHasChanged();
}
}
}
/// <summary>
/// 关闭所有标签页方法
/// </summary>
protected void CloseAllTab()
{
NavigationManager?.NavigateTo(Layout.RootLayout.HomeUrl);
}
/// <summary>
/// 关闭当前标签页方法
/// </summary>
protected async Task CloseCurrentTab()
{
var tabId = Tabs.FirstOrDefault(t => t.Active)?.Id;
await CloseTab(tabId);
}
/// <summary>
/// 关闭其他标签页方法
/// </summary>
protected void CloseOtherTab()
{
var tabId = Tabs.FirstOrDefault(t => t.Active)?.Id;
if (!string.IsNullOrEmpty(tabId))
{
Tabs.RemoveAll(t => t.Id != tabId);
Pages.RemoveAll(page => page.Id != tabId);
curId = tabId;
StateHasChanged();
}
}
/// <summary>
/// 前移标签页方法
/// </summary>
protected async Task MovePrev()
{
var url = await JSRuntime.MovePrevTabAsync();
if (!string.IsNullOrEmpty(url)) NavigationManager?.NavigateTo(url);
}
/// <summary>
/// 后移标签页方法
/// </summary>
/// <returns></returns>
protected async Task MoveNext()
{
var url = await JSRuntime.MoveNextTabAsync();
if (!string.IsNullOrEmpty(url)) NavigationManager?.NavigateTo(url);
}
}
}

View File

@ -0,0 +1,339 @@
using Bootstrap.Admin.Extensions;
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 表格组件类
/// </summary>
public class TableBase<TItem> : ComponentBase
{
/// <summary>
///
/// </summary>
public const int DefaultPageItems = 20;
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment<TItem>? TableHeader { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment<TItem>? RowTemplate { get; set; }
/// <summary>
/// 按钮模板
/// </summary>
[Parameter]
public RenderFragment<TItem>? ButtonTemplate { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment<TItem>? EditTemplate { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? TableFooter { get; set; }
/// <summary>
///
/// </summary>
protected IEnumerable<TItem> Items { get; set; } = new TItem[0];
/// <summary>
///
/// </summary>
public List<TItem> SelectedItems { get; } = new List<TItem>();
/// <summary>
/// 获得/设置 是否显示行号
/// </summary>
[Parameter]
public bool ShowLineNo { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示选择列 默认为 true
/// </summary>
[Parameter]
public bool ShowCheckbox { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示按钮列 默认为 true
/// </summary>
[Parameter]
public bool ShowButtons { get; set; } = true;
/// <summary>
/// 获得/设置 是否显示工具栏 默认为 true
/// </summary>
[Parameter]
public bool ShowToolBar { get; set; } = true;
/// <summary>
/// 获得/设置 按钮列 Header 文本 默认为 操作
/// </summary>
[Parameter]
public string ButtonTemplateHeaderText { get; set; } = "操作";
/// <summary>
/// 点击翻页回调方法
/// </summary>
[Parameter]
public Func<int, int, QueryData<TItem>>? OnQuery { get; set; }
/// <summary>
/// 新建按钮回调方法
/// </summary>
[Parameter]
public Func<TItem>? OnAdd { get; set; }
/// <summary>
/// 编辑按钮回调方法
/// </summary>
[Parameter]
public Action<TItem>? OnEdit { get; set; }
/// <summary>
/// 保存按钮回调方法
/// </summary>
[Parameter]
public Func<TItem, bool>? OnSave { get; set; }
/// <summary>
/// 删除按钮回调方法
/// </summary>
[Parameter]
public Func<IEnumerable<TItem>, bool>? OnDelete { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public int TotalCount { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public int PageIndex { get; set; } = 1;
/// <summary>
///
/// </summary>
[Parameter]
public int PageItems { get; set; } = DefaultPageItems;
/// <summary>
/// 确认删除弹窗
/// </summary>
protected Modal? ConfirmModal { get; set; }
#nullable disable
/// <summary>
///
/// </summary>
protected TItem EditModel { get; set; }
#nullable restore
/// <summary>
/// 编辑数据弹窗
/// </summary>
protected SubmitModal<TItem>? EditModal { get; set; }
/// <summary>
///
/// </summary>
protected override void OnInitialized()
{
if (OnAdd != null) EditModel = OnAdd.Invoke();
if (OnQuery != null)
{
var queryData = OnQuery.Invoke(1, DefaultPageItems);
Items = queryData.Items;
TotalCount = queryData.TotalCount;
}
}
/// <summary>
///
/// </summary>
/// <param name="pageIndex"></param>
/// <param name="pageItems"></param>
protected void PageClick(int pageIndex, int pageItems)
{
if (pageIndex != PageIndex)
{
PageIndex = pageIndex;
PageItems = pageItems;
Query();
}
}
/// <summary>
///
/// </summary>
protected void PageItemsChange(int pageItems)
{
if (OnQuery != null)
{
PageIndex = 1;
PageItems = pageItems;
Query();
}
}
/// <summary>
///
/// </summary>
/// <param name="item"></param>
/// <param name="check"></param>
protected void ToggleCheck(TItem item, bool check)
{
if (item == null)
{
SelectedItems.Clear();
if (check) SelectedItems.AddRange(Items);
}
else
{
if (check) SelectedItems.Add(item);
else SelectedItems.Remove(item);
}
StateHasChanged();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected CheckBoxState CheckState(TItem item)
{
var ret = CheckBoxState.UnChecked;
if (SelectedItems.Count > 0)
{
ret = SelectedItems.Count == Items.Count() ? CheckBoxState.Checked : CheckBoxState.Mixed;
}
return ret;
}
/// <summary>
///
/// </summary>
public void Add()
{
if (OnAdd != null) EditModel = OnAdd.Invoke();
SelectedItems.Clear();
EditModal?.Toggle();
}
/// <summary>
/// Toast 组件实例
/// </summary>
protected Toast? Toast { get; set; }
/// <summary>
///
/// </summary>
/// <param name="title"></param>
/// <param name="text"></param>
/// <param name="cate"></param>
protected void ShowMessage(string title, string text, ToastCategory cate = ToastCategory.Success) => Toast?.ShowMessage(title, text, cate);
/// <summary>
///
/// </summary>
public void Edit()
{
if (SelectedItems.Count == 1)
{
EditModel = SelectedItems[0].Clone();
EditModal?.Toggle();
}
else
{
ShowMessage("编辑数据", "请选择一个要编辑的数据", ToastCategory.Information);
}
}
/// <summary>
/// 更新查询数据
/// </summary>
/// <param name="queryData"></param>
public void Query(QueryData<TItem> queryData)
{
SelectedItems.Clear();
PageIndex = queryData.PageIndex;
Items = queryData.Items;
TotalCount = queryData.TotalCount;
StateHasChanged();
}
private void Query()
{
if (OnQuery != null) Query(OnQuery.Invoke(PageIndex, PageItems));
}
/// <summary>
///
/// </summary>
/// <param name="context"></param>
protected void Save(EditContext context)
{
var valid = OnSave?.Invoke((TItem)context.Model) ?? false;
if (valid)
{
EditModal?.Toggle();
Query();
}
ShowMessage("保存数据", "保存数据" + (valid ? "成功" : "失败"), valid ? ToastCategory.Success : ToastCategory.Error);
}
/// <summary>
/// 删除按钮方法
/// </summary>
public void Delete()
{
if (SelectedItems.Count > 0)
{
ConfirmModal?.Toggle();
}
else
{
ShowMessage("删除数据", "请选择要删除的数据", ToastCategory.Information);
}
}
/// <summary>
/// 确认删除方法
/// </summary>
public void Confirm()
{
var result = OnDelete?.Invoke(SelectedItems) ?? false;
if (result)
{
ConfirmModal?.Toggle();
Query();
}
ShowMessage("删除数据", "删除数据" + (result ? "成功" : "失败"), result ? ToastCategory.Success : ToastCategory.Error);
}
}
}

View File

@ -0,0 +1,123 @@
using Bootstrap.Admin.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// Toast 组件基类
/// </summary>
public class ToastBase : ComponentBase
{
/// <summary>
/// IJSRuntime 实例
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
/// <summary>
/// 是否自动隐藏 默认为 true
/// </summary>
[Parameter]
public bool AutoHide { get; set; } = true;
/// <summary>
/// 自动隐藏延时 默认为 1500 毫秒
/// </summary>
[Parameter]
public int Interval { get; set; } = 2000;
/// <summary>
/// 组件显示位置
/// </summary>
[Parameter]
public Placement Placement { get; set; }
/// <summary>
/// Toast 类型
/// </summary>
[Parameter]
public ToastCategory Category { get; set; }
/// <summary>
/// 组件标题文本
/// </summary>
[Parameter]
public string Title { get; set; } = "BootstrapAdmin Blazor";
/// <summary>
/// 消息正文
/// </summary>
[Parameter]
public string Text { get; set; } = "Toast 消息正文内容-未设置";
/// <summary>
/// 控件呈现后回调方法
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
JSRuntime.InitToast();
}
if (_show)
{
_show = false;
Show();
}
}
/// <summary>
///
/// </summary>
protected void Show()
{
JSRuntime.ShowToast();
}
private bool _show;
/// <summary>
///
/// </summary>
public void ShowMessage(string title, string text, ToastCategory category = ToastCategory.Success)
{
Title = title;
Text = text;
Category = category;
_show = true;
StateHasChanged();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected string RenderCategory()
{
var ret = "";
switch (Category)
{
case ToastCategory.Error:
ret = "fa fa-times-circle text-danger";
break;
case ToastCategory.Information:
ret = "fa fa-exclamation-triangle text-info";
break;
case ToastCategory.Success:
ret = "fa fa-check-circle text-success";
break;
}
return ret;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
protected string RenderAnimation()
{
return AutoHide ? $"transition: width {(Interval * 1.0 - 200) / 1000}s linear;" : "";
}
}
}

View File

@ -0,0 +1,21 @@
namespace Bootstrap.Admin.Components
{
/// <summary>
/// Toast 组件类型
/// </summary>
public enum ToastCategory
{
/// <summary>
/// 成功
/// </summary>
Success,
/// <summary>
/// 提示信息
/// </summary>
Information,
/// <summary>
/// 错误信息
/// </summary>
Error
}
}

View File

@ -0,0 +1,187 @@
using Bootstrap.Admin.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public abstract class ValidateInputBase<TItem> : InputBase<TItem>, IValidateComponent
{
/// <summary>
///
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
/// <summary>
///
/// </summary>
[CascadingParameter]
public LgbEditFormBase? EditForm { get; set; }
/// <summary>
///
/// </summary>
public string Id
{
get { return $"{EditForm?.Id}_{FieldIdentifier.FieldName}"; }
}
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
///
/// </summary>
protected string? PlaceHolder
{
get
{
if (AdditionalAttributes != null &&
AdditionalAttributes.TryGetValue("placeholder", out var ph) &&
!string.IsNullOrEmpty(Convert.ToString(ph)))
{
return ph.ToString();
}
return null;
}
}
/// <summary>
///
/// </summary>
protected string ErrorMessage { get; set; } = "";
/// <summary>
///
/// </summary>
protected string ValidCss { get; set; } = "";
/// <summary>
///
/// </summary>
protected string DisplayName { get; set; } = "-";
/// <summary>
///
/// </summary>
protected override void OnInitialized()
{
EditForm?.AddValidator((EditForm, FieldIdentifier.Model.GetType(), FieldIdentifier.FieldName), this);
DisplayName = FieldIdentifier.GetDisplayName();
}
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
if (!string.IsNullOrEmpty(_tooltipMethod) && !string.IsNullOrEmpty(Id))
{
JSRuntime.Tooltip(Id, _tooltipMethod);
_tooltipMethod = "";
}
}
/// <summary>
///
/// </summary>
public ICollection<ValidatorComponentBase> Rules { get; } = new HashSet<ValidatorComponentBase>();
private string _tooltipMethod = "";
/// <summary>
///
/// </summary>
/// <param name="propertyValue"></param>
/// <param name="context"></param>
/// <param name="results"></param>
public void ValidateProperty(object? propertyValue, ValidationContext context, List<ValidationResult> results)
{
Rules.ToList().ForEach(validator => validator.Validate(propertyValue, context, results));
}
/// <summary>
/// 显示/隐藏验证结果方法
/// </summary>
/// <param name="results"></param>
/// <param name="validProperty">是否对本属性进行数据验证</param>
public void ToggleMessage(IEnumerable<ValidationResult> results, bool validProperty)
{
if (Rules.Any())
{
var messages = results.Where(item => item.MemberNames.Any(m => m == FieldIdentifier.FieldName));
if (messages.Any())
{
ErrorMessage = messages.First().ErrorMessage;
ValidCss = "is-invalid";
// 控件自身数据验证时显示 tooltip
// EditForm 数据验证时调用 tooltip('enable') 保证 tooltip 组件生成
// 调用 tooltip('hide') 后导致鼠标悬停时 tooltip 无法正常显示
_tooltipMethod = validProperty ? "show" : "enable";
}
else
{
ErrorMessage = "";
ValidCss = "is-valid";
_tooltipMethod = "dispose";
}
}
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="result"></param>
/// <param name="validationErrorMessage"></param>
/// <returns></returns>
protected override bool TryParseValueFromString(string value, out TItem result, out string? validationErrorMessage)
{
if (typeof(TItem) == typeof(string))
{
result = (TItem)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(TItem).IsEnum)
{
var success = BindConverter.TryConvertTo<TItem>(value, CultureInfo.CurrentCulture, out var parsedValue);
if (success)
{
result = parsedValue;
validationErrorMessage = null;
return true;
}
else
{
#nullable disable
result = default;
#nullable restore
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
}
else if (typeof(TItem).IsValueType)
{
result = (TItem)Convert.ChangeType(value, typeof(TItem));
validationErrorMessage = null;
return true;
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TItem)}'.");
}
}
}

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public abstract class ValidatorComponentBase : ComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public string ErrorMessage { get; set; } = "";
/// <summary>
///
/// </summary>
[CascadingParameter]
public LgbInputTextBase? Input { get; set; }
/// <summary>
/// 初始化方法
/// </summary>
protected override void OnInitialized()
{
if (Input == null)
{
throw new InvalidOperationException($"{nameof(ValidatorComponentBase)} requires a cascading " +
$"parameter of type {nameof(LgbInputTextBase)}. For example, you can use {nameof(ValidatorComponentBase)} " +
$"inside an LgbInputText.");
}
Input.Rules.Add(this);
}
/// <summary>
///
/// </summary>
/// <param name="propertyValue"></param>
/// <param name="context"></param>
/// <param name="results"></param>
public abstract void Validate(object? propertyValue, ValidationContext context, List<ValidationResult> results);
}
}

View File

@ -19,6 +19,6 @@ namespace Bootstrap.Admin.Controllers.Api
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}")]
public IEnumerable<App> Get(string id) => AppHelper.RetrievesByRoleId(id);
public IEnumerable<DataAccess.App> Get(string id) => AppHelper.RetrievesByRoleId(id);
}
}

View File

@ -3,6 +3,7 @@ using Bootstrap.DataAccess;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace Bootstrap.Admin.Controllers
{
@ -16,11 +17,12 @@ namespace Bootstrap.Admin.Controllers
/// Index View
/// </summary>
/// <returns></returns>
public IActionResult Index()
public IActionResult Index([FromServices]IConfiguration configuration)
{
var model = new HeaderBarModel(User.Identity.Name);
var homeUrl = DictHelper.RetrieveHomeUrl(model.AppId);
return homeUrl.Equals("~/Home/Index", System.StringComparison.OrdinalIgnoreCase) ? (IActionResult)View(model) : Redirect(homeUrl);
var useBlazor = configuration.GetValue("UseBlazor", false);
return homeUrl.Equals("~/Home/Index", System.StringComparison.OrdinalIgnoreCase) ? (useBlazor ? Redirect("~/Pages") : (IActionResult)View(model)) : Redirect(homeUrl);
}
/// <summary>
@ -29,9 +31,9 @@ namespace Bootstrap.Admin.Controllers
/// <param name="id"></param>
/// <returns></returns>
[AllowAnonymous]
public IActionResult Error(int id)
public IActionResult Error(int? id = 0)
{
var model = ErrorModel.CreateById(id);
var model = ErrorModel.CreateById(id ?? 0);
if (id != 403)
{
var returnUrl = Request.Query[CookieAuthenticationDefaults.ReturnUrlParameter].ToString();

View File

@ -0,0 +1,17 @@
using Microsoft.Extensions.Configuration;
namespace Bootstrap.Admin.Extensions
{
/// <summary>
/// 配置文件静态操作类
/// </summary>
public static class ConfigurationHelperExtensions
{
/// <summary>
/// 获得本站 AppId
/// </summary>
/// <param name="configuration"></param>
/// <returns></returns>
public static string GetAppId(this IConfiguration configuration) => configuration.GetValue("AppId", "BA");
}
}

View File

@ -0,0 +1,46 @@
using Bootstrap.Security;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
///
/// </summary>
public static class DisplayNamesExtensions
{
private static ConcurrentDictionary<(Type ModelType, string FieldName), string> _displayNameCache = new ConcurrentDictionary<(Type, string), string>();
/// <summary>
/// 向系统中加入实体类显示名称字典
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddDisplayNames(this IServiceCollection services)
{
_displayNameCache.TryAdd((typeof(BootstrapDict), nameof(BootstrapDict.Category)), "字典标签");
_displayNameCache.TryAdd((typeof(BootstrapDict), nameof(BootstrapDict.Name)), "字典名称");
_displayNameCache.TryAdd((typeof(BootstrapDict), nameof(BootstrapDict.Code)), "字典代码");
_displayNameCache.TryAdd((typeof(BootstrapDict), nameof(BootstrapDict.Define)), "字典类型");
return services;
}
/// <summary>
///
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="displayName"></param>
/// <returns></returns>
public static bool TryGetValue((Type ModelType, string FieldName) cacheKey, out string? displayName) => _displayNameCache.TryGetValue(cacheKey, out displayName);
/// <summary>
///
/// </summary>
/// <param name="cacheKey"></param>
/// <param name="valueFactory"></param>
/// <returns></returns>
public static string GetOrAdd((Type ModelType, string FieldName) cacheKey, Func<(Type, string), string> valueFactory) => _displayNameCache.GetOrAdd(cacheKey, valueFactory);
}
}

View File

@ -0,0 +1,38 @@
using Bootstrap.Admin.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Forms;
using System.ComponentModel;
namespace Bootstrap.Admin.Extensions
{
/// <summary>
///
/// </summary>
public static class FieldIdentifierExtensions
{
/// <summary>
///
/// </summary>
/// <param name="fieldIdentifier"></param>
/// <returns></returns>
public static string GetDisplayName(this FieldIdentifier fieldIdentifier)
{
var cacheKey = (fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!DisplayNamesExtensions.TryGetValue(cacheKey, out var dn))
{
if (BootstrapAdminEditContextDataAnnotationsExtensions.TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var displayNameAttribute = propertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), true);
if (displayNameAttribute.Length > 0)
{
dn = ((DisplayNameAttribute)displayNameAttribute[0]).DisplayName;
// add display name into cache
DisplayNamesExtensions.GetOrAdd((fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName), key => dn);
}
}
}
return dn ?? "未设置";
}
}
}

View File

@ -0,0 +1,83 @@
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Bootstrap.Admin.Extensions
{
/// <summary>
/// JSRuntime 扩展操作类
/// </summary>
public static class JSRuntimeExtensions
{
/// <summary>
/// 根据指定菜单 ID 激活侧边栏菜单项
/// </summary>
/// <param name="jsRuntime"></param>
/// <param name="menuId"></param>
/// <returns></returns>
public static void ActiveMenu(this IJSRuntime? jsRuntime, string? menuId)
{
if (!string.IsNullOrEmpty(menuId) && jsRuntime != null) jsRuntime.InvokeVoidAsync("$.activeMenu", menuId);
}
/// <summary>
/// 导航条前移一个 Tab
/// </summary>
/// <param name="jSRuntime"></param>
/// <returns></returns>
public static async ValueTask<string> MovePrevTabAsync(this IJSRuntime? jSRuntime) => jSRuntime == null ? "" : await jSRuntime.InvokeAsync<string>("$.movePrevTab");
/// <summary>
/// 导航条后移一个 Tab
/// </summary>
/// <param name="jSRuntime"></param>
/// <returns></returns>
public static async ValueTask<string> MoveNextTabAsync(this IJSRuntime? jSRuntime) => jSRuntime == null ? "" : await jSRuntime.InvokeAsync<string>("$.moveNextTab");
/// <summary>
/// 移除指定 ID 的导航条
/// </summary>
/// <param name="jSRuntime"></param>
/// <param name="tabId"></param>
/// <returns></returns>
public static async ValueTask<string> RemoveTabAsync(this IJSRuntime? jSRuntime, string? tabId) => string.IsNullOrEmpty(tabId) || jSRuntime == null ? "" : await jSRuntime.InvokeAsync<string>("$.removeTab", tabId);
/// <summary>
/// 启用动画
/// </summary>
/// <param name="jSRuntime"></param>
public static void InitDocument(this IJSRuntime? jSRuntime) => jSRuntime.InvokeVoidAsync("$.initDocument");
/// <summary>
/// 修复 Modal 组件
/// </summary>
/// <param name="jSRuntime"></param>
public static void InitModal(this IJSRuntime? jSRuntime) => jSRuntime.InvokeVoidAsync("$.initModal");
/// <summary>
/// 修复 Modal 组件
/// </summary>
/// <param name="jSRuntime"></param>
public static void InitToast(this IJSRuntime? jSRuntime) => jSRuntime.InvokeVoidAsync("$.initToast");
/// <summary>
/// 弹出 Modal 组件
/// </summary>
/// <param name="jSRuntime"></param>
/// <param name="modalId"></param>
public static void ToggleModal(this IJSRuntime? jSRuntime, string modalId) => jSRuntime.InvokeVoidAsync("$.toggleModal", modalId);
/// <summary>
/// 弹出 Toast 组件
/// </summary>
/// <param name="jSRuntime"></param>
public static void ShowToast(this IJSRuntime? jSRuntime) => jSRuntime.InvokeVoidAsync("$.showToast");
/// <summary>
/// 弹出 Tooltip 组件
/// </summary>
/// <param name="jSRuntime"></param>
/// <param name="id"></param>
/// <param name="method"></param>
public static void Tooltip(this IJSRuntime? jSRuntime, string id, string method) => jSRuntime.InvokeVoidAsync("$.tooltip", $"#{id}", method);
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Linq;
namespace Bootstrap.Admin.Extensions
{
/// <summary>
///
/// </summary>
public static class ObjectExtensions
{
/// <summary>
///
/// </summary>
/// <typeparam name="TItem"></typeparam>
/// <param name="item"></param>
/// <returns></returns>
public static TItem Clone<TItem>(this TItem item)
{
var ret = item;
if (item != null)
{
var type = item.GetType();
if (typeof(ICloneable).IsAssignableFrom(type))
{
var clv = type.GetMethod("Clone")?.Invoke(type, null);
if (clv != null) ret = (TItem)clv;
}
if (type.IsClass)
{
ret = Activator.CreateInstance<TItem>();
var valType = ret?.GetType();
if (valType != null)
{
type.GetProperties().ToList().ForEach(p =>
{
var v = p.GetValue(item);
valType.GetProperty(p.Name)?.SetValue(ret, v);
});
}
}
}
return ret;
}
}
}

View File

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
namespace Bootstrap.Admin.Extensions
{
/// <summary>
/// Url 地址辅助操作类
/// </summary>
public static class UrlHelperExtensions
{
/// <summary>
/// 转换为 Blazor 地址 ~/Admin/Index => /Admin/Index
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string ToBlazorLink(this string url) => url.TrimStart('~');
/// <summary>
/// 转化为 Blazor 菜单地址 ~/Admin/Index => /Pages/Admin/Index
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string ToBlazorMenuUrl(this string url) => url.Replace("~", "/Pages");
/// <summary>
/// 转化为 Mvc 菜单地址 /Pages/Admin/Index => ~/Admin/Index
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string ToMvcMenuUrl(this string url)
{
var index = new List<string>() { "/Pages", "/Pages/Admin" };
return index.Any(u => u.Contains(url, System.StringComparison.OrdinalIgnoreCase)) ? "~/Admin/Index" : url.Replace("/Pages", "~");
}
}
}

View File

@ -47,7 +47,7 @@ namespace Bootstrap.Admin.Models
/// <summary>
/// 获得 当前用户显示名称
/// </summary>
public string DisplayName { get; } = "";
public string DisplayName { get; set; } = "";
/// <summary>
/// 获得 用户头像地址

View File

@ -16,10 +16,20 @@ namespace Bootstrap.Admin.Models
/// 构造函数
/// </summary>
/// <param name="controller"></param>
public NavigatorBarModel(ControllerBase controller) : base(controller.User.Identity.Name)
public NavigatorBarModel(ControllerBase controller) : this(controller.User.Identity.Name)
{
Navigations = MenuHelper.RetrieveSystemMenus(UserName, $"~{controller.HttpContext.Request.Path}");
var authApps = AppHelper.RetrievesByUserName(controller.User.Identity.Name);
}
/// <summary>
/// Blazor 使用构造函数
/// </summary>
/// <param name="userName"></param>
/// <param name="activeUrl"></param>
public NavigatorBarModel(string? userName, string activeUrl = "~/Admin/Index") : base(userName)
{
Navigations = MenuHelper.RetrieveSystemMenus(UserName, activeUrl);
var authApps = AppHelper.RetrievesByUserName(userName);
Applications = DictHelper.RetrieveApps().Where(app => app.Key.IsNullOrEmpty() || authApps.Any(key => key.Equals(app.Key, StringComparison.OrdinalIgnoreCase)));
}

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Linq;
@ -50,10 +51,19 @@ namespace Bootstrap.Admin.Models
}
}
if (controller.User.Identity.AuthenticationType != Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme) External = true;
if (controller.User.Identity.AuthenticationType != CookieAuthenticationDefaults.AuthenticationScheme) External = true;
// 设置 当前用户默认应用名称
AppName = Applications.FirstOrDefault(app => app.Key == AppId).Value;
}
/// <summary>
/// 构造函数 Blazor 页面调用
/// </summary>
public ProfilesModel(string? userName) :base(userName)
{
// 设置 当前用户默认应用名称
AppName = Applications.FirstOrDefault(app => app.Key == AppId).Value;
}
}
}

View File

@ -20,6 +20,15 @@ namespace Bootstrap.Admin.Models
AutoLockScreen = EnableAutoLockScreen ? "" : "lockScreen";
}
/// <summary>
/// 构造函数 Blazor 使用
/// </summary>
public SettingsModel(string? userName) : base(userName)
{
Themes = DictHelper.RetrieveThemes();
AutoLockScreen = EnableAutoLockScreen ? "" : "lockScreen";
}
/// <summary>
/// 获得 系统配置的所有样式表
/// </summary>

View File

@ -0,0 +1,38 @@
@inherits DictsBase
<EditPage Id="dict" TItem="Bootstrap.Security.BootstrapDict" QueryModel="QueryModel" OnQuery="Query" OnAdd="Add" OnDelete="Delete" OnSave="Save">
<QueryBody>
<LgbInputText @bind-Value="@context.Category" maxlength="50" />
<Select Items="QueryDefine" TItem="int" @bind-Value="@context.Define" />
<LgbInputText @bind-Value="@context.Name" maxlength="50" />
</QueryBody>
<TableHeader>
<LgbTableHeader TItem="string" @bind-Value="@context.Category"></LgbTableHeader>
<LgbTableHeader TItem="string" @bind-Value="@context.Name"></LgbTableHeader>
<LgbTableHeader TItem="string" @bind-Value="@context.Code"></LgbTableHeader>
<LgbTableHeader TItem="int" @bind-Value="@context.Define"></LgbTableHeader>
</TableHeader>
<RowTemplate>
<td>@context.Category</td>
<td>@context.Name</td>
<td>@context.Code</td>
<td>@(DefineItems.FirstOrDefault(d => d.Value == context.Define.ToString())?.Text ?? "-")</td>
</RowTemplate>
<EditTemplate>
<div class="row">
<LgbInputText @bind-Value="@context.Category" placeholder="不可为空50字以内" maxlength="50">
<RequiredValidator />
<StringLengthValidator Length="50" />
</LgbInputText>
<Select Items="DefineItems" TItem="int" @bind-Value="@context.Define"></Select>
<LgbInputText @bind-Value="@context.Name" placeholder="不可为空50字以内" maxlength="50">
<RequiredValidator />
<StringLengthValidator Length="50" />
</LgbInputText>
<LgbInputText @bind-Value="@context.Code" placeholder="不可为空2000字以内" maxlength="2000">
<RequiredValidator />
<StringLengthValidator Length="2000" />
</LgbInputText>
</div>
</EditTemplate>
</EditPage>

View File

@ -0,0 +1,6 @@
<label class="control-label" for="groupName">部门名称</label>
<input type="text" class="form-control" id="groupName" placeholder="不可为空50字以内" maxlength="50" data-valid="true" />
@code {
}

View File

@ -0,0 +1,21 @@
@page "/Pages/Admin/Dicts"
@page "/Pages/Admin/Groups"
@page "/Pages/Account/Lock"
@page "/Pages/Admin/Menus"
@page "/Pages/Admin/Profiles"
@page "/Pages/Admin/Roles"
@page "/Pages/Admin/Users"
@page "/Pages/Admin/Settings"
@page "/Pages/Home/Index"
@page "/Pages/Admin/Messages"
@page "/Pages/Admin/Tasks"
@page "/Pages/Admin/Notifications"
@page "/Pages/Admin/Logs"
@page "/Pages/Admin/Logins"
@page "/Pages/Admin/Traces"
@page "/Pages/Admin/SQL"
@page "/Pages/Admin/Online"
@page "/Pages/Admin/Analyse"
@page "/Pages/Admin/Exceptions"
@page "/Pages/Admin/Healths"
@page "/Pages/Admin/Mobile"

View File

@ -0,0 +1,6 @@

<h3>Lock</h3>
@code {
}

View File

@ -0,0 +1,6 @@

<h3>Menus</h3>
@code {
}

View File

@ -0,0 +1,133 @@
@inherits BootstrapComponentBase
<div class="card" asp-auth="saveDisplayName">
<div class="card-header">基本资料</div>
<div class="card-body" data-toggle="LgbValidate" data-valid-button="#btnSaveDisplayName">
@if (IsDemo)
{
<div class="alert alert-danger" role="alert">
<span>演示系统禁止更改管理员显示名称</span>
</div>
}
<form class="form-inline">
<div class="row">
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="userName">登录名称</label>
<input type="text" class="form-control ignore" value="@Model?.UserName" readonly />
</div>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="DisplayName">显示名称</label>
<input type="text" class="form-control" @bind="Layout.DisplayName" placeholder="不可为空20字以内" maxlength="20" data-valid="true" />
</div>
</div>
</form>
@if (!IsDemo)
{
<div class="modal-footer">
<button id="btnSaveDisplayName" data-method="user" class="btn btn-secondary" type="button" @onclick="SaveDisplayName"><i class="fa fa-save"></i><span>保存</span></button>
</div>
}
</div>
</div>
<div class="card" asp-auth="savePassword" asp-condition="!@Model?.External">
<div class="card-header">修改密码</div>
<div class="card-body" data-toggle="LgbValidate" data-valid-button="#btnSavePassword">
<div class="alert alert-danger" role="alert" asp-condition="@Model?.IsDemo">
<span>演示系统禁止更改管理员密码</span>
</div>
<form class="form-inline">
<div class="row">
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="currentPassword">原密码: </label>
<input type="password" class="form-control" id="currentPassword" autocomplete="off" placeholder="原密码" maxlength="16" data-valid="true" />
</div>
</div>
<div class="row">
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="newPassword">新密码: </label>
<input type="password" class="form-control" id="newPassword" autocomplete="off" placeholder="新密码" maxlength="16" data-valid="true" />
</div>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="confirmPassword">确认密码: </label>
<input type="password" class="form-control" id="confirmPassword" autocomplete="off" placeholder="与新密码一致" maxlength="16" equalTo="#newPassword" data-valid="true" />
</div>
</div>
</form>
<div class="modal-footer" asp-condition="!@Model?.IsDemo">
<button id="btnSavePassword" data-method="password" class="btn btn-secondary" type="button"><i class="fa fa-save"></i><span>保存</span></button>
</div>
</div>
</div>
<div class="card" asp-auth="saveApp">
<div class="card-header">默认应用</div>
<div class="card-body">
<div class="form-group">
<div class="btn-group" role="group">
<button id="app" class="btn btn-success dropdown-select dropdown-toggle" data-toggle="dropdown" value="@Model?.AppId">@Model?.AppName</button>
<div class="dropdown-menu">
@foreach (var app in Model?.Applications ?? new KeyValuePair<string, string>[0])
{
<a href="#" data-val="@app.Key">@app.Value</a>
}
</div>
</div>
</div>
<div class="modal-footer">
<button id="btnSaveApp" data-method="app" class="btn btn-secondary" type="button"><i class="fa fa-save"></i><span>保存</span></button>
</div>
</div>
</div>
<div class="card" asp-auth="saveTheme">
<div class="card-header">网站样式</div>
<div class="card-body">
<div class="alert alert-info" role="alert">
<span>注意:本设置将覆盖<b><a class="badge-pill" href="./Settings">网站设置</a></b>中设置的网站样式</span>
</div>
<div class="form-group">
<div class="btn-group" role="group">
<button id="css" class="btn btn-success dropdown-select dropdown-toggle" data-toggle="dropdown" data-default-val="" value="@Model?.Css">默认样式</button>
<div class="dropdown-menu">
<a href="#" data-val="">默认样式</a>
@foreach (var css in Model?.Themes ?? new Bootstrap.Security.BootstrapDict[0])
{
<a href="#" data-val="@css.Code">@css.Name</a>
}
</div>
</div>
</div>
<div class="modal-footer">
<button id="btnSaveCss" data-method="profileCss" class="btn btn-secondary" type="button"><i class="fa fa-save"></i><span>保存</span></button>
</div>
</div>
</div>
<div class="card" asp-auth="saveIcon">
<div class="card-header">修改头像</div>
<div class="card-body">
<form enctype="multipart/form-data">
<div class="form-group">
<input id="fileIcon" type="file" data-init="@Model?.Size" data-file="@Model?.FileName">
</div>
</form>
<img class="card-img d-none" src="@Model?.Icon.ToBlazorLink()" />
</div>
</div>
@code {
private ProfilesModel? Model;
private bool IsDemo;
protected override void OnInitialized()
{
base.OnInitialized();
Model = new ProfilesModel(Layout?.UserName);
IsDemo = Model?.IsDemo ?? false;
}
private void SaveDisplayName()
{
if (!string.IsNullOrEmpty(Model?.UserName) && Layout != null && !string.IsNullOrEmpty(Layout.DisplayName))
{
Bootstrap.DataAccess.UserHelper.SaveDisplayName(Model.UserName, Layout.DisplayName);
}
}
}

View File

@ -0,0 +1,6 @@

<h3>Roles</h3>
@code {
}

View File

@ -0,0 +1,6 @@

<h3>Settings</h3>
@code {
}

View File

@ -0,0 +1,41 @@

@using System.Globalization
<h1 style="font-style:@_headingFontStyle">@_headingText</h1>
<form>
<div>
@*
A check box sets the font style and is bound to the
_italicsCheck field.
*@
<input type="checkbox" id="italicsCheck"
@bind="_italicsCheck" />
<label class="form-check-label"
for="italicsCheck">Use italics</label>
</div>
@*
When the form is submitted, the onclick event executes
the UpdateHeading method.
*@
<button type="button" class="btn btn-primary" @onclick="UpdateHeading">
Update heading
</button>
</form>
@code {
private static TextInfo _tinfo = CultureInfo.CurrentCulture.TextInfo;
private string _headingText =
_tinfo.ToTitleCase("welcome to blazor!");
private string _headingFontStyle = "normal";
private bool _italicsCheck = false;
// When UpdateHeading is executed, _italicsCheck determines
// the value of _headingFontStyle to set the font style of the
// heading.
public void UpdateHeading()
{
_headingFontStyle = _italicsCheck ? "italic" : "normal";
}
}

View File

@ -0,0 +1,2 @@
@layout AdminLayout
@attribute [Authorize]

View File

@ -0,0 +1,25 @@
@page "/Pages"
@page "/Pages/Admin"
@page "/Pages/Admin/Index"
@layout DefaultLayout
@attribute [Authorize]
@inject IJSRuntime JSRuntime
<Section ShowCardTitle="@Layout.Model.ShowCardTitle" LockScreenPeriod="@Layout.Model.LockScreenPeriod" ShowBackground="true">
<h4 style="color: #fff">欢迎使用后台管理</h4>
</Section>
@code {
[CascadingParameter(Name = "Default")]
protected DefaultLayout Layout { get; set; } = new DefaultLayout();
/// <summary>
///
/// </summary>
protected override void OnAfterRender(bool firstRender)
{
var menus = DataAccess.MenuHelper.RetrieveAllMenus(Layout.UserName);
var menu = menus.FirstOrDefault(menu => "/Pages/Admin/Index".Contains(menu.Url.ToBlazorMenuUrl(), StringComparison.OrdinalIgnoreCase));
JSRuntime.ActiveMenu(menu.Id);
}
}

View File

@ -0,0 +1,54 @@
@page "/Pages"
@namespace Bootstrap.Admin.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="~/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="~/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="~/favicon.png" />
<base href="~/" />
<title>通用后台管理框架 Blazor 版</title>
<environment include="Development">
<link href="~/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
<link href="~/lib/font-awesome/css/font-awesome.css" rel="stylesheet" />
<link href="~/lib/overlayscrollbars/OverlayScrollbars.css" rel="stylesheet" />
</environment>
<environment exclude="Development">
<link href="~/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<link href="~/lib/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<link href="~/lib/overlayscrollbars/OverlayScrollbars.min.css" rel="stylesheet" />
</environment>
<link href="~/lib/captcha/slidercaptcha.css" rel="stylesheet" />
<link href="~/lib/longbow-select/longbow-select.css" rel="stylesheet" />
<link href="~/lib/longbow-checkbox/longbow-checkbox.css" rel="stylesheet" />
<link href="~/css/theme.css" rel="stylesheet" asp-append-version="true" />
<link href="~/css/theme-responsive.css" rel="stylesheet" asp-append-version="true" />
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
<link href="~/css/site-responsive.css" rel="stylesheet" asp-append-version="true" />
<link href="~/css/lte.css" rel="stylesheet" asp-append-version="true" />
<link href="~/css/blazor.css" rel="stylesheet" asp-append-version="true" />
</head>
<body class="trans-mute">
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<environment include="Development">
<script src="~/lib/jquery/jquery.js"></script>
<script src="~/lib/twitter-bootstrap/js/bootstrap.bundle.js"></script>
<script src="~/lib/overlayscrollbars/jquery.overlayScrollbars.js"></script>
</environment>
<environment exclude="Development">
<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/twitter-bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/overlayscrollbars/jquery.overlayScrollbars.min.js"></script>
</environment>
<script src="~/js/ba.blazor.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

View File

@ -0,0 +1,8 @@
@inherits AdminLayoutComponentBase
@layout DefaultLayout
<CascadingValue Value=this Name="Admin">
<Section ShowCardTitle="@RootLayout.Model.ShowCardTitle" LockScreenPeriod=@RootLayout.Model.LockScreenPeriod>
<TabSet @ref="TabSet"></TabSet>
</Section>
</CascadingValue>

View File

@ -0,0 +1,14 @@
@inherits AlertBase
<Modal Backdrop="@Backdrop" IsCentered="@IsCentered" Size="@Size" Id="@Id" Title="@Title" ShowFooter="@ShowFooter">
<ModalBody>
@AlertBody
</ModalBody>
<ModalFooter>
@AlertFooter
</ModalFooter>
</Modal>
@code {
}

View File

@ -0,0 +1,9 @@
@typeparam TItem
@inherits CheckboxBase<TItem>
<label role="checkbox" aria-checked="@State.ToCss()" class="@RenderStateCss()" @onclick="e => ToggleCallback(Item, !Checked)">
<span class="checkbox-input">
<span class="checkbox-inner"></span>
</span>
<span class="checkbox-label">@Text</span>
</label>

View File

@ -0,0 +1,8 @@
@inherits DefaultLayoutComponentBase
<CascadingValue Value=this Name="Default">
<Header Icon="@Model.Icon" Title="@Model.Title" IsAdmin=@IsAdmin UserName="@Model.UserName" @bind-DisplayName="@DisplayName"></Header>
<SideBar @bind-Model=@Model />
@Body
<Footer Text="@Model.Footer" IsDemo=@Model.IsDemo></Footer>
</CascadingValue>

View File

@ -0,0 +1,31 @@
@typeparam TItem
@inherits EditPageBase<TItem>
<Query Id="@($"{Id}_query")" OnQuery="Query" TItem="TItem" QueryModel="QueryModel">
@QueryBody?.Invoke(context)
</Query>
<div class="card">
<div class="card-header">
查询结果
</div>
<div class="card-body">
<Table @ref="Table" Id="@Id" TItem="TItem" OnQuery="QueryData" OnAdd="OnAdd" OnDelete="OnDelete" OnSave="OnSave">
<TableHeader>
@TableHeader?.Invoke(context)
</TableHeader>
<RowTemplate>
@RowTemplate?.Invoke(context)
</RowTemplate>
<ButtonTemplate>
<div class='btn-group'>
<button class='btn btn-sm btn-success' asp-auth="edit" @onclick="e => Edit(context)"><i class='fa fa-edit'></i><span>编辑</span></button>
<button class='btn btn-sm btn-danger' asp-auth="del" @onclick="e => Delete(context)"><i class='fa fa-remove'></i><span>删除</span></button>
@ButtonTemplate
</div>
</ButtonTemplate>
<EditTemplate>
@EditTemplate?.Invoke(context)
</EditTemplate>
</Table>
</div>
</div>

View File

@ -0,0 +1,20 @@
<footer class="position-fixed">
<div>
<span id="websiteFooter">@Text</span>
@if (IsDemo)
{
<span>(演示系统)</span>
}
</div>
<a id="gotoTop" href="#" class="go-top" title="返回顶部" data-toggle="tooltip" data-placement="left">
<i class="fa fa-angle-up"></i>
</a>
</footer>
@code {
[Parameter]
public string Text { get; set; } = "未设置";
[Parameter]
public bool IsDemo { get; set; }
}

View File

@ -0,0 +1,127 @@
@inherits HeaderBase
<header class="header">
<div class="bg"></div>
<div class="d-flex align-items-center">
<a id="navbar" href="@Layout.RootLayout.HomeUrl" class="sidebar-toggle-box">
<i class="fa fa-bars"></i>
<span id="websiteTitle">@Title</span>
</a>
<div class="nav">
@if (IsAdmin)
{
<!-- tasks start -->
<div class="dropdown">
<a data-toggle="dropdown" class="shadow-primary" href="#">
<i class="fa fa-tasks"></i>
<span id="msgHeaderTaskBadge" class="badge badge-pill badge-primary"></span>
</a>
<div class="dropdown-menu">
<div class="dropdown-arrow arrow-primary"></div>
<div id="msgHeaderTaskContent" class="dropdown-header bg-primary">您有 <span id="msgHeaderTask">0</span> 个未完成的任务</div>
<div class="dropdown-footer">
<a href="/Pages/Admin/Tasks">查看所有任务</a>
</div>
</div>
</div>
<!-- tasks end -->
<!-- message dropdown start-->
<div class="dropdown">
<a data-toggle="dropdown" class="shadow-info" href="#">
<i class="fa fa-envelope"></i>
<span id="msgHeaderMsgBadge" class="badge badge-pill badge-info"></span>
</a>
<div class="dropdown-menu">
<div class="dropdown-arrow arrow-info"></div>
<div id="msgHeaderMsgContent" class="dropdown-header bg-info">您有 <span id="msgHeaderMsg">0</span> 个未读的消息</div>
<div class="dropdown-footer">
<a href="/Pages/Admin/Messages">查看所有消息</a>
</div>
</div>
</div>
<!-- message dropdown end -->
<!-- users dropdown start-->
<div class="dropdown">
<a data-toggle="dropdown" class="shadow-success" href="#">
<i class="fa fa-user-plus"></i>
<span id="msgHeaderUserBadge" class="badge badge-pill badge-success"></span>
</a>
<div class="dropdown-menu">
<div class="dropdown-arrow arrow-success"></div>
<div id="msgHeaderUserContent" class="dropdown-header bg-success">您有 <span id="msgHeaderUser">0</span> 条新用户通知</div>
<div class="dropdown-footer">
<a href="~/Admin/Notifications">查看所有通知</a>
</div>
</div>
</div>
<!-- users dropdown end -->
<!-- apps dropdown start-->
<div class="dropdown">
<a data-toggle="dropdown" class="shadow-warning" href="#">
<i class="fa fa-bug"></i>
<span id="msgHeaderAppBadge" class="badge badge-pill badge-warning"></span>
</a>
<div class="dropdown-menu">
<div class="dropdown-arrow arrow-warning"></div>
<div id="msgHeaderAppContent" class="dropdown-header bg-warning">您有 <span id="msgHeaderApp">0</span> 条程序异常通知</div>
<div class="dropdown-footer">
<a href="/Pages/Admin/Exceptions">查看所有异常</a>
</div>
</div>
</div>
<!-- apps dropdown end -->
<!-- db dropdown start-->
<div class="dropdown">
<a data-toggle="dropdown" class="shadow-danger" href="#">
<i class="fa fa-database"></i>
<span id="msgHeaderDbBadge" class="badge badge-pill badge-danger"></span>
</a>
<div class="dropdown-menu">
<div class="dropdown-arrow arrow-danger"></div>
<div id="msgHeaderDbContent" class="dropdown-header bg-danger">您有 <span id="msgHeaderDb">0</span> 条数据库异常通知</div>
<div class="dropdown-footer">
<a href="/Pages/Admin/Exceptions">查看所有异常</a>
</div>
</div>
</div>
<!-- db dropdown end -->
<div class="dropdown">
<a class="shadow-success" href="/Account/Lock" data-toggle="tooltip" title="系统锁屏">
<i class="fa fa-tv"></i>
</a>
</div>
}
</div>
<div class="dropdown userinfo">
<div data-toggle="dropdown" class="dropdown-toggle shadow-default">
<img id="headerIcon" alt="" src="@Icon.ToBlazorLink()" />
<span id="userDisplayName" data-userName="@UserName" class="username text-truncate d-inline-block">@DisplayName</span>
</div>
<div class="dropdown-menu dropdown-menu-right">
<div class="dropdown-item">
<div class="d-flex flex-fill align-items-center">
<img src="@Icon.ToBlazorLink()">
<div class="flex-fill">
<div class="username text-truncate">@DisplayName</div>
<div>登录名:@UserName</div>
</div>
</div>
</div>
<div class="dropdown-item">
<a href="/Pages/Admin/Profiles"><i class=" fa fa-suitcase"></i>个人中心</a>
<a href="/Pages/Admin/Index"><i class="fa fa-cog"></i>设置</a>
<a href="/Pages/Admin/Notifications"><i class="fa fa-bell"></i>通知<span id="logoutNoti" class="badge badge-pill badge-success"></span></a>
</div>
<div class="dropdown-item">
<a href="/Account/Logout"><i class="fa fa-key"></i>注销</a>
</div>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/Pages"><i class="fa fa-home"></i>首页</a></li>
<li class="breadcrumb-item d-none" id="breadNav"></li>
</ol>
</nav>
</header>

View File

@ -0,0 +1,13 @@
@inherits LgbEditFormBase
<CascadingValue Value="this">
<EditForm Model="@Model" AdditionalAttributes="@AdditionalAttributes" OnSubmit="OnSubmit" OnValidSubmit="OnValidSubmit" OnInvalidSubmit="OnInvalidSubmit">
<BootstrapAdminDataAnnotationsValidator />
@ChildContent
<button type="submit" class="d-none">submit</button>
</EditForm>
</CascadingValue>
@code {
}

View File

@ -0,0 +1,9 @@
@inherits LgbInputTextBase
<div class="form-group col-sm-6">
<label class="control-label" for="@Id">@DisplayName</label>
<input Id="@Id" data-original-title="@ErrorMessage" class="@($"form-control {CssClass} {ValidCss}")" placeholder="@PlaceHolder" maxlength=@MaxLength type="text" @bind="Value" @oninput="EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString)" />
<CascadingValue Value="this" IsFixed="true">
@ChildContent
</CascadingValue>
</div>

View File

@ -0,0 +1,2 @@
@inherits LayoutComponentBase
@Body

View File

@ -0,0 +1,27 @@
@inherits ModalBase
<div class="modal fade" id="@Id" tabindex="-1" role="dialog" aria-labelledby="modal_@Id" aria-hidden="true" data-backdrop="@(Backdrop ? "none" : "static")">
<div class="@(IsCentered ? "modal-dialog modal-dialog-centered" : "modal-dialog") @RenderModalSize()" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal_@Id">@Title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
@ModalBody
</div>
@if (ShowFooter)
{
<div class="modal-footer">
@ModalFooter
</div>
}
</div>
</div>
</div>
@code {
}

View File

@ -0,0 +1,17 @@
@inherits NavItemBase
@if (Menu.Menus.Any())
{
<li class="nav-item parent">
<a href="javascripts:void();" data-target="@string.Format("#collapse_{0}",Menu.Id)" data-toggle="collapse" id="@string.Format("menus_{0}",Menu.Id)" class="nav-link @(Menu.Active == "active" ? "collapsed show" : "collapsed")"><i class="@Menu.Icon"></i><span class="flex-fill">@Menu.Name</span><i class="fa fa-angle-left"></i></a>
<div id="@string.Format("collapse_{0}",Menu.Id)" class="@(Menu.Active == "active" ? "show" : "collapse")">
<NavItems Menus="@Menu.Menus"></NavItems>
</div>
</li>
}
else
{
<li class="nav-item">
<a href="@Menu.Url.ToBlazorMenuUrl()" id="@string.Format("menus_{0}",Menu.Id)" class="@(Menu.Active =="active" ? "nav-link active" : "nav-link")"><i class="@Menu.Icon"></i><span class="flex-fill">@Menu.Name</span></a>
</li>
}

View File

@ -0,0 +1,11 @@
<ul class="sub nav flex-column">
@foreach (var menu in Menus)
{
<NavItem Menu="@menu"></NavItem>
}
</ul>
@code {
[Parameter]
public IEnumerable<BootstrapMenu> Menus { get; set; } = new BootstrapMenu[0];
}

View File

@ -0,0 +1,28 @@
@inherits LayoutComponentBase
<div class="error-content">
<div class="error-wrapper text-center">
<img src="@model?.Image.ToBlazorLink()" />
<h1>@model?.Content</h1>
<h3>@model?.Detail</h3>
<p><a href="/Account/Logout">登录</a> <span>或者</span> <a href="@model?.ReturnUrl.ToBlazorLink()">返回首页</a> <span>或者</span> <a href="/healths-ui">系统自检</a></p>
@if (@model?.Id == 403)
{
<div>
<p>也可能是以下原因导致您没有权限</p>
<p>1. 没有登录,请登录后查看</p>
<p>2. 当前用户未获得此资源的相应权限,请联系管理员</p>
</div>
}
</div>
@Body
</div>
@code {
protected ErrorModel? model;
protected override void OnInitialized()
{
var id = 404;
model = ErrorModel.CreateById(id);
}
}

View File

@ -0,0 +1,31 @@
@inherits PaginationBase
<CascadingValue Value="this">
<nav class="d-flex align-items-center" aria-label="分页组件">
<div class="pagination-bar">
显示第 <span>@((PageIndex - 1) * PageItems + 1)</span> 到第 <span>@(Math.Min(PageIndex * PageItems, TotalCount))</span> 条记录,总共 <span>@TotalCount</span> 条记录 每页显示
<div class="btn-group dropup">
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">@PageItems</button>
<div class="dropdown-menu">
@foreach (var page in GetPages())
{
<div class="@(PageItems == page ? "dropdown-item active" : "dropdown-item")" @onclick="@(e => ClickItem(page))">@(page.ToString())</div>
}
</div>
</div>
条记录
</div>
<ul class="@(PageCount > 1 ? "pagination" : "pagination d-none")">
<li class="page-item" @onclick="MovePrev"><div class="page-link" aria-label="上一页"><i class="fa fa-angle-double-left"></i></div></li>
@for (int i = StartPageIndex; i <= EndPageIndex; i++)
{
<PaginationItem Active="i == PageIndex" PageIndex="i"></PaginationItem>
}
<li class="page-item" @onclick="MoveNext"><div class="page-link" aria-label="下一页"><i class="fa fa-angle-double-right"></i></div></li>
</ul>
</nav>
</CascadingValue>
@code {
}

View File

@ -0,0 +1,29 @@
<li class="@(Active ? "page-item active" : "page-item")" @onclick="ClickPage"><div class="page-link" aria-label="@string.Format("第 {0} 页", PageIndex)">@PageIndex</div></li>
@code {
/// <summary>
///
/// </summary>
[Parameter]
public bool Active { get; set; }
/// <summary>
///
/// </summary>
[CascadingParameter]
public Pagination? Pagination { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public int PageIndex { get; set; } = 1;
/// <summary>
///
/// </summary>
protected void ClickPage()
{
Pagination?.OnPageClick?.Invoke(PageIndex, Pagination.PageItems);
}
}

View File

@ -0,0 +1,16 @@
@typeparam TItem
@inherits QueryBase<TItem>
<div class="card">
<div class="card-header">@Title</div>
<div class="card-body">
<LgbEditForm class="form-inline" Id="@Id" Model="QueryModel" OnValidSubmit="e => OnQuery?.Invoke()">
<div class="row">
@ChildContent?.Invoke(QueryModel)
<div class="form-group col-sm-6 col-md-auto flex-sm-fill justify-content-sm-end align-self-sm-end">
<button type="submit" class="btn btn-primary btn-fill"><i class="fa fa-search"></i><span>@Text</span></button>
</div>
</div>
</LgbEditForm>
</div>
</div>

View File

@ -0,0 +1,9 @@
@inherits SectionBase
<section class="main-content @ShowCardTitle @(ShowBackground ? @"welcome-bg" : @"")">
@if (LockScreenPeriod > 0)
{
<input id="lockScreenPeriod" type="hidden" value="@LockScreenPeriod" />
}
@ChildContent
</section>

View File

@ -0,0 +1,21 @@
@typeparam TItem
@inherits SelectBase<TItem>
<div class="form-group col-sm-6">
<label class="control-label" for="@Id">@DisplayName</label>
<div data-toggle="lgbSelect" class="form-select dropdown">
<input type="text" readonly="readonly" class="form-control form-select-input" id="@Id" data-toggle="dropdown" placeholder="@PlaceHolder" value="@SelectedItem.Text" />
<span class="form-select-append"><i class="fa fa-angle-up"></i></span>
<div class="dropdown-menu-arrow"></div>
<div class="dropdown-menu">
@foreach (var item in Items)
{
<SelectItem Item="@item" ItemClickCallback="ItemClickCallback"></SelectItem>
}
</div>
</div>
</div>
@code {
}

View File

@ -0,0 +1,7 @@
@inherits SelectItemBase
<div class="@(Item.Active ? "dropdown-item active" : "dropdown-item")" data-val="@Item.Value" @onclick="@(e=>ItemClickCallback(Item))">@Item.Text</div>
@code {
}

View File

@ -0,0 +1,25 @@
@inherits SideBarBase
<aside class="@Model.ShowSideBar">
<div class="bg"></div>
<div class="nav-brand justify-content-center">
<a href="@Layout.RootLayout.HomeUrl">
<img src="@Model.WebSiteLogo.ToBlazorLink()" />
<span>@Model.Title</span>
</a>
</div>
<div class="nav-header flex-fill align-items-center">
<a href="/Pages/Admin/Profiles">
<img src="@Model.Icon.ToBlazorLink()" />
<span class="username d-inline-block text-truncate flex-fill">@Model.DisplayName</span>
</a>
</div>
<div class="sidebar">
<ul class="nav-sidebar nav nav-pills flex-column flex-nowrap">
@foreach (var menu in Model.Navigations)
{
<NavItem Menu="@menu"></NavItem>
}
</ul>
</div>
</aside>

View File

@ -0,0 +1,20 @@
@typeparam TItem
@inherits SubmitModalBase<TItem>
<Modal Id="@Id" Title="@Title" Size="@Size">
<ModalBody>
<LgbEditForm class="form-inline" Id="@Id" Model="Model" OnValidSubmit="OnValidSubmit" OnInvalidSubmit="OnInvalidSubmit" OnSubmit="OnSubmit">
@ModalBody
</LgbEditForm>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fa fa-times"></i>
<span>关闭</span>
</button>
<button type="button" class="btn btn-primary" onclick="$.submitForm(this)">
<i class="fa fa-save"></i>
<span>保存</span>
</button>
</ModalFooter>
</Modal>

View File

@ -0,0 +1,5 @@
@inherits TabBase
<div class="@(Active ? "nav-link active" : "nav-link")" @onclick="ClickTab" id="@Id" url="@Url">
<i class="@Icon"></i><span>@Title</span><i class="fa fa-close" @onclick="CloseTab"></i>
</div>

View File

@ -0,0 +1,30 @@
@inherits TabSetBase
<nav>
<div class="d-flex">
<div class="nav-link-bar" @onclick="MovePrev"><i class="fa fa-chevron-left"></i></div>
<div id="navBar" class="nav nav-tabs flex-fill flex-nowrap overflow-hidden" role="tablist">
@foreach (var tab in Tabs)
{
<Tab @key="@tab.Id" Active="@tab.Active" Id="@tab.Id" Icon="@tab.Icon" Url="@tab.Url.ToBlazorMenuUrl()" Title="@tab.Title"></Tab>
}
</div>
<div class="nav-link-bar" @onclick="MoveNext"><i class="fa fa-chevron-right"></i></div>
<div class="dropdown nav-link-bar">
<div class="nav-link-close dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-chevron-down"></i></div>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="关闭标签下拉菜单">
<div class="dropdown-item" @onclick="CloseCurrentTab"><span>关闭当前标签</span></div>
<div class="dropdown-item" @onclick="CloseOtherTab"><span>关闭其他标签</span></div>
<div class="dropdown-item" @onclick="CloseAllTab"><span>关闭所有标签</span></div>
</div>
</div>
</div>
</nav>
<div class="tab-content">
@foreach (var comp in Pages)
{
<div id="@string.Format("page_{0}", comp.Id)" class="tab-pane @(comp.Active ? "fade active show" : "fade")" role="tabpanel" aria-labelledby="nav-profile-tab">
<PageContent @key="comp" Name="@comp.Name"></PageContent>
</div>
}
</div>

View File

@ -0,0 +1,88 @@
@typeparam TItem
@inherits TableBase<TItem>
<div class="bootstrap-table">
<div class="@(ShowToolBar ? "bs-bars" : "bs-bars d-none")">
<div class="toolbar btn-group">
<button type="button" @onclick="Add" class="btn btn-success" asp-auth="add"><i class="fa fa-plus" aria-hidden="true"></i><span>新增</span></button>
<button type="button" @onclick="Delete" class="btn btn-danger" asp-auth="del"><i class="fa fa-remove" aria-hidden="true"></i><span>删除</span></button>
<button type="button" @onclick="Edit" class="btn btn-primary" asp-auth="edit"><i class="fa fa-pencil" aria-hidden="true"></i><span>编辑</span></button>
</div>
<div class="gear btn-group">
<button class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" type="button"><i class="fa fa-gear"></i></button>
<div class="dropdown-menu">
<div class="dropdown-item" title="新增" @onclick="Add" asp-auth="add"><i class="fa fa-plus"></i></div>
<div class="dropdown-item" title="删除" @onclick="Delete" asp-auth="del"><i class="fa fa-remove"></i></div>
<div class="dropdown-item" title="编辑" @onclick="Edit" asp-auth="edit"><i class="fa fa-pencil"></i></div>
</div>
</div>
</div>
<table class="table table-striped table-bordered table-hover table-selected">
<thead>
<tr>
@if (ShowLineNo)
{
<th class="table-col-lineno">行号</th>
}
@if (ShowCheckbox)
{
<th class="table-col-checkbox"><Checkbox TItem="TItem" SetCheckCallback="CheckState" ToggleCallback="ToggleCheck"></Checkbox></th>
}
@TableHeader?.Invoke(EditModel)
@if (ShowButtons)
{
<th>@ButtonTemplateHeaderText</th>
}
</tr>
</thead>
<tbody>
@for (int index = 0; index < Items.Count(); index++)
{
<tr>
@if (ShowLineNo)
{
<td class="table-col-lineno">@(index + 1 + (PageIndex - 1) * PageItems)</td>
}
@if (ShowCheckbox)
{
<td class="table-col-checkbox"><Checkbox TItem="TItem" Item="Items.ElementAt(index)" SetCheckCallback="item => SelectedItems.Contains(item) ? CheckBoxState.Checked : CheckBoxState.UnChecked" ToggleCallback="ToggleCheck"></Checkbox></td>
}
@RowTemplate?.Invoke(Items.ElementAt(index))
@if (ShowButtons)
{
<td>@ButtonTemplate?.Invoke(Items.ElementAt(index))</td>
}
</tr>
}
</tbody>
<tfoot>
<tr>@TableFooter</tr>
</tfoot>
</table>
<Pagination PageItems="PageItems" TotalCount="TotalCount" PageIndex="PageIndex" OnPageClick="PageClick" OnPageItemsChange="PageItemsChange"></Pagination>
</div>
<Modal @ref="ConfirmModal" Id="@($"{Id}_confirm")" Title="数据删除">
<ModalBody>
<div class="modal-confirm-body">您确定要删除选中的所有数据吗?</div>
</ModalBody>
<ModalFooter>
<button type="button" class="btn btn-danger" @onclick="Confirm">
<i class="fa fa-trash-o"></i>
<span>我要删除</span>
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fa fa-times"></i>
<span>取消</span>
</button>
</ModalFooter>
</Modal>
<SubmitModal @ref="EditModal" Id="@($"{Id}_edit")" TItem="TItem" Title="字典编辑窗口" Size="ModalSize.ExtraLarge" @bind-Model="EditModel" OnValidSubmit="Save">
<ModalBody>
@EditTemplate?.Invoke(EditModel)
</ModalBody>
</SubmitModal>
<Toast @ref="Toast"></Toast>

View File

@ -0,0 +1,19 @@
@inherits ToastBase
<div class="toast fade @Placement.ToCss("toast")" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="@(AutoHide ? "true":"false")" data-delay="@Interval">
<div class="toast-header">
<div class="toast-bar"><i class="@RenderCategory()"></i></div>
<strong class="mr-auto">@Title</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
@Text
</div>
<div class="toast-progress" style="@RenderAnimation()"></div>
</div>
@code {
}

View File

@ -25,11 +25,18 @@ namespace Bootstrap.Admin
/// 构造函数
/// </summary>
/// <param name="configuration"></param>
public Startup(IConfiguration configuration)
/// <param name="env"></param>
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
Enviroment = env;
}
/// <summary>
/// 获得 当前运行时环境
/// </summary>
public IWebHostEnvironment Enviroment { get; }
/// <summary>
/// 获得 系统配置项 Iconfiguration 实例
/// </summary>
@ -75,6 +82,12 @@ namespace Bootstrap.Admin
options.Filters.Add<ExceptionFilter>();
options.Filters.Add<SignalRExceptionFilter<SignalRHub>>();
}).AddJsonOptions(op => op.JsonSerializerOptions.AddDefaultConverters());
services.AddRazorPages();
services.AddServerSideBlazor().AddCircuitOptions(options =>
{
if (Enviroment.IsDevelopment()) options.DetailedErrors = true;
});
services.AddDisplayNames();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -114,6 +127,8 @@ namespace Bootstrap.Admin
endpoints.MapHub<SignalRHub>("/NotiHub");
endpoints.MapHub<TaskLogHub>("/TaskLogHub");
endpoints.MapBootstrapHealthChecks();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
endpoints.MapDefaultControllerRoute();
});
}

View File

@ -1,7 +1,6 @@
using Bootstrap.DataAccess;
using Longbow.Tasks;
using Microsoft.Extensions.Hosting;
using System;
using System.Linq;
using System.Threading;
using Task = System.Threading.Tasks.Task;
@ -45,7 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection
// 本机调试时此处会抛出异常,配置文件中默认开启了任务持久化到物理文件,此处异常只有首次加载时会抛出
// 此处异常是示例自定义任务内部未进行捕获异常时任务仍然能继续运行,不会导致整个进程崩溃退出
// 此处代码可注释掉
TaskServicesManager.GetOrAdd("故障任务", token => throw new Exception("故障任务"));
//TaskServicesManager.GetOrAdd("故障任务", token => throw new Exception("故障任务"));
TaskServicesManager.GetOrAdd("取消任务", token => Task.Delay(1000)).Triggers.First().Enabled = false;
// 创建任务并禁用

View File

@ -0,0 +1,20 @@
@using Bootstrap.Admin
@using Bootstrap.Admin.Components
@using Bootstrap.Admin.Extensions
@using Bootstrap.Admin.Models
@using Bootstrap.Admin.Pages
@using Bootstrap.Admin.Shared
@using Bootstrap.Security
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Mvc
@using Microsoft.Extensions.Configuration
@using Microsoft.JSInterop
@using Longbow.Web.Mvc
@using System.Linq
@using System.Net
@using System.Net.Http
@using System.Security.Claims

View File

@ -64,6 +64,7 @@
}
}
],
"UseBlazor": true,
"SwaggerPathBase": "",
"AllowOrigins": "http://localhost:49185",
"HealthsCloudUrl": "https://client.sdgxgz.com/api/Interface/Healths",

View File

@ -1,4 +1,4 @@
{
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
@ -62,6 +62,7 @@
],
"AppId": "BA",
"UseHttps": true,
"UseBlazor": true,
"SwaggerPathBase": "",
"AllowOrigins": "http://localhost,http://ba.sdgxgz.cn",
"HealthsCloudUrl": "https://client.sdgxgz.com/api/Interface/Healths",

View File

@ -0,0 +1,217 @@
#components-reconnect-modal {
z-index: 9999 !important;
}
.nav-link-bar {
border: 1px solid #dee2e6;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
padding: 0.5rem 1rem;
background-color: #fff;
}
nav .dropdown .nav-link-close.dropdown-toggle:after {
display: none;
}
.dropdown-item, .nav-tabs .nav-link .fa, .nav-link, .nav-link-bar, .page-link {
cursor: pointer;
}
.dropdown-item:hover {
background-color: #4f9ecc;
}
.nav-tabs .nav-link, .nav-link-bar, .fa-close {
transition: all .3s linear;
}
.nav-tabs .nav-link {
white-space: nowrap;
color: #495057;
border-color: #dee2e6 #dee2e6 transparent;
transition: all .3s linear;
}
.nav-tabs .nav-link:hover, .nav-link-bar:hover {
background-color: #4f9ecc;
border-bottom-color: #4f9ecc;
}
.nav-tabs .nav-link span {
margin: 0 10px;
}
.nav-tabs .nav-link .fa-close {
border-radius: 4px;
padding: 2px;
width: 18px;
height: 18px;
text-align: center;
}
.nav-tabs .nav-link .fa-close:hover {
background-color: #eee;
}
.nav-link-bar.dropdown {
padding: 0;
}
.nav-link-close {
padding: 0.5rem 1rem;
}
.tab-content {
padding-top: 1rem;
}
.table th, .table td {
padding: 0.5rem;
}
.table .btn.btn-sm {
font-size: 0.75rem;
padding: 1px 5px;
}
.table .btn-group {
white-space: nowrap;
}
.table-col-lineno {
width: 46px;
text-align: right;
white-space: nowrap;
}
.table-col-checkbox {
width: 32px;
text-align: center;
}
.table-col-checkbox .checkbox-label {
display: none;
}
.table-selected .form-checkbox {
line-height: 1;
vertical-align: text-bottom;
}
.card-body .bootstrap-table {
margin-top: 0rem;
}
.bs-bars {
margin-bottom: 0.625rem;
}
.bs-bars .dropdown-menu.show {
display: flex;
min-width: unset;
}
.bs-bars .dropdown-item {
padding: 6px 12px;
}
.bs-bars .dropdown-item:not(:first-child) {
border-left: solid 1px #aeb2b7;
}
.bootstrap-table {
margin-bottom: 0;
}
.pagination-bar {
flex: 1 1 auto;
margin-bottom: 1rem;
}
.toast {
position: absolute;
z-index: -1;
min-width: 260px;
}
.toast.show {
z-index: 1080;
}
.toast-top {
left: 0;
right: 0;
top: 1rem;
margin: 0 auto;
}
.toast-top-left {
top: 1rem;
left: 1rem;
}
.toast-top-right {
top: 1rem;
right: 1rem;
}
.toast-bottom {
left: 0;
right: 0;
bottom: 1rem;
margin: 0 auto;
}
.toast-bottom-left {
bottom: 1rem;
left: 1rem;
}
.toast-bottom-right {
bottom: 1rem;
right: 1rem;
}
.toast-center {
left: 0;
right: 0;
margin: 0 auto;
margin-top: calc(50vh - 42px);
}
.toast-bar {
margin-right: 8px;
}
.toast-header {
border-bottom: 1px solid #ccc;
}
.toast-body {
background-color: #e9ecef;
color: rgba(0, 0, 0, 0.9);
word-break: break-all;
}
.toast-progress {
position: absolute;
right: 0;
bottom: 0;
height: 4px;
background-color: #000000;
opacity: 0.4;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
width: 0;
}
.showing .toast-progress, .show .toast-progress {
width: 100%;
}
.modal-confirm-body {
margin-bottom: 15px;
font-size: 1.125rem;
}

View File

@ -474,3 +474,56 @@ pre {
.userinfo .dropdown-toggle, .header .nav .dropdown > a {
color: #777;
}
.error-content {
position: absolute;
bottom: 0;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background: #8075c6;
color: #fff;
font-size: 1rem;
}
.error-content .error-wrapper {
max-width: 30rem;
}
.error-wrapper img {
width: 150px;
height: auto;
}
.error-wrapper h1 {
font-size: 1.5rem;
font-weight: bold;
margin: 1.25rem 0;
}
.error-wrapper h3 {
font-size: 1.17rem;
font-weight: bold;
margin: 1.125rem 0;
}
.error-wrapper p {
padding-top: 10px;
}
.error-wrapper div {
text-align: left;
}
.error-wrapper a {
cursor: pointer;
}
.error-wrapper a, .error-wrapper a:hover, .error-wrapper a:focus {
text-decoration: none;
outline: none;
color: #371ed4;
}

View File

@ -6,7 +6,6 @@
body {
color: #797979;
background: #f1f2f7;
-webkit-font-smoothing: antialiased;
-webkit-overflow-scrolling: touch;
}

Some files were not shown because too many files have changed in this diff Show More