!49 增加功能:Blazor 框架下字典表维护功能完成

* feat: Query 组件支持泛型
* refactor: Dicts 组件移除不用的引用
* refactor: EditPage 组件增加 Id 保护
* doc: 增加程序注释
* feat: 增加验证控件扩展
* refactor: IValidateComponent 更改方法名
* refactor: 重构 checkbox 选中机制
* fix: 查询按钮过滤条件不生效问题
* fix: 修复查询区域按钮点击不能切换到首页数据问题
* feat: 完善保存功能
* refactor: Save 方法参数更改为实体类
* refactor: 优化每页显示数据算法
* fix: 修复新建数据项时清除已选数据功能
* feat: 完善删除功能
* feat: 查询后清楚已选条目
* feat: 修复编辑按钮功能
* feat: 增加行号设置
* refactor: 重构分页组件移除 Table 组件对其引用
* refactor: 重构 Dicts 页面组件化
* feat: 增加 Validate 功能
* refactor: 查询方法增加默认参数值
* feat: 增加删除确认功能
* style: 微调表格内按钮高度
* style: 微调 gear 样式
* fix: 修复表格数据删除后仍然可编辑
* feat: 增加 Table 表格内按钮组
* refactor: 增加删除数据是否成功提示
* feat: 增加 Toast 组件
* feat: 增加 Alert 控件
* feat: 增加保存功能
* feat: 表格首列设置为 checkbox 功能
* feat: 增加选择列
This commit is contained in:
Argo 2019-12-03 01:42:19 +08:00
parent c824c008fc
commit 93af3948be
50 changed files with 2345 additions and 345 deletions

View File

@ -22,6 +22,11 @@ namespace Bootstrap.Admin.Components
[CascadingParameter(Name = "Default")]
public DefaultLayout RootLayout { get; protected set; } = new DefaultLayout();
/// <summary>
/// Toast 组件实例
/// </summary>
protected Toast Toast { get; set; } = new Toast();
/// <summary>
///
/// </summary>
@ -47,5 +52,13 @@ namespace Bootstrap.Admin.Components
var menu = menus.FirstOrDefault(menu => path.Contains(menu.Url.ToBlazorMenuUrl()));
if (menu != null) TabSet?.AddTab(menu);
}
/// <summary>
///
/// </summary>
/// <param name="title"></param>
/// <param name="text"></param>
/// <param name="cate"></param>
public void ShowMessage(string title, string text, ToastCategory cate = ToastCategory.Success) => Toast.ShowMessage(title, text, cate);
}
}

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,46 @@
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

@ -26,5 +26,13 @@ namespace Bootstrap.Admin.Components
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { 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) => Layout.ShowMessage(title, text, cate);
}
}

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,63 @@
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 ?? ""));
}
}

View File

@ -0,0 +1,153 @@
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// 可编辑页面组件包含查询与数据表格
/// </summary>
public class EditPageBase<TItem> : BootstrapComponentBase
{
/// <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? 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; }
/// <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>
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(Id)) throw new InvalidOperationException($"The property {nameof(Id)} can't 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,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,119 @@
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>
/// 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<(Type ModelType, string FieldName), IValidateComponent> _validatorCache = new ConcurrentDictionary<(Type, string), IValidateComponent>();
/// <summary>
/// 添加数据验证组件到 EditForm 中
/// </summary>
/// <param name="key"></param>
/// <param name="comp"></param>
public void AddValidator((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.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((context.ObjectType, context.MemberName), out var validator))
{
validator.ValidateProperty(propertyValue, context, results);
validator.ToggleMessage(results, true);
}
}
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
}
}
}

View File

@ -0,0 +1,135 @@
using Bootstrap.Admin.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// LgbInputText 组件
/// </summary>
public class LgbInputText : InputText, IValidateComponent
{
/// <summary>
///
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
/// <summary>
///
/// </summary>
[CascadingParameter]
public LgbEditFormBase? EditForm { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
///
/// </summary>
protected string ErrorMessage { get; set; } = "";
/// <summary>
///
/// </summary>
protected string ValidCss { get; set; } = "";
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 (results.Where(r => r.MemberNames.Any(m => m == FieldIdentifier.FieldName)).Any())
{
ErrorMessage = results.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>
protected override void OnInitialized()
{
EditForm?.AddValidator((FieldIdentifier.Model.GetType(), FieldIdentifier.FieldName), this);
}
/// <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>();
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", $"{CssClass} {ValidCss}");
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValue));
builder.AddAttribute(4, "oninput", EventCallback.Factory.CreateBinder<string>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
if (!string.IsNullOrEmpty(ErrorMessage)) builder.AddAttribute(5, "data-original-title", ErrorMessage);
if (!string.IsNullOrEmpty(Id)) builder.AddAttribute(6, "id", Id);
builder.OpenComponent<CascadingValue<LgbInputText>>(7);
builder.AddAttribute(8, "IsFixed", true);
builder.AddAttribute(9, "Value", this);
builder.AddAttribute(10, "ChildContent", ChildContent);
builder.CloseComponent();
builder.CloseElement();
}
}
}

View File

@ -15,6 +15,18 @@ namespace Bootstrap.Admin.Components
[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>
@ -28,22 +40,28 @@ namespace Bootstrap.Admin.Components
public string Title { get; set; } = "未设置";
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? ModalBody { get; set; }
/// <summary>
///
/// 获得/设置 是否允许点击后台关闭弹窗 默认为 false
/// </summary>
[Parameter]
public bool Backdrop { get; set; }
/// <summary>
///
/// 获得/设置 弹窗大小
/// </summary>
[Parameter]
public RenderFragment? ModalFooter { get; set; }
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>
///
@ -56,5 +74,60 @@ namespace Bootstrap.Admin.Components
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

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
namespace Bootstrap.Admin.Components
{
@ -10,25 +10,13 @@ namespace Bootstrap.Admin.Components
public class PaginationBase : ComponentBase
{
/// <summary>
/// 获得/设置 页码总数
///
/// </summary>
[Parameter]
public int PageCount { get; set; } = 0;
public int TotalCount { get; set; }
/// <summary>
/// 获得/设置 每页显示数据数量
/// </summary>
[Parameter]
public int PageItems { get; set; } = 20;
/// <summary>
/// 获得/设置 数据总数
/// </summary>
[Parameter]
public int ItemsCount { get; set; } = 0;
/// <summary>
/// 获得/设置 当前页码
///
/// </summary>
[Parameter]
public int PageIndex { get; set; } = 1;
@ -36,25 +24,38 @@ namespace Bootstrap.Admin.Components
/// <summary>
///
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
[Parameter]
public int PageItems { get; set; }
/// <summary>
/// 获得/设置 页码总数
/// </summary>
public int PageCount
{
get
{
return (int)Math.Ceiling(TotalCount * 1.0 / PageItems); ;
}
}
/// <summary>
///
/// </summary>
public Action<int> ClickPageCallback { get; set; } = new Action<int>(i => { });
[Parameter]
public Action<int, int> OnPageClick { get; set; } = new Action<int, int>((pageIndex, pageItems) => { });
/// <summary>
///
/// </summary>
public Action PageItemsChangeCallback { get; set; } = new Action(() => { });
[Parameter]
public Action<int> OnPageItemsChange { get; set; } = new Action<int>((pageItems) => { });
/// <summary>
///
/// </summary>
protected void MovePrev()
{
if (PageIndex > 1) ClickPageCallback(PageIndex - 1);
if (PageIndex > 1) OnPageClick(PageIndex - 1, PageItems);
}
/// <summary>
@ -62,7 +63,7 @@ namespace Bootstrap.Admin.Components
/// </summary>
protected void MoveNext()
{
if (PageIndex < PageCount) ClickPageCallback(PageIndex + 1);
if (PageIndex < PageCount) OnPageClick(PageIndex + 1, PageItems);
}
/// <summary>
@ -72,7 +73,36 @@ namespace Bootstrap.Admin.Components
protected void ClickItem(int pageItems)
{
PageItems = pageItems;
PageItemsChangeCallback();
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>
/// <param name="totalCount"></param>
/// <param name="pageIndex"></param>
/// <param name="pageItems"></param>
public void Update(int totalCount, int pageIndex, int pageItems)
{
TotalCount = totalCount;
PageIndex = pageIndex;
PageItems = pageItems;
}
}
}

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 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 EventCallback<TItem> QueryModelChanged { get; set; }
/// <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,33 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class RequiredValidator : ValidatorComponentBase
{
/// <summary>
/// 获得/设置 是否允许空字符串 默认 false 不允许
/// </summary>
[Parameter]
public bool AllowEmptyString { get; set; } = false;
/// <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

@ -1,27 +1,23 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Bootstrap.Admin.Components
{
/// <summary>
/// Select 组件基类
/// </summary>
public class SelectBase : ComponentBase
public class SelectBase<TItem> : ComponentBase
{
/// <summary>
///
/// 获得/设置 控件 ID
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
///
/// 获得/设置 背景显示文字
/// </summary>
[Parameter]
public string PlaceHolder { get; set; } = "请选择 ...";
@ -29,49 +25,64 @@ namespace Bootstrap.Admin.Components
/// <summary>
/// 当前选择项实例
/// </summary>
public SelectItemBase? SelectedItem { get; set; }
public SelectedItem SelectedItem { get; set; } = new SelectedItem();
/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
[Parameter]
public List<SelectedItem> Items { get; set; } = new List<SelectedItem>();
#nullable disable
private TItem _value;
/// <summary>
///
/// </summary>
[Parameter]
public int SelectedValue { get; set; }
/// <summary>
///
/// </summary>
[Parameter]
public EventCallback<int> SelectedValueChanged { get; set; }
/// <summary>
///
/// </summary>
public Action<SelectItemBase>? ClickItemCallback { get; set; }
/// <summary>
///
/// </summary>
public Action<SelectItemBase>? ActiveChanged { get; set; }
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
public TItem SelectedValue
{
if (firstRender)
get { return _value; }
set
{
ClickItemCallback = UpdateItem;
ActiveChanged = UpdateItem;
_value = value;
Items.ForEach(t =>
{
t.Active = t.Value == _value.ToString();
if (t.Active) SelectedItem = t;
});
}
}
#nullable restore
///<summary>
///
/// </summary>
[Parameter]
public EventCallback<TItem> SelectedValueChanged { get; set; }
/// <summary>
///
/// </summary>
protected override void OnInitialized()
{
if (!SelectedItem.Active)
{
SelectedItem = Items.FirstOrDefault(item => item.Active) ?? Items.First();
}
}
/// <summary>
///
/// </summary>
/// <param name="item"></param>
protected void UpdateItem(SelectItemBase item)
[Parameter]
public Action<SelectedItem> SelectedItemChanged { get; set; } = new Action<SelectedItem>(v => { });
/// <summary>
///
/// </summary>
public void ItemClickCallback(SelectedItem item)
{
SelectedItemChanged(item);
SelectedItem = item;
StateHasChanged();
}

View File

@ -1,5 +1,5 @@
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using System;
namespace Bootstrap.Admin.Components
{
@ -12,44 +12,12 @@ namespace Bootstrap.Admin.Components
///
/// </summary>
[Parameter]
public bool Active { get; set; }
public SelectedItem Item { get; set; } = new SelectedItem();
/// <summary>
///
/// </summary>
[Parameter]
public string Text { get; set; } = "";
/// <summary>
///
/// </summary>
[Parameter]
public string Value { get; set; } = "";
/// <summary>
///
/// </summary>
[CascadingParameter]
public Select? Select { get; set; }
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (Active) Select?.ActiveChanged?.Invoke(this);
}
}
/// <summary>
///
/// </summary>
protected void ClickItem()
{
Select?.ClickItemCallback?.Invoke(this);
}
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,30 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components;
namespace Bootstrap.Admin.Components
{
/// <summary>
///
/// </summary>
public class StringLengthValidator : ValidatorComponentBase
{
/// <summary>
///
/// </summary>
[Parameter]
public int Length { 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 (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

@ -1,21 +1,27 @@
using Bootstrap.Admin.Extensions;
using Bootstrap.Admin.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
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
public class TableBase<TItem> : BootstrapComponentBase
{
/// <summary>
///
/// </summary>
[Inject]
protected IJSRuntime? JSRuntime { get; set; }
public const int DefaultPageItems = 20;
/// <summary>
///
/// </summary>
[Parameter]
public string Id { get; set; } = "";
/// <summary>
///
@ -29,6 +35,18 @@ namespace Bootstrap.Admin.Components
[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>
@ -38,30 +56,271 @@ namespace Bootstrap.Admin.Components
/// <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 IEnumerable<TItem> Items { get; set; } = new TItem[0];
public int TotalCount { get; set; }
/// <summary>
///
/// </summary>
public Action AddCallback { get; set; } = new Action(() => { });
/// <summary>
///
/// </summary>
public Action EditCallback { get; set; } = new Action(() => { });
[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>
/// <param name="item"></param>
/// <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>
///
/// </summary>
public void Edit()
{
if (SelectedItems.Count == 1)
{
EditModel = SelectedItems[0];
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,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 LgbInputText? Input { get; set; }
/// <summary>
/// 初始化方法
/// </summary>
protected override void OnInitialized()
{
if (Input == null)
{
throw new InvalidOperationException($"{nameof(ValidatorComponentBase)} requires a cascading " +
$"parameter of type {nameof(LgbInputText)}. 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

@ -45,7 +45,7 @@ namespace Bootstrap.Admin.Extensions
/// 启用动画
/// </summary>
/// <param name="jSRuntime"></param>
public static void EnableAnimation(this IJSRuntime? jSRuntime) => jSRuntime.InvokeVoidAsync("$.enableAnimation");
public static void EnableAnimation(this IJSRuntime? jSRuntime) => jSRuntime.InvokeVoidAsync("$.initDocument");
/// <summary>
/// 修复 Modal 组件
@ -53,11 +53,31 @@ namespace Bootstrap.Admin.Extensions
/// <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

@ -1,177 +1,56 @@
@inject IJSRuntime JSRuntime
@inherits DictsBase
<div class="card">
<div class="card-header">查询条件</div>
<div class="card-body">
<form class="form-inline">
<div class="row">
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="txt_dict_name">字典名称</label>
<input type="text" class="form-control" id="txt_dict_name" />
</div>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="txt_dict_cate">字典标签</label>
<input type="text" class="form-control" id="txt_dict_cate" data-provide="typeahead" />
</div>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="txt_dict_define">字典类别</label>
<Select @ref="DictCate">
<SelectItem Text="全部" Value="" Active="true"></SelectItem>
<SelectItem Text="系统使用" Value="0"></SelectItem>
<SelectItem Text="自定义" Value="1"></SelectItem>
</Select>
</div>
<div class="form-group col-sm-6 col-md-auto flex-sm-fill justify-content-sm-end align-self-sm-end">
<button type="button" id="btn_query" class="btn btn-primary btn-fill" @onclick="e => Query(1)"><i class="fa fa-search" aria-hidden="true"></i><span>查询</span></button>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
查询结果
</div>
<div class="card-body">
<div class="bootstrap-table">
<Table @ref="Table" Items="Items" TItem="Bootstrap.Security.BootstrapDict">
<TableHeader>
<th>字典标签</th>
<th>字典名称</th>
<th>字典代码</th>
<th>字典类型</th>
</TableHeader>
<RowTemplate>
<td>@context.Category</td>
<td>@context.Name</td>
<td>@context.Code</td>
<td>@context.Define</td>
</RowTemplate>
</Table>
<Pagination @ref="Pagination" PageCount="PageCount" PageIndex="PageIndex" ItemsCount="ItemsCount"></Pagination>
<EditPage Id="dict" TItem="Bootstrap.Security.BootstrapDict" QueryModel="QueryModel" OnQuery="Query" OnAdd="Add" OnDelete="Delete" OnSave="Save">
<QueryBody>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="txt_dict_cate">字典标签</label>
<input type="text" class="form-control" @bind="context.Category" data-provide="typeahead" />
</div>
</div>
</div>
<div id="tableButtons" class="d-none">
<div class='btn-group'>
<button class='edit btn btn-sm btn-success' asp-auth="edit"><i class='fa fa-edit'></i><span>编辑</span></button>
<button class='del btn btn-sm btn-danger' asp-auth="del"><i class='fa fa-remove'></i><span>删除</span></button>
</div>
</div>
<Modal Id="DialogDict" Title="字典编辑窗口">
<ModalBody>
<form class="form-inline">
<div class="row">
<div class="form-group col-sm-6">
<label class="control-label" for="dictCate">字典标签</label>
<input type="text" class="form-control" id="dictCate" @bind="Model.Category" placeholder="不可为空50字以内" maxlength="50" data-valid="true" />
</div>
<div class="form-group col-sm-6">
<label class="control-label" for="dictDefine">字典类型</label>
<Select Id="dictDefine" @bind-SelectedValue="Model.Define">
<SelectItem Text="系统使用" Value="0" Active="true"></SelectItem>
<SelectItem Text="自定义" Value="1"></SelectItem>
</Select>
</div>
<div class="form-group col-sm-6">
<label class="control-label" for="dictName">字典名称</label>
<input type="text" class="form-control" id="dictName" @bind="Model.Name" placeholder="不可为空50字以内" maxlength="50" data-valid="true" />
</div>
<div class="form-group col-sm-6">
<label class="control-label" for="dictCode">字典代码</label>
<input type="text" class="form-control" id="dictCode" @bind="Model.Code" placeholder="不可为空2000字以内" maxlength="2000" data-valid="true" />
</div>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="txt_dict_name">字典名称</label>
<input type="text" class="form-control" @bind="context.Name" />
</div>
<div class="form-group col-sm-6 col-md-auto">
<label class="control-label" for="txt_dict_define">字典类型</label>
<Select Items="QueryDefine" TItem="int" SelectedItemChanged="item=>context.Define = int.Parse(item.Value)"></Select>
</div>
</QueryBody>
<TableHeader>
<th>字典标签</th>
<th>字典名称</th>
<th>字典代码</th>
<th>字典类型</th>
</TableHeader>
<RowTemplate>
<td>@context.Category</td>
<td>@context.Name</td>
<td>@context.Code</td>
<td>@context.Define</td>
</RowTemplate>
<EditTemplate>
<div class="row">
<div class="form-group col-sm-6">
<label class="control-label" for="dictCate">字典标签</label>
<LgbInputText Id="dictCate" type="text" class="form-control" data-toggle="tooltip" @bind-Value="context.Category" placeholder="不可为空50字以内">
<RequiredValidator AllowEmptyString="false" ErrorMessage="这是必填字段" />
<StringLengthValidator Length="5" ErrorMessage="不可为空50字以内" />
</LgbInputText>
</div>
</form>
</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="Save">
<i class="fa fa-save"></i>
<span>保存</span>
</button>
</ModalFooter>
</Modal>
@code {
[Parameter]
public IEnumerable<BootstrapDict> Items { get; set; } = new BootstrapDict[0];
[Parameter]
public int ItemsCount { get; set; } = 0;
[Parameter]
public int PageCount { get; set; } = 0;
[Parameter]
public int PageIndex { get; set; } = 1;
protected Select? DictCate { get; set; }
protected Pagination? Pagination { get; set; }
protected Table<BootstrapDict>? Table { get; set; }
protected BootstrapDict Model { get; set; } = new BootstrapDict();
protected override void OnInitialized()
{
Query(1);
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (Pagination != null)
{
Pagination.ClickPageCallback = pageIndex =>
{
if (pageIndex != PageIndex)
{
Query(pageIndex);
StateHasChanged();
}
};
Pagination.PageItemsChangeCallback = () =>
{
Query(1);
StateHasChanged();
};
}
if (Table != null)
{
Table.AddCallback = () =>
{
JSRuntime.ToggleModal("#DialogDict");
};
Table.EditCallback = () =>
{
};
}
}
}
protected void Query(int pageIndex)
{
var data = DataAccess.DictHelper.RetrieveDicts();
if (!string.IsNullOrEmpty(DictCate?.SelectedItem?.Value)) data = data.Where(d => d.Define.ToString() == DictCate?.SelectedItem?.Value);
ItemsCount = data.Count();
var pageItems = Pagination?.PageItems ?? 20;
Items = data.Skip((pageIndex - 1) * pageItems).Take(pageItems);
PageCount = (int)Math.Ceiling(data.Count() * 1.0d / pageItems);
PageIndex = pageIndex;
}
protected void Save()
{
JSRuntime.ToggleModal("#DialogDict");
}
}
<div class="form-group col-sm-6">
<label class="control-label" for="dictDefine">字典类型</label>
<Select Id="dictDefine" Items="DefineItems" TItem="int" @bind-SelectedValue="context.Define" SelectedItemChanged="@(item=>context.Define = int.Parse(item.Value))"></Select>
</div>
<div class="form-group col-sm-6">
<label class="control-label" for="dictName">字典名称</label>
<LgbInputText Id="dictName" type="text" class="form-control" @bind-Value="context.Name" placeholder="不可为空50字以内" maxlength="50">
<RequiredValidator AllowEmptyString="false" ErrorMessage="这是必填字段" />
<StringLengthValidator Length="5" ErrorMessage="不可为空50字以内" />
</LgbInputText>
</div>
<div class="form-group col-sm-6">
<label class="control-label" for="dictCode">字典代码</label>
<LgbInputText Id="dictCode" type="text" class="form-control" @bind-Value="context.Code" placeholder="不可为空2000字以内" maxlength="2000" />
</div>
</div>
</EditTemplate>
</EditPage>

View File

@ -17,23 +17,16 @@
<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/sweetalert/sweetalert2.css" rel="stylesheet" />
<link href="~/lib/toastr.js/toastr.css" rel="stylesheet" />
<link href="~/lib/nprogress/nprogress.css" rel="stylesheet" />
<link href="~/lib/sweetalert/sweetalert2.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/sweetalert/sweetalert2.min.css" rel="stylesheet" />
<link href="~/lib/toastr.js/toastr.min.css" rel="stylesheet" />
<link href="~/lib/nprogress/nprogress.min.css" rel="stylesheet" />
<link href="~/lib/sweetalert/sweetalert2.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" />

View File

@ -5,4 +5,5 @@
<Section ShowCardTitle="@RootLayout.Model.ShowCardTitle" LockScreenPeriod=@RootLayout.Model.LockScreenPeriod>
<TabSet @ref="TabSet"></TabSet>
</Section>
<Toast @ref="Toast"></Toast>
</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,31 @@
@typeparam TItem
@inherits EditPageBase<TItem>
<Query OnQuery="Query" TItem="TItem" @bind-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
</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,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

@ -1,8 +1,7 @@
@inherits ModalBase
<!-- Modal -->
<div class="modal fade" id="@Id" tabindex="-1" role="dialog" aria-labelledby="modal_@Id" aria-hidden="true" data-backdrop="@(Backdrop ? "none" : "static")">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<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>
@ -13,9 +12,12 @@
<div class="modal-body">
@ModalBody
</div>
<div class="modal-footer">
@ModalFooter
</div>
@if (ShowFooter)
{
<div class="modal-footer">
@ModalFooter
</div>
}
</div>
</div>
</div>

View File

@ -3,18 +3,19 @@
<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, ItemsCount))</span> 条记录,总共 <span>@ItemsCount</span> 条记录 每页显示
显示第 <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">
<div class="@(PageItems == 20 ? "dropdown-item active" : "dropdown-item")" @onclick="@(e => ClickItem(20))">20</div>
<div class="@(PageItems == 40 ? "dropdown-item active" : "dropdown-item")" @onclick="@(e => ClickItem(40))">40</div>
<div class="@(PageItems == 80 ? "dropdown-item active" : "dropdown-item")" @onclick="@(e => ClickItem(80))">80</div>
@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 d-none" : "pagination")">
<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 = 1; i <= PageCount; i++)
{

View File

@ -17,13 +17,13 @@
///
/// </summary>
[Parameter]
public int PageIndex { get; set; } = 0;
public int PageIndex { get; set; } = 1;
/// <summary>
///
/// </summary>
protected void ClickPage()
{
Pagination?.ClickPageCallback?.Invoke(PageIndex);
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">
<div class="form-inline">
<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="button" id="btn_query" class="btn btn-primary btn-fill" @onclick="e => OnQuery?.Invoke()"><i class="fa fa-search" aria-hidden="true"></i><span>@Text</span></button>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,13 +1,15 @@
@inherits SelectBase
@typeparam TItem
@inherits SelectBase<TItem>
<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">
<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">
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@foreach (var item in Items)
{
<SelectItem Item="@item" ItemClickCallback="ItemClickCallback"></SelectItem>
}
</div>
</div>

View File

@ -1,6 +1,7 @@
@inherits SelectItemBase
<div class="@(Active ? "dropdown-item active" : "dropdown-item")" data-val="@Value" @onclick="ClickItem">@Text</div>
<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,20 @@
@typeparam TItem
@inherits SubmitModalBase<TItem>
<Modal Id="@Id" Title="Title" Size="Size">
<ModalBody>
<LgbEditForm class="form-inline" 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

@ -1,36 +1,89 @@
@typeparam TItem
@inherits TableBase<TItem>
<div class="bootstrap-table">
<div class="@(ShowToolBar ? "bs-bars" : "bs-bars d-none")">
<div class="toolbar btn-group">
<button id="btn_add" type="button" @onclick="(e => AddCallback())" class="btn btn-success" asp-auth="add"><i class="fa fa-plus" aria-hidden="true"></i><span>新增</span></button>
<button id="btn_delete" type="button" @onclick="Delete" class="btn btn-danger" asp-auth="del"><i class="fa fa-remove" aria-hidden="true"></i><span>删除</span></button>
<button id="btn_edit" type="button" @onclick="(e => EditCallback())" class="btn btn-primary" asp-auth="edit"><i class="fa fa-pencil" aria-hidden="true"></i><span>编辑</span></button>
<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 id="tb_add" title="新增" @onclick="(e => AddCallback())" asp-auth="add"><i class="fa fa-plus"></i></div>
<div id="tb_delete" title="删除" @onclick="Delete" asp-auth="del"><i class="fa fa-remove"></i></div>
<div id="tb_edit" title="编辑" @onclick="(e => EditCallback())" asp-auth="edit"><i class="fa fa-pencil"></i></div>
<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">
<thead>
<tr>@TableHeader</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
<tr>@RowTemplate?.Invoke(item)</tr>
}
</tbody>
<tfoot>
<tr>@TableFooter</tr>
</tfoot>
</table>
<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
@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}_submit")" TItem="TItem" Title="字典编辑窗口" Size="ModalSize.ExtraLarge" @bind-Model="EditModel" OnValidSubmit="Save">
<ModalBody>
@EditTemplate?.Invoke(EditModel)
</ModalBody>
</SubmitModal>
@code {

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

@ -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;

View File

@ -1,3 +1,7 @@
#components-reconnect-modal {
z-index: 9999 !important;
}
.nav-link-bar {
border: 1px solid #dee2e6;
border-top-left-radius: 0.25rem;
@ -67,6 +71,35 @@ nav .dropdown .nav-link-close.dropdown-toggle:after {
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;
}
@ -75,6 +108,19 @@ nav .dropdown .nav-link-close.dropdown-toggle:after {
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;
}
@ -83,3 +129,94 @@ nav .dropdown .nav-link-close.dropdown-toggle:after {
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;
}
/* Validation */
input[type="text"].valid, input[type="password"].valid {
padding-right: 30px;
}

View File

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

View File

@ -106,8 +106,9 @@
var $curTab = $navBar.find('.active').first();
return $curTab.next().attr('url');
},
enableAnimation: function () {
initDocument: function () {
$('body').removeClass('trans-mute');
$('[data-toggle="tooltip"]').tooltip();
},
initSidebar: function () {
$('.sidebar').addNiceScroll().autoScrollSidebar();
@ -119,15 +120,30 @@
initModal: function () {
$('.modal').appendTo($('body'));
},
initToast: function () {
$('.toast').appendTo($('body'));
},
toggleModal: function (modalId) {
$(modalId).modal('toggle');
},
showToast: function () {
$('.toast').toast('show');
},
tooltip: function (id, method) {
$(id).tooltip(method);
},
submitForm: function (btn) {
$(btn).parent().prev().find('form :submit').click();
}
});
$(function () {
$(document)
.on('click', '.nav-tabs .nav-link', function (e) {
.on('hidden.bs.toast', '.toast', function () {
$(this).removeClass('hide');
})
.on('inserted.bs.tooltip', '.is-invalid', function () {
$('#' + $(this).attr('aria-describedby')).addClass('is-invalid');
});
});
})(jQuery);

View File

@ -51,7 +51,7 @@
margin-left: 10px;
}
.form-checkbox.is-checked .checkbox-input .checkbox-inner {
.form-checkbox.is-checked .checkbox-input .checkbox-inner, .form-checkbox.is-indeterminate .checkbox-input .checkbox-inner {
background-color: #409eff;
border-color: #409eff;
}
@ -77,8 +77,20 @@
border-color: #c0c4cc;
}
.form-checkbox.is-indeterminate .checkbox-input .checkbox-inner:before {
content: "";
position: absolute;
display: block;
background-color: #fff;
height: 2px;
transform: scale(.5);
left: 0;
right: 0;
top: 5px;
}
@media (min-width: 576px) {
.form-inline .form-checkbox {
display: inline-block;
}
}
}