diff --git a/exclusion.dic b/exclusion.dic index d639525ab3d..a3d2a0b1822 100644 --- a/exclusion.dic +++ b/exclusion.dic @@ -114,3 +114,7 @@ inputmode Totp otpauth Hotp +univer +rdkit +webkitdirectory +dotx diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadAvatars.razor b/src/BootstrapBlazor.Server/Components/Samples/UploadAvatars.razor new file mode 100644 index 00000000000..c18a81a9598 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadAvatars.razor @@ -0,0 +1,79 @@ +@page "/upload-avatar" +@inject IOptionsMonitor WebsiteOption +@inject IStringLocalizer Localizer +@inject ToastService ToastService + +

@Localizer["UploadsTitle"]

+ +

@Localizer["UploadsSubTitle"]

+ +

@((MarkupString)Localizer["UploadsNote"].Value)

+ +
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
+ + + + + + + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadAvatars.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/UploadAvatars.razor.cs new file mode 100644 index 00000000000..bdef10c2e00 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadAvatars.razor.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Forms; + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// AvatarUpload sample code +/// +public partial class UploadAvatars : IDisposable +{ + private static readonly long MaxFileLength = 5 * 1024 * 1024; + private CancellationTokenSource? _token; + private readonly List _previewFileList = []; + private readonly Person _foo = new(); + private bool _isUploadButtonAtFirst; + private bool _isCircle; + private int _radius = 49; + private bool _isMultiple = true; + private bool _isDisabled = false; + + private string? RadiusString => $"{_radius}px"; + + /// + /// + /// + protected override void OnInitialized() + { + base.OnInitialized(); + + _previewFileList.AddRange( + [ + new UploadFile { PrevUrl = $"{WebsiteOption.CurrentValue.AssetRootPath}images/Argo.png" } + ]); + } + + private async Task OnChange(UploadFile file) + { + // 示例代码,使用 base64 格式 + if (file is { File: not null }) + { + var format = file.File.ContentType; + if (file.IsImage()) + { + _token ??= new CancellationTokenSource(); + if (_token.IsCancellationRequested) + { + _token.Dispose(); + _token = new CancellationTokenSource(); + } + + await file.RequestBase64ImageFileAsync(format, 640, 480, MaxFileLength, null, _token.Token); + } + else + { + file.Code = 1; + file.Error = Localizer["UploadsFormatError"]; + } + + if (file.Code != 0) + { + await ToastService.Error(Localizer["UploadsAvatarMsg"], $"{file.Error} {format}"); + } + } + } + + private Task OnAvatarValidSubmit(EditContext context) + { + return ToastService.Error(Localizer["UploadsValidateFormTitle"], Localizer["UploadsValidateFormValidContent"]); + } + + /// + /// + /// + public void Dispose() + { + _token?.Cancel(); + GC.SuppressFinalize(this); + } + + private List GetAttributes() => + [ + new() + { + Name = "Width", + Description = Localizer["UploadsWidth"], + Type = "int", + ValueList = " — ", + DefaultValue = "0" + }, + new() + { + Name = "Height", + Description = Localizer["UploadsHeight"], + Type = "int", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "IsCircle", + Description = Localizer["UploadsIsCircle"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "BorderRadius", + Description = Localizer["UploadsBorderRadius"], + Type = "string?", + ValueList = " — ", + DefaultValue = " — " + } + ]; + + class Person + { + [Required] + [StringLength(20, MinimumLength = 2)] + public string Name { get; set; } = "Blazor"; + + [Required] + [FileValidation(Extensions = [".png", ".jpg", ".jpeg"], FileSize = 5 * 1024 * 1024)] + public List? Picture { get; set; } + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadButtons.razor b/src/BootstrapBlazor.Server/Components/Samples/UploadButtons.razor new file mode 100644 index 00000000000..78cad6f9254 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadButtons.razor @@ -0,0 +1,61 @@ +@page "/upload-button" +@inject IOptionsMonitor WebsiteOption +@inject IStringLocalizer Localizer +@inject ToastService ToastService + +

@Localizer["UploadsTitle"]

+ +

@Localizer["UploadsSubTitle"]

+ +

@((MarkupString)Localizer["UploadsNote"].Value)

+ +
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadButtons.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/UploadButtons.razor.cs new file mode 100644 index 00000000000..6e96250e7b6 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadButtons.razor.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// ButtonUpload sample code +/// +public partial class UploadButtons : IDisposable +{ + private static readonly Random Random = new(); + private static readonly long MaxFileLength = 5 * 1024 * 1024; + private CancellationTokenSource? _token; + + private bool _isMultiple = true; + private bool _showProgress = true; + private bool _showUploadFileList = true; + private bool _showDownloadButton = true; + private bool _isDirectory = false; + private bool _isDisabled = false; + + private async Task OnClickToUpload(UploadFile file) + { + // 示例代码,模拟 80% 几率保存成功 + var error = Random.Next(1, 100) > 80; + if (error) + { + file.Code = 1; + file.Error = Localizer["UploadsError"]; + } + else + { + await SaveToFile(file); + } + } + + private async Task OnDownload(UploadFile item) + { + await ToastService.Success("文件下载", $"下载 {item.FileName} 成功"); + } + + private async Task OnDelete(UploadFile item) + { + await ToastService.Success("文件操作", $"删除文件 {item.FileName} 成功"); + return true; + } + + private async Task SaveToFile(UploadFile file) + { + // Server Side 使用 + // Web Assembly 模式下必须使用 WebApi 方式去保存文件到服务器或者数据库中 + // 生成写入文件名称 + if (!string.IsNullOrEmpty(WebsiteOption.CurrentValue.WebRootPath)) + { + var uploaderFolder = Path.Combine(WebsiteOption.CurrentValue.WebRootPath, "images", "uploader"); + file.FileName = $"{Path.GetFileNameWithoutExtension(file.OriginFileName)}-{DateTimeOffset.Now:yyyyMMddHHmmss}{Path.GetExtension(file.OriginFileName)}"; + var fileName = Path.Combine(uploaderFolder, file.FileName); + + _token ??= new CancellationTokenSource(); + try + { + var ret = await file.SaveToFileAsync(fileName, MaxFileLength, _token.Token); + + if (ret) + { + // 保存成功 + file.PrevUrl = $"{WebsiteOption.CurrentValue.AssetRootPath}images/uploader/{file.FileName}"; + } + else + { + var errorMessage = $"{Localizer["UploadsSaveFileError"]} {file.OriginFileName}"; + file.Code = 1; + file.Error = errorMessage; + await ToastService.Error(Localizer["UploadFile"], errorMessage); + } + } + catch (OperationCanceledException) + { + + } + } + else + { + file.Code = 1; + file.Error = Localizer["UploadsWasmError"]; + await ToastService.Information(Localizer["UploadsSaveFile"], Localizer["UploadsSaveFileMsg"]); + } + } + + /// + /// + /// + public void Dispose() + { + _token?.Cancel(); + _token?.Dispose(); + _token = null; + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadCards.razor b/src/BootstrapBlazor.Server/Components/Samples/UploadCards.razor new file mode 100644 index 00000000000..841d13409e4 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadCards.razor @@ -0,0 +1,101 @@ +@page "/upload-card" +@inject IOptionsMonitor WebsiteOption +@inject IStringLocalizer Localizer +@inject ToastService ToastService + +

@Localizer["UploadsTitle"]

+ +

@Localizer["UploadsSubTitle"]

+ +

@((MarkupString)Localizer["UploadsNote"].Value)

+ +
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadCards.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/UploadCards.razor.cs new file mode 100644 index 00000000000..31f9dba89f6 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadCards.razor.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Newtonsoft.Json.Linq; + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// CardUpload sample code +/// +public partial class UploadCards : IDisposable +{ + private bool _isMultiple = true; + private bool _isDirectory = false; + private bool _isDisabled = false; + private bool _isUploadButtonAtFirst = false; + private bool _showProgress = true; + private bool _showZoomButton = true; + private bool _showDeleteButton = true; + private bool _showDownloadButton = true; + + private List DefaultFormatFileList { get; } = + [ + new() { FileName = "Test.xls" }, + new() { FileName = "Test.doc" }, + new() { FileName = "Test.ppt" }, + new() { FileName = "Test.mp3" }, + new() { FileName = "Test.mp4" }, + new() { FileName = "Test.pdf" }, + new() { FileName = "Test.cs" }, + new() { FileName = "Test.zip" }, + new() { FileName = "Test.txt" }, + new() { FileName = "Test.dat" } + ]; + + private List Base64FormatFileList { get; } = + [ + new() { FileName = "Test", PrevUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAkCAYAAAD/yagrAAAE60lEQVR4AWJwL/AB9GIWvI0jURz/L1NomZmZP8p9kWNmhnIbKPdomZmZGcJgO7QsWMbce55Yl11t47FbVdKTYpjOr/+H4y4Z/MpYeJVV8KVvokFRylq9osKfVtGQ/NHyPl2CbNVGwau2oOlmgUDJtDJGzxtvvIA3vRH+7IgeA0VtYhQBtKPlToFgC6RY58aggfwLNKhr0Zry2NnPrpIM2YGW2+UBG1IEmdGVJEVXE+hQXt8joKhLjdGVZHd7TSD9DHmT3J1dheroSF4fhnvUVQwbZx9UOnHSzQjkTN0tIG+8hFdbxet4PQOG4WqJwN1hFVb+xUBmCblxvxRkIE+Q+Yf0T31NSrp4/RV4FhHgoSTchQRZBK4D1+Fe2q2g8CYXU6buQyNDaiZKZhn0IYXHVwbkRQydHyawOAGGisa/o3DvI/jF3QKKiuAy+LKs5CvaXMLdeXb35wbkKTjmReDZHysCBosWJqN7r0jZ/VfgXtIlUNQnVpK7D4nNJSC5BHGi1d108Ppr8MxlF0cJqBSyFJaUfUnvHLyO4cttgaI+NhGB7HE0MaSUks/gU5vwe1gv5tfhmUmAhxME8jbIUtiEDus+fhmDJlgCRY0ylZRZhybKWtNinmYlH9NvL5puO3m9UHOkgzb3xuF5HCkDyhYiSwrYVfQPTpYChTc+E35tG9VJBjGFFNmtVlNMDnjzbx0EBpKq1eTeh2awbArBRuHadBWu6WVB4U/NITfuobYoCZl7QL//wDtr+nTmsgzQh2Kwgtz7QAZWFeGw/RI8s94Kigp1PvzZg2i9x11FDtKn/oZCoZdZxhaAXmG4/ohJKHudLE1Gyu66DOfs10BRR5CU3TKQIrtzj9BAkF991dsMslTZEClLsI+jFmANZYFAdAJl9QG03jWH9GrcFh9QTP4Of6GfJGSpsv1J2aoEKRuWCANNJNqOFAaPAW36rVRMCjVf6ZCtqYF2p6Asxg5mWK6tQamY9RCs8z1wF+FxTR5UqYT/3GC7oCkMHcjxKguq6cnl+Egf2whip3C9Yu56jk+GXXOtv1XIa8L1f3AFkHe9az83AmNanwV/bhfa5JKJ3n1slCUL8dk7CNfvVFMfySRThoxbK7fh18tTZXI2QeySLk88IRGsbHkqQj6wUJ72G5CloCXKZrdbLPgVMgVfQMq5m97bfQWOOeVbaK02nSA2ofn2y662UJE47horLZTe38oDjdxQUhmawocxa0OJ5hXjnZE4w1y0uc/iULKGfk+xNuZxI/BnjhFs+THPq4phmTsbeYPXRjFsJJ+NSMlnInHkxrwzGDjR3uBcG1/B/b/TwZkhubb6tP2oVfTAD2G4kw9vYiA2h+zy4GwYxd7S4qGOgd6oqVn6re1DTXiO4e4wPF+yQlF4ZCBfheSPIjJn+ciS/w93qjA6aZYqeQ5D3dTqvuE6GZNTkov5vqsYvLh7j8u1sWWkLIdBQR+q+XdtaKGhJCWBDpkwgSw9gRpKyoNKw6rjCLCDP4yVflSgWFwt3C0HSUo2BTFYX28fVAaWPpDx7wtwjOSSUszawnUT0KTudlfrFQwZ3WNf867CNZQhk/C8kIFUhJLt/O3Jzn62IYNwrUvA8zws4W61CGlDSfugXMz5pJgiSFYyXMYiRXdH4GmyqaR9UHLxzxG41KAwpZxRPN4kRf85Z5I4MvYfFUFGfemJG40AAAAASUVORK5CYII=" }, + ]; + + private static long MaxFileLength => 5 * 1024 * 1024; + private CancellationTokenSource? _token; + + private async Task OnCardUpload(UploadFile file) + { + if (file is { File: not null }) + { + // 服务器端验证当文件大于 5MB 时提示文件太大信息 + if (file.Size > MaxFileLength) + { + await ToastService.Information(Localizer["UploadsFileMsg"], Localizer["UploadsFileError"]); + file.Code = 1; + file.Error = Localizer["UploadsFileError"]; + } + else + { + // 模拟保存成功 + await Task.Delay(100); + await SaveToFile(file); + await ToastService.Success(Localizer["UploadsFileMsg"], $"{file.File!.Name} {Localizer["UploadsSuccess"]}"); + } + } + } + + private async Task SaveToFile(UploadFile file) + { + // Server Side 使用 + // Web Assembly 模式下必须使用 WebApi 方式去保存文件到服务器或者数据库中 + // 生成写入文件名称 + if (!string.IsNullOrEmpty(WebsiteOption.CurrentValue.WebRootPath)) + { + var uploaderFolder = Path.Combine(WebsiteOption.CurrentValue.WebRootPath, "images", "uploader"); + file.FileName = $"{Path.GetFileNameWithoutExtension(file.OriginFileName)}-{DateTimeOffset.Now:yyyyMMddHHmmss}{Path.GetExtension(file.OriginFileName)}"; + var fileName = Path.Combine(uploaderFolder, file.FileName); + + _token ??= new CancellationTokenSource(); + try + { + var ret = await file.SaveToFileAsync(fileName, MaxFileLength, _token.Token); + + if (ret) + { + // 保存成功 + file.PrevUrl = $"{WebsiteOption.CurrentValue.AssetRootPath}images/uploader/{file.FileName}"; + } + else + { + var errorMessage = $"{Localizer["UploadsSaveFileError"]} {file.OriginFileName}"; + file.Code = 1; + file.Error = errorMessage; + await ToastService.Error(Localizer["UploadFile"], errorMessage); + } + } + catch (OperationCanceledException) + { + + } + } + else + { + file.Code = 1; + file.Error = Localizer["UploadsWasmError"]; + await ToastService.Information(Localizer["UploadsSaveFile"], Localizer["UploadsSaveFileMsg"]); + } + } + + /// + /// + /// + public void Dispose() + { + _token?.Cancel(); + _token?.Dispose(); + _token = null; + GC.SuppressFinalize(this); + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadDrops.razor b/src/BootstrapBlazor.Server/Components/Samples/UploadDrops.razor new file mode 100644 index 00000000000..1b2949baba6 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadDrops.razor @@ -0,0 +1,60 @@ +@page "/upload-drop" +@inject IOptionsMonitor WebsiteOption +@inject IStringLocalizer Localizer +@inject ToastService ToastService + +

@Localizer["UploadsTitle"]

+ +

@Localizer["UploadsSubTitle"]

+ +

@((MarkupString)Localizer["UploadsNote"].Value)

+ +
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
+ + diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadDrops.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/UploadDrops.razor.cs new file mode 100644 index 00000000000..f74d75b4fba --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadDrops.razor.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// DropUpload sample code +/// +public partial class UploadDrops +{ + private bool _isMultiple = true; + private bool _isDisabled = false; + private bool _showProgress = true; + private bool _showFooter = true; + private bool _showUploadFileList = true; + private bool _showDownloadButton = true; + + private Task OnDropUpload(UploadFile file) + { + // 模拟保存文件等处理 + if (file.File is { Size: > 5 * 1024 * 1024 }) + { + file.Code = 1004; + ToastService.Information("Error", Localizer["DropUploadFooterText"]); + } + return Task.CompletedTask; + } + + private List GetAttributes() => + [ + new() + { + Name = "BodyTemplate", + Description = Localizer["UploadsBodyTemplate"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "IconTemplate", + Description = Localizer["UploadsIconTemplate"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "TextTemplate", + Description = Localizer["UploadsTextTemplate"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "UploadIcon", + Description = Localizer["UploadsUploadIcon"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "UploadText", + Description = Localizer["UploadsUploadText"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "ShowFooter", + Description = Localizer["UploadsShowFooter"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "FooterTemplate", + Description = Localizer["UploadsFooterTemplate"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "FooterText", + Description = Localizer["UploadsFooterText"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + } + ]; +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadInputs.razor b/src/BootstrapBlazor.Server/Components/Samples/UploadInputs.razor new file mode 100644 index 00000000000..201feac8a82 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadInputs.razor @@ -0,0 +1,49 @@ +@page "/upload-input" +@inject IStringLocalizer Localizer +@inject ToastService ToastService + +

@Localizer["UploadsTitle"]

+ +

@Localizer["UploadsSubTitle"]

+ +

@((MarkupString)Localizer["UploadsNote"].Value)

+ +
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);
+ + +
+
+ +
+
+
+ + +
+
    +
  • @((MarkupString)Localizer["UploadFormSettingsLi1"].Value)
  • +
  • @((MarkupString)Localizer["UploadFormSettingsLi2"].Value)
  • +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + diff --git a/src/BootstrapBlazor.Server/Components/Samples/UploadInputs.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/UploadInputs.razor.cs new file mode 100644 index 00000000000..56412699741 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/UploadInputs.razor.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Forms; + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// InputUpload sample code +/// +public partial class UploadInputs +{ + private Person Foo1 { get; set; } = new Person(); + + private async Task OnFileChange(UploadFile file) + { + // 未真正保存文件 + // file.SaveToFile() + await Task.Delay(200); + await ToastService.Information(Localizer["UploadsSaveFile"], $"{file.File!.Name} {Localizer["UploadsSuccess"]}"); + } + + private static Task OnSubmit(EditContext context) + { + // 示例代码请根据业务情况自行更改 + // var fileName = Foo.Picture?.Name; + return Task.CompletedTask; + } + + private List GetAttributes() => + [ + new() + { + Name = "IsDirectory", + Description = Localizer["UploadsIsDirectory"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "IsMultiple", + Description = Localizer["UploadsIsMultiple"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "ShowDeleteButton", + Description = Localizer["UploadsShowDeleteButton"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "IsDisabled", + Description = Localizer["UploadsIsDisabled"], + Type = "boolean", + ValueList = "true / false", + DefaultValue = "false" + }, + new() + { + Name = "PlaceHolder", + Description = Localizer["UploadsPlaceHolder"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "Accept", + Description = Localizer["UploadsAccept"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "BrowserButtonClass", + Description = Localizer["UploadsBrowserButtonClass"], + Type = "string", + ValueList = " — ", + DefaultValue = "btn-primary" + }, + new() + { + Name = "BrowserButtonIcon", + Description = Localizer["UploadsBrowserButtonIcon"], + Type = "string", + ValueList = " — ", + DefaultValue = "fa-regular fa-folder-open" + }, + new() + { + Name = "BrowserButtonText", + Description = Localizer["UploadsBrowserButtonText"], + Type = "string", + ValueList = " — ", + DefaultValue = "" + }, + new() + { + Name = "DeleteButtonClass", + Description = Localizer["UploadsDeleteButtonClass"], + Type = "string", + ValueList = " — ", + DefaultValue = "btn-danger" + }, + new() + { + Name = "DeleteButtonIcon", + Description = Localizer["UploadsDeleteButtonIcon"], + Type = "string", + ValueList = " — ", + DefaultValue = "fa-regular fa-trash" + }, + new() + { + Name = "DeleteButtonText", + Description = Localizer["UploadsDeleteButtonText"], + Type = "string", + ValueList = " — ", + DefaultValue = Localizer["UploadsDeleteButtonTextDefaultValue"] + }, + new() + { + Name = "OnDelete", + Description = Localizer["UploadsOnDelete"], + Type = "Func>", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "OnChange", + Description = Localizer["UploadsOnChange"], + Type = "Func", + ValueList = " — ", + DefaultValue = " — " + } + ]; + + class Person + { + [Required] + [StringLength(20, MinimumLength = 2)] + public string Name { get; set; } = "Blazor"; + + [Required] + [FileValidation(Extensions = [".png", ".jpg", ".jpeg"], FileSize = 5 * 1024 * 1024)] + public IBrowserFile? Picture { get; set; } + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/Uploads.razor b/src/BootstrapBlazor.Server/Components/Samples/Uploads.razor deleted file mode 100644 index 17dc5ab1615..00000000000 --- a/src/BootstrapBlazor.Server/Components/Samples/Uploads.razor +++ /dev/null @@ -1,196 +0,0 @@ -@page "/upload" -@inject IOptionsMonitor WebsiteOption -@inject IStringLocalizer Localizer -@inject ToastService ToastService -@implements IDisposable - -

@Localizer["UploadsTitle"]

- -

@Localizer["UploadsSubTitle"]

- -

@((MarkupString)Localizer["UploadsNote"].Value)

- -
builder.Services.Configure<HubOptions>(option => option.MaximumReceiveMessageSize = null);
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
    -
  • @((MarkupString)Localizer["UploadFormSettingsLi1"].Value)
  • -
  • @((MarkupString)Localizer["UploadFormSettingsLi2"].Value)
  • -
-
- -
-
- -
-
- -
-
- -
-
-
-
- - -
-
-

@((MarkupString)Localizer["UploadClickUploadTips1"].Value)

- -
-
-
-

@((MarkupString)Localizer["UploadClickUploadTips2"].Value)

- -

@((MarkupString)Localizer["UploadClickUploadTips3ShowUploadList"].Value)

- -
-
- - -
-
- -
-
-
- - -
-
- -
-
-
- - -
-
-

@Localizer["AvatarUploadTips1"]

- -
-
-

@Localizer["AvatarUploadTips2"]

- -
-
-

@((MarkupString)Localizer["AvatarUploadTips3"].Value)

- -

@((MarkupString)Localizer["AvatarUploadTips5"].Value)

- -
-
-

@((MarkupString)Localizer["AvatarUploadTips6"].Value)

- -
-
-

@((MarkupString)Localizer["AvatarUploadTips7"].Value)

- -
-
- -
-
- -
-
- -
-
-
-
-
- -
- - -
-
@((MarkupString)Localizer["UploadPreCardStyleSSR"].Value)
-
@((MarkupString)Localizer["UploadPreCardStyleServerSide"].Value)
-
@((MarkupString)Localizer["UploadPreCardStyleWasm"].Value)
-
@((MarkupString)Localizer["UploadPreCardStyleWasmSide"].Value)
-
@((MarkupString)Localizer["UploadPreCardStyleLink", WebsiteOption.CurrentValue.VideoLibUrl].Value)
-
@((MarkupString)Localizer["UploadPreCardStyleValidation"].Value)
-
-
-
-

@((MarkupString)Localizer["UploadPreCardStyleTips1"].Value)

- -
-
-

@((MarkupString)Localizer["UploadPreCardStyleTips2"].Value)

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/BootstrapBlazor.Server/Components/Samples/Uploads.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Uploads.razor.cs deleted file mode 100644 index e641cc263b3..00000000000 --- a/src/BootstrapBlazor.Server/Components/Samples/Uploads.razor.cs +++ /dev/null @@ -1,492 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License -// See the LICENSE file in the project root for more information. -// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone - -using Microsoft.AspNetCore.Components.Forms; - -namespace BootstrapBlazor.Server.Components.Samples; - -/// -/// Uploads -/// -public sealed partial class Uploads -{ - [NotNull] - private ConsoleLogger? Logger1 { get; set; } - - [NotNull] - private ConsoleLogger? Logger2 { get; set; } - - private static readonly Random random = new(); - - private CancellationTokenSource? ReadToken { get; set; } - - private static long MaxFileLength => 5 * 1024 * 1024; - - private Person Foo1 { get; set; } = new Person(); - - private Person Foo2 { get; set; } = new Person(); - - private List PreviewFileList { get; } = []; - - private CancellationTokenSource? ReadAvatarToken { get; set; } - - private List DefaultFormatFileList { get; } = - [ - new UploadFile { FileName = "Test.xls" }, - new UploadFile { FileName = "Test.doc" }, - new UploadFile { FileName = "Test.ppt" }, - new UploadFile { FileName = "Test.mp3" }, - new UploadFile { FileName = "Test.mp4" }, - new UploadFile { FileName = "Test.pdf" }, - new UploadFile { FileName = "Test.cs" }, - new UploadFile { FileName = "Test.zip" }, - new UploadFile { FileName = "Test.txt" }, - new UploadFile { FileName = "Test.dat" } - ]; - - private List Base64FormatFileList { get; } = - [ - new UploadFile { FileName = "Test", PrevUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAkCAYAAAD/yagrAAAE60lEQVR4AWJwL/AB9GIWvI0jURz/L1NomZmZP8p9kWNmhnIbKPdomZmZGcJgO7QsWMbce55Yl11t47FbVdKTYpjOr/+H4y4Z/MpYeJVV8KVvokFRylq9osKfVtGQ/NHyPl2CbNVGwau2oOlmgUDJtDJGzxtvvIA3vRH+7IgeA0VtYhQBtKPlToFgC6RY58aggfwLNKhr0Zry2NnPrpIM2YGW2+UBG1IEmdGVJEVXE+hQXt8joKhLjdGVZHd7TSD9DHmT3J1dheroSF4fhnvUVQwbZx9UOnHSzQjkTN0tIG+8hFdbxet4PQOG4WqJwN1hFVb+xUBmCblxvxRkIE+Q+Yf0T31NSrp4/RV4FhHgoSTchQRZBK4D1+Fe2q2g8CYXU6buQyNDaiZKZhn0IYXHVwbkRQydHyawOAGGisa/o3DvI/jF3QKKiuAy+LKs5CvaXMLdeXb35wbkKTjmReDZHysCBosWJqN7r0jZ/VfgXtIlUNQnVpK7D4nNJSC5BHGi1d108Ppr8MxlF0cJqBSyFJaUfUnvHLyO4cttgaI+NhGB7HE0MaSUks/gU5vwe1gv5tfhmUmAhxME8jbIUtiEDus+fhmDJlgCRY0ylZRZhybKWtNinmYlH9NvL5puO3m9UHOkgzb3xuF5HCkDyhYiSwrYVfQPTpYChTc+E35tG9VJBjGFFNmtVlNMDnjzbx0EBpKq1eTeh2awbArBRuHadBWu6WVB4U/NITfuobYoCZl7QL//wDtr+nTmsgzQh2Kwgtz7QAZWFeGw/RI8s94Kigp1PvzZg2i9x11FDtKn/oZCoZdZxhaAXmG4/ohJKHudLE1Gyu66DOfs10BRR5CU3TKQIrtzj9BAkF991dsMslTZEClLsI+jFmANZYFAdAJl9QG03jWH9GrcFh9QTP4Of6GfJGSpsv1J2aoEKRuWCANNJNqOFAaPAW36rVRMCjVf6ZCtqYF2p6Asxg5mWK6tQamY9RCs8z1wF+FxTR5UqYT/3GC7oCkMHcjxKguq6cnl+Egf2whip3C9Yu56jk+GXXOtv1XIa8L1f3AFkHe9az83AmNanwV/bhfa5JKJ3n1slCUL8dk7CNfvVFMfySRThoxbK7fh18tTZXI2QeySLk88IRGsbHkqQj6wUJ72G5CloCXKZrdbLPgVMgVfQMq5m97bfQWOOeVbaK02nSA2ofn2y662UJE47horLZTe38oDjdxQUhmawocxa0OJ5hXjnZE4w1y0uc/iULKGfk+xNuZxI/BnjhFs+THPq4phmTsbeYPXRjFsJJ+NSMlnInHkxrwzGDjR3uBcG1/B/b/TwZkhubb6tP2oVfTAD2G4kw9vYiA2h+zy4GwYxd7S4qGOgd6oqVn6re1DTXiO4e4wPF+yQlF4ZCBfheSPIjJn+ciS/w93qjA6aZYqeQ5D3dTqvuE6GZNTkov5vqsYvLh7j8u1sWWkLIdBQR+q+XdtaKGhJCWBDpkwgSw9gRpKyoNKw6rjCLCDP4yVflSgWFwt3C0HSUo2BTFYX28fVAaWPpDx7wtwjOSSUszawnUT0KTudlfrFQwZ3WNf867CNZQhk/C8kIFUhJLt/O3Jzn62IYNwrUvA8zws4W61CGlDSfugXMz5pJgiSFYyXMYiRXdH4GmyqaR9UHLxzxG41KAwpZxRPN4kRf85Z5I4MvYfFUFGfemJG40AAAAASUVORK5CYII=" }, - ]; - - /// - /// - /// - protected override void OnInitialized() - { - base.OnInitialized(); - - PreviewFileList.AddRange(new[] - { - new UploadFile { PrevUrl = $"{WebsiteOption.CurrentValue.AssetRootPath}images/Argo.png" } - }); - } - - private Task OnFileChange(UploadFile file) - { - // 未真正保存文件 - // file.SaveToFile() - Logger1.Log($"{file.File!.Name} {Localizer["UploadsSuccess"]}"); - return Task.CompletedTask; - } - - private Task OnFileDelete(UploadFile item) - { - Logger1.Log($"{item.OriginFileName} {Localizer["UploadsRemoveMsg"]}"); - return Task.FromResult(true); - } - - private static Task OnSubmit(EditContext context) - { - // 示例代码请根据业务情况自行更改 - // var fileName = Foo.Picture?.Name; - return Task.CompletedTask; - } - - private async Task OnClickToUpload(UploadFile file) - { - // 示例代码,模拟 80% 几率保存成功 - var error = random.Next(1, 100) > 80; - if (error) - { - file.Code = 1; - file.Error = Localizer["UploadsError"]; - } - else - { - await SaveToFile(file); - } - } - - private async Task OnClickToUploadNoUploadList(UploadFile file) - { - await ToastService.Success("Upload", $"{file.OriginFileName} uploaded success."); - } - - private async Task SaveToFile(UploadFile file) - { - // Server Side 使用 - // Web Assembly 模式下必须使用 WebApi 方式去保存文件到服务器或者数据库中 - // 生成写入文件名称 - var ret = false; - if (!string.IsNullOrEmpty(WebsiteOption.CurrentValue.WebRootPath)) - { - var uploaderFolder = Path.Combine(WebsiteOption.CurrentValue.WebRootPath, $"images{Path.DirectorySeparatorChar}uploader"); - file.FileName = $"{Path.GetFileNameWithoutExtension(file.OriginFileName)}-{DateTimeOffset.Now:yyyyMMddHHmmss}{Path.GetExtension(file.OriginFileName)}"; - var fileName = Path.Combine(uploaderFolder, file.FileName); - - ReadToken ??= new CancellationTokenSource(); - ret = await file.SaveToFileAsync(fileName, MaxFileLength, ReadToken.Token); - - if (ret) - { - // 保存成功 - file.PrevUrl = $"{WebsiteOption.CurrentValue.AssetRootPath}images/uploader/{file.FileName}"; - } - else - { - var errorMessage = $"{Localizer["UploadsSaveFileError"]} {file.OriginFileName}"; - file.Code = 1; - file.Error = errorMessage; - await ToastService.Error(Localizer["UploadFile"], errorMessage); - } - } - else - { - file.Code = 1; - file.Error = Localizer["UploadsWasmError"]; - await ToastService.Information(Localizer["UploadsSaveFile"], Localizer["UploadsSaveFileMsg"]); - } - return ret; - } - - private async Task OnDownload(UploadFile item) - { - await ToastService.Success("文件下载", $"下载 {item.FileName} 成功"); - } - - private async Task OnUploadFolder(UploadFile file) - { - // 上传文件夹时会多次回调此方法 - await SaveToFile(file); - } - - private async Task OnAvatarUpload(UploadFile file) - { - // 示例代码,使用 base64 格式 - if (file != null && file.File != null) - { - var format = file.File.ContentType; - if (CheckValidAvatarFormat(format)) - { - ReadAvatarToken ??= new CancellationTokenSource(); - if (ReadAvatarToken.IsCancellationRequested) - { - ReadAvatarToken.Dispose(); - ReadAvatarToken = new CancellationTokenSource(); - } - - await file.RequestBase64ImageFileAsync(format, 640, 480, MaxFileLength, ReadAvatarToken.Token); - } - else - { - file.Code = 1; - file.Error = Localizer["UploadsFormatError"]; - } - - if (file.Code != 0) - { - await ToastService.Error(Localizer["UploadsAvatarMsg"], $"{file.Error} {format}"); - } - } - } - - private static bool CheckValidAvatarFormat(string format) - { - return "jpg;png;bmp;gif;jpeg".Split(';').Any(f => format.Contains(f, StringComparison.OrdinalIgnoreCase)); - } - - private Task OnAvatarValidSubmit(EditContext context) - { - Logger2.Log(Foo2.Picture?.Name ?? ""); - return Task.CompletedTask; - } - - private async Task OnCardUpload(UploadFile file) - { - if (file != null && file.File != null) - { - // 服务器端验证当文件大于 5MB 时提示文件太大信息 - if (file.Size > MaxFileLength) - { - await ToastService.Information(Localizer["UploadsFileMsg"], Localizer["UploadsFileError"]); - file.Code = 1; - file.Error = Localizer["UploadsFileError"]; - } - else - { - await SaveToFile(file); - } - } - } - - /// - /// Dispose - /// - public void Dispose() - { - ReadToken?.Cancel(); - ReadAvatarToken?.Cancel(); - GC.SuppressFinalize(this); - } - - private List GetInputAttributes() => - [ - new() { - Name = "ShowDeleteButton", - Description = Localizer["UploadsShowDeleteButton"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "IsDisabled", - Description = Localizer["UploadsIsDisabled"], - Type = "boolean", - ValueList = "true / false", - DefaultValue = "false" - }, - new() { - Name = "PlaceHolder", - Description = Localizer["UploadsPlaceHolder"], - Type = "string", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "Accept", - Description = Localizer["UploadsAccept"], - Type = "string", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "BrowserButtonClass", - Description = Localizer["UploadsBrowserButtonClass"], - Type = "string", - ValueList = " — ", - DefaultValue = "btn-primary" - }, - new() { - Name = "BrowserButtonIcon", - Description = Localizer["UploadsBrowserButtonIcon"], - Type = "string", - ValueList = " — ", - DefaultValue = "fa-regular fa-folder-open" - }, - new() { - Name = "BrowserButtonText", - Description = Localizer["UploadsBrowserButtonText"], - Type = "string", - ValueList = " — ", - DefaultValue = "" - }, - new() { - Name = "DeleteButtonClass", - Description = Localizer["UploadsDeleteButtonClass"], - Type = "string", - ValueList = " — ", - DefaultValue = "btn-danger" - }, - new() { - Name = "DeleteButtonIcon", - Description = Localizer["UploadsDeleteButtonIcon"], - Type = "string", - ValueList = " — ", - DefaultValue = "fa-regular fa-trash" - }, - new() { - Name = "DeleteButtonText", - Description = Localizer["UploadsDeleteButtonText"], - Type = "string", - ValueList = " — ", - DefaultValue = Localizer["UploadsDeleteButtonTextDefaultValue"] - }, - new() { - Name = "OnDelete", - Description = Localizer["UploadsOnDelete"], - Type = "Func>", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnChange", - Description = Localizer["UploadsOnChange"], - Type = "Func", - ValueList = " — ", - DefaultValue = " — " - } - ]; - - private List GetButtonAttributes() => - [ - new() { - Name = "IsDirectory", - Description = Localizer["UploadsIsDirectory"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "IsMultiple", - Description = Localizer["UploadsIsMultiple"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "IsSingle", - Description = Localizer["UploadsIsSingle"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "ShowProgress", - Description = Localizer["UploadsShowProgress"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "Accept", - Description = Localizer["UploadsAccept"], - Type = "string", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "BrowserButtonClass", - Description = Localizer["UploadsBrowserButtonClass"], - Type = "string", - ValueList = " — ", - DefaultValue = "btn-primary" - }, - new() { - Name = "BrowserButtonIcon", - Description = Localizer["UploadsBrowserButtonIcon"], - Type = "string", - ValueList = " — ", - DefaultValue = "fa-regular fa-folder-open" - }, - new() { - Name = "BrowserButtonText", - Description = Localizer["UploadsBrowserButtonText"], - Type = "string", - ValueList = " — ", - DefaultValue = "" - }, - new() { - Name = "DefaultFileList", - Description = Localizer["UploadsDefaultFileList"], - Type = "List", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnGetFileFormat", - Description = Localizer["UploadsOnGetFileFormat"], - Type = "Func", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnDelete", - Description = Localizer["UploadsOnDelete"], - Type = "Func>", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnChange", - Description = Localizer["UploadsOnChange"], - Type = "Func", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnDownload", - Description = Localizer["UploadsOnDownload"], - Type = "Func", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "IconTemplate", - Description = Localizer["UploadsIconTemplate"], - Type = "RenderFragment", - ValueList = " — ", - DefaultValue = " — " - } - ]; - - private List GetAvatarAttributes() => - [ - new() { - Name = "Width", - Description = Localizer["UploadsWidth"], - Type = "int", - ValueList = " — ", - DefaultValue = "0" - }, - new() { - Name = "Height", - Description = Localizer["UploadsHeight"], - Type = "int", - ValueList = "—", - DefaultValue = "0" - }, - new() { - Name = "IsCircle", - Description = Localizer["UploadsIsCircle"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "IsSingle", - Description = Localizer["UploadsIsSingle"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "ShowProgress", - Description = Localizer["UploadsShowProgress"], - Type = "bool", - ValueList = "true|false", - DefaultValue = "false" - }, - new() { - Name = "Accept", - Description = Localizer["UploadsAccept"], - Type = "string", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "DefaultFileList", - Description = Localizer["UploadsDefaultFileList"], - Type = "List", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnDelete", - Description = Localizer["UploadsOnDelete"], - Type = "Func>", - ValueList = " — ", - DefaultValue = " — " - }, - new() { - Name = "OnChange", - Description = Localizer["UploadsOnChange"], - Type = "Func", - ValueList = " — ", - DefaultValue = " — " - } - ]; - - private class Person - { - [Required] - [StringLength(20, MinimumLength = 2)] - public string Name { get; set; } = "Blazor"; - - [Required] - [FileValidation(Extensions = [".png", ".jpg", ".jpeg"], FileSize = 50 * 1024)] - public IBrowserFile? Picture { get; set; } - } -} diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs index 2f419d70d46..80ea5a633d3 100644 --- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs @@ -489,8 +489,28 @@ void AddForm(DemoMenuItem item) }, new() { - Text = Localizer["Upload"], - Url = "upload" + Text = Localizer["InputUpload"], + Url = "upload-input" + }, + new() + { + Text = Localizer["ButtonUpload"], + Url = "upload-button" + }, + new() + { + Text = Localizer["AvatarUpload"], + Url = "upload-avatar" + }, + new() + { + Text = Localizer["CardUpload"], + Url = "upload-card" + }, + new() + { + Text = Localizer["DropUpload"], + Url = "upload-drop" }, new() { diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 872ce12f3b0..65cd7bd79d6 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -3460,60 +3460,49 @@ "RightHeaderTemplate": "the right panel Header template", "RightItemTemplate": "the right panel Item template" }, - "BootstrapBlazor.Server.Components.Samples.Uploads": { - "UploadsTitle": "Upload", - "UploadsSubTitle": "Upload the file by clicking", + "BootstrapBlazor.Server.Components.Samples.UploadAvatars": { + "UploadsTitle": "AvatarUpload", + "UploadsSubTitle": "Click to upload a file, usually used to upload a preview of one or a group of avatar-like images", "UploadsNote": "If you edit too much content, signalR communication interruption may be triggered. Please adjust the HubOptions configuration.", - "UploadNormalTitle": "Basic usage", - "UploadNormalIntro": "The InputUpload component is used with other form components to display the file name, select the file and upload it by clicking the browse button, and by setting the ShowRemoveButton parameter, display the delete button, click the delete button to call back onDelete delegate method", - "UploadNormalLabelName": "Name:", - "UploadNormalLabelAddress": "Address:", - "UploadNormalLabelPhoto": "Photo:", - "UploadFormSettingsTitle": "FormSettings", - "UploadFormSettingsIntro": "Use the file upload component to constrain the file format within the form", - "UploadFormSettingsLi1": "Using the ValidateForm form component, custom validation is set by setting the fileValidation label of the model properties to support file extensionsizevalidation, in this case with the extension .png .jpg .jpeg and the file size limit to 50K", - "UploadFormSettingsLi2": "After selecting the file and not starting to upload the file, click the submit button to verify that the data is legitimate, and then upload the file OnSubmit callback delegate, noting that the Pictureproperty type is IBrowserFile", - "UploadFormSettingsButtonText": "Submit", - "UploadClickUploadTitle": "Click upload", - "UploadClickUploadIntro": "The ButtonUpload components, classic styles, user click button to pop up the file selection box.", - "UploadClickUploadTips1": "Click on the browse button select file upload, in this case set IsMultiple-true multiple-selectable file can be uploaded", - "UploadClickUploadTips2": "When you set up IsSingle, you can upload only one image or file", - "UploadClickUploadTips3ShowUploadList": "Set ShowUploadFileList value to false as normal button", - "UploadedFilesTitle": "A list of files has been uploaded", - "UploadedFilesIntro": "Use DefaultFileList to set up uploaded content", - "UploadFolderTitle": "Upload a folder", - "UploadFolderIntro": "Use DefaultFileList to set up uploaded content", - "AvatarUploadTitle": "User profile picture upload", - "AvatarUploadIntro": "AvatarUpload component, using the OnChange to limit the format and size of images uploaded by users. In this example, only jpg/png/bmp/jpeg/gif five picture formats are allowed", - "AvatarUploadTips1": "Card form avatar box", - "AvatarUploadTips2": "Round avatar frame", + "AvatarUploadTitle": "Basic usage", + "AvatarUploadIntro": "AvatarUpload component, using the OnChange to limit the format and size of images uploaded by users. In this example, only jpg/png/bmp/jpeg/gif five picture formats are allowed. The component processes user uploaded avatars by setting the OnChange callback function. If this callback is not provided, the component will use the built-in method to try to read the uploaded file and generate the preview data in base64 format.", "AvatarUploadTips3": "When you set up IsSingle, you can upload only one image or file", - "AvatarUploadTips4": "
The component provides Accept property for upload file filtering, in this case the circular avatar box accepts both GIF and JPEG images, sets the Accept='image/gif, image/jpeg' and can be written as: Accept='image/*' if you don't restrict the format of the image. Whether this property is not secure or should be to file format validation using the server-side authentication
", "AvatarUploadTips5": "RELATED: [Accept] [Media Types]", "AvatarUploadTips6": "Set the preview address PrevUrl with the DefaultFileList property", "AvatarUploadTips7": "Verify that an example of using a picture box is used in the form", "AvatarUploadButtonText": "Submit", - "UploadPreCardStyleTitle": "Preview the card style", - "UploadPreCardStyleIntro": "CardUpload components and rendered in card-style band preview mode", - "UploadPreCardStyleSSR": "SSR mode ", - "UploadPreCardStyleServerSide": "Server Side mode, you can use the IWebHostEnvironment injection service to get to the wwwroot directory and save the file to the images/uploader, which does not require a direct call the controller secondary of MVCSaveToFile method", - "UploadPreCardStyleWasm": "Wasm mode ", - "UploadPreCardStyleWasmSide": "It wasn't available in wasm mode, IWebHostEnvironment needed to save files to the server side by calling webapi interface, and so on", - "UploadPreCardStyleLink": "Interested students can view their knowledge of Upload components through the wiki in the open source repository related resources of the [The portal]", - "UploadPreCardStyleValidation": "In this example, server-side verification prompts the file for too much prompt when the file size exceeds 5MB", - "UploadPreCardStyleTips1": "In this example, the ShowProgress=true display upload progress bar", - "UploadPreCardStyleTips2": "When you set up IsSingle, you can upload only one image or file", - "UploadFileIconTitle": "The file icon", - "UploadFileIconIntro": "Icons are displayed in different file formats", - "UploadFileIconTemplateTitle": "Custom file icon", - "UploadFileIconTemplateIntro": "By setting the IconTemplate parameter and using the FileIcon component, you can further customize the file icon [FileIcon example]", - "UploadBase64Title": "Base64 format", - "UploadBase64Intro": "use data:image/xxx;base64,xxx format data string as PrevUrl value", + "AvatarUploadValidateTitle": "ValidateForm", + "AvatarUploadValidateIntro": "Place it in ValidateForm to integrate automatic data validation function. For details, please refer to ValidateForm component. In this example, the uploaded file extension is limited to .png, .jpg, .jpeg. An error message will be displayed when uploading other formats. The file size limit is 5M. An error message will also be displayed when it exceeds", + "AvatarUploadAcceptTitle": "Accept", + "AvatarUploadAcceptIntro": "The component provides Accept property for upload file filtering, in this case the circular avatar box accepts both GIF and JPEG images, sets the Accept='image/gif, image/jpeg' and can be written as: Accept='image/*' if you don't restrict the format of the image. Whether this property is not secure or should be to file format validation using the server-side authentication", + "UploadsWidth": "The width of the preview box", + "UploadsHeight": "The height of the preview box", + "UploadsIsCircle": "Whether it is circular avatar mode", + "UploadsBorderRadius": "Border radius", + "UploadsValidateFormTitle": "ValidateForm", + "UploadsValidateFormValidContent": "Saved successfully", + "UploadsFormatError": "The file format is incorrect", + "UploadsAvatarMsg": "Avatar upload" + }, + "BootstrapBlazor.Server.Components.Samples.UploadInputs": { + "UploadsTitle": "InputUpload", + "UploadsSubTitle": "Select one or more files to upload by clicking the Browse button.", + "UploadsNote": "If you edit too much content, signalR communication interruption may be triggered. Please adjust the HubOptions configuration.", + "UploadNormalTitle": "Basic usage", + "UploadNormalIntro": "You can show the Delete button by setting ShowDeleteButton=\"true\"", + "UploadNormalLabelPhoto": "Select one or more files", + "UploadFormSettingsTitle": "ValidateForm", + "UploadFormSettingsIntro": "Use the file upload component to constrain the file format within the form", + "UploadFormSettingsLi1": "Using the ValidateForm form component, custom validation is set by setting the fileValidation label of the model properties to support file extensionsizevalidation, in this case with the extension .png .jpg .jpeg and the file size limit to 5M", + "UploadFormSettingsLi2": "After selecting the file and not starting to upload the file, click the submit button to verify that the data is legitimate, and then upload the file OnSubmit callback delegate, noting that the Pictureproperty type is IBrowserFile", + "UploadFormSettingsButtonText": "Submit", + "UploadsIsDirectory": "Whether to upload the entire directory", + "UploadsIsMultiple": "Whether to allow multiple file uploads", + "UploadsShowProgress": "Whether to display the upload progress", + "UploadsDefaultFileList": "The collection of files has been uploaded", "UploadsShowDeleteButton": "Whether to display the Delete button", - "UploadsShowDownloadButton": "Whether to display the Download button", "UploadsIsDisabled": "Whether to disable it", "UploadsPlaceHolder": "The place-in string", - "UploadsAccept": "Upload the received file format", "UploadsBrowserButtonClass": "Upload button style", "UploadsBrowserButtonIcon": "Browse the button icon", "UploadsBrowserButtonText": "The browse button displays text", @@ -3521,34 +3510,47 @@ "UploadsDeleteButtonIcon": "Remove the button icon", "UploadsDeleteButtonText": "Delete the button text", "UploadsDeleteButtonTextDefaultValue": "Delete", + "UploadsAccept": "Upload the received file format", "UploadsOnDelete": "Call back this method when you click the Delete button", - "UploadsOnChange": "Call back this method when you click the Browse button", - "UploadsOnDownload": "Call back this method when you click the Download button", - "UploadsIsDirectory": "Whether to upload the entire directory", - "UploadsIsMultiple": "Whether to allow multiple file uploads", - "UploadsIsSingle": "Whether to upload only once", - "UploadsShowProgress": "Whether to display the upload progress", - "UploadsDefaultFileList": "The collection of files has been uploaded", - "UploadsOnGetFileFormat": "Set the file format icon to call back the delegate", - "UploadsWidth": "The width of the preview box", - "UploadsHeight": "The height of the preview box", - "UploadsIsCircle": "Whether it is circular avatar mode", - "UploadsSuccess": "The upload was successful", - "UploadsError": "The simulated upload failed", - "UploadsFormatError": "The file format is incorrect", - "UploadsAvatarMsg": "Avatar upload", - "UploadsFileMsg": "Upload the file", - "UploadsFileError": "The file size is greater than 5MB", - "UploadsSaveFileError": "Failed to save the file", - "UploadFile": "Upload the file", - "UploadsWasmError": "Wasm mode does not implement saving code", - "UploadsSaveFile": "Save the file", - "UploadsSaveFileMsg": "The current mode is WebAssembly, call Webapi mode to save files to the server side or database", - "UploadsRemoveMsg": "The removal was successful", - "UploadsIconTemplate": "The template of file icon", - "DropUploadTitle": "Drop to upload", - "DropUploadIntro": "Drag and drop files into the specified area to upload", - "DropUploadFooterText": "file size less than 5Mb" + "UploadsOnChange": "Call back this method when you click the Browse button" + }, + "BootstrapBlazor.Server.Components.Samples.UploadButtons": { + "UploadsTitle": "ButtonUpload", + "UploadsSubTitle": "Upload files by clicking on them, usually used to upload file attachments", + "UploadsNote": "If you edit too much content, signalR communication interruption may be triggered. Please adjust the HubOptions configuration.", + "ButtonUploadTitle": "Basic usage", + "ButtonUploadIntro": "By setting ShowUploadFileList=\"true\" you can display the uploaded file list, and setting ShowDeleteButton=\"true\" you can display the Delete button" + }, + "BootstrapBlazor.Server.Components.Samples.UploadCards": { + "UploadsTitle": "CardUpload", + "UploadsSubTitle": "Click the button to pop up the file selection box to select one or more files, which will be presented in a card-style preview mode.", + "UploadsNote": "If you edit too much content, signalR communication interruption may be triggered. Please adjust the HubOptions configuration.", + "ButtonUploadTitle": "Basic usage", + "ButtonUploadIntro": "Use DefaultFileList to set up uploaded content", + "UploadPreCardStyleTitle": "Preview the card style", + "UploadPreCardStyleIntro": "CardUpload components and rendered in card-style band preview mode", + "UploadFileIconTitle": "The file icon", + "UploadFileIconIntro": "Icons are displayed in different file formats", + "UploadFileIconTemplateTitle": "Custom file icon", + "UploadFileIconTemplateIntro": "By setting the IconTemplate parameter and using the FileIcon component, you can further customize the file icon [FileIcon example]", + "UploadBase64Title": "Base64 format", + "UploadBase64Intro": "By setting the PrevUrl parameter value of the UploadFile instance, use the image content string in the data:image/xxx;base64,XXXXX format as the preview file path" + }, + "BootstrapBlazor.Server.Components.Samples.UploadDrops": { + "UploadsTitle": "CardDrop", + "UploadsSubTitle": "Upload one or more files by clicking on the component or by dragging or pasting", + "UploadsNote": "If the uploaded file is too large, it may trigger signalR communication interruption. Please adjust the HubOptions configuration yourself.", + "DropUploadTitle": "Basic usage", + "DropUploadIntro": "Handle all uploaded files via the OnChange callback", + "DropUploadFooterText": "File size should not exceed 5Mb", + "UploadsBodyTemplate": "Body Template", + "UploadsIconTemplate": "Icon Template", + "UploadsTextTemplate": "Text Template", + "UploadsUploadIcon": "Icon", + "UploadsUploadText": "Text", + "UploadsShowFooter": "Whether to display Footer", + "UploadsFooterTemplate": "Footer Text Template", + "UploadsFooterText": "Footer text" }, "BootstrapBlazor.Server.Components.Samples.ValidateForms": { "ChangeButtonText": "Change", @@ -4949,7 +4951,12 @@ "VideoDevice": "IVideoDevice", "AudioDevice": "IAudioDevice", "FullScreenButton": "FullScreenButton", - "Meet": "Meet" + "Meet": "Meet", + "InputUpload": "InputUpload", + "ButtonUpload": "ButtonUpload", + "AvatarUpload": "AvatarUpload", + "CardUpload": "CardUpload", + "DropUpload": "DropUpload" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "Header grouping function", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index a7742e06cc4..4411ee902bb 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -3460,60 +3460,49 @@ "RightHeaderTemplate": "右侧数据 Header 模板", "RightItemTemplate": "右侧数据项模板" }, - "BootstrapBlazor.Server.Components.Samples.Uploads": { - "UploadsTitle": "Upload 上传", - "UploadsSubTitle": "通过点击上传文件", + "BootstrapBlazor.Server.Components.Samples.UploadAvatars": { + "UploadsTitle": "AvatarUpload 头像上传组件", + "UploadsSubTitle": "通过点击上传文件,通常用作上传预览一个或者一组类似头像的图片", "UploadsNote": "如果上传文件过大,可能会触发 signalR 通讯中断问题,请自行调整 HubOptions 配置即可。", - "UploadNormalTitle": "基础用法", - "UploadNormalIntro": "InputUpload 组件与其他表单组件一起使用,显示文件名称,点击 浏览 按钮后选择文件并上传;通过设置 ShowRemoveButton 参数,显示 删除 按钮,点击删除按钮时回调 OnDelete 委托方法", - "UploadNormalLabelName": "姓名:", - "UploadNormalLabelAddress": "地址:", - "UploadNormalLabelPhoto": "照片:", - "UploadFormSettingsTitle": "表单应用", - "UploadFormSettingsIntro": "在表单内使用文件上传组件对文件格式进行约束", - "UploadFormSettingsLi1": "使用 ValidateForm 表单组件,通过设置模型属性的 FileValidation 标签设置自定义验证,支持文件 扩展名 大小 验证,本例中设置扩展名为 .png .jpg .jpeg,文件大小限制为 50K", - "UploadFormSettingsLi2": "选择文件后并未开始上传文件,点击 提交 按钮数据验证合法后,再 OnSubmit 回调委托中进行上传文件操作,注意 Picture 属性类型为 IBrowserFile", - "UploadFormSettingsButtonText": "提交", - "UploadClickUploadTitle": "点击上传", - "UploadClickUploadIntro": "ButtonUpload 组件,经典款式,用户点击按钮弹出文件选择框。", - "UploadClickUploadTips1": "点击 浏览按钮 选择文件上传,本例中设置 IsMultiple=true 可多选文件进行上传", - "UploadClickUploadTips2": "设置 IsSingle 时,仅可以上传一张图片或者文件", - "UploadClickUploadTips3ShowUploadList": "设置 ShowUploadFileList 值为 false 组件即与普通按钮一样,可自行处理上传文件逻辑", - "UploadedFilesTitle": "已上传文件列表", - "UploadedFilesIntro": "使用 DefaultFileList 设置已上传的内容", - "UploadFolderTitle": "上传文件夹", - "UploadFolderIntro": "使用 DefaultFileList 设置已上传的内容", - "AvatarUploadTitle": "用户头像上传", - "AvatarUploadIntro": "AvatarUpload 组件,使用 OnChange 限制用户上传的图片格式和大小。本例中仅允许上传 jpg/png/bmp/jpeg/gif 五种图片格式", - "AvatarUploadTips1": "卡片形式头像框", - "AvatarUploadTips2": "圆形头像框", + "AvatarUploadTitle": "基本用法", + "AvatarUploadIntro": "通过设置 IsMultiple 控制是否允许多文件上传,通过设置 IsCircle 控制是否为圆角,通过设置 BorderRadius 控制圆角曲率。组件通过设置 OnChange 回调函数处理用户上传头像,如果未提供此回调时,将使用内置方法尝试读取上传文件生成 base64 格式预览数据", "AvatarUploadTips3": "设置 IsSingle 时,仅可以上传一张图片或者文件", - "AvatarUploadTips4": "
组件提供了 Accept 属性用于设置上传文件过滤功能,本例中圆形头像框接受 GIF 和 JPEG 两种图像,设置 Accept='image/gif, image/jpeg',如果不限制图像的格式,可以写为:Accept='image/*',该属性并不安全还是应该是使用 服务器端验证 进行文件格式验证
", "AvatarUploadTips5": "相关文档:[Accept 属性详解] [Media Types 详细列表]", "AvatarUploadTips6": "通过 DefaultFileList 属性设置预览地址 PrevUrl 即可", "AvatarUploadTips7": "验证表单内使用头像框示例", "AvatarUploadButtonText": "提交", - "UploadPreCardStyleTitle": "预览卡片式", - "UploadPreCardStyleIntro": "CardUpload 组件,呈现为卡片式带预览模式", - "UploadPreCardStyleSSR": "SSR 模式", - "UploadPreCardStyleServerSide": "Server Side 模式中可以使用 IWebHostEnvironment 注入服务获取到 wwwwroot 目录,保存文件到 images\\uploader 中,此功能无需 MVC 的控制器辅助进行文件的保存,直接调用 SaveToFile 方法即可", - "UploadPreCardStyleWasm": "Wasm 模式", - "UploadPreCardStyleWasmSide": "wasm 模式中无法使用 IWebHostEnvironment 需要调用 webapi 接口等形式将文件保存到服务器端", - "UploadPreCardStyleLink": "有兴趣的同学可以通过开源仓库中的 wiki 文档中相关资源查看关于 Upload 组件的相关知识技巧 [传送门]", - "UploadPreCardStyleValidation": "本例中通过服务器端验证当文件大小超过 5MB 时,提示文件太大提示信息", - "UploadPreCardStyleTips1": "本例中设置 ShowProgress=true 显示上传进度条", - "UploadPreCardStyleTips2": "设置 IsSingle 时,仅可以上传一张图片或者文件", - "UploadFileIconTitle": "文件图标", - "UploadFileIconIntro": "不同文件格式显示的图标不同", - "UploadFileIconTemplateTitle": "自定义文件图标", - "UploadFileIconTemplateIntro": "通过设置 IconTemplate 参数,使用 FileIcon 组件可以对文件图标进行进一步自定义 [FileIcon 示例]", - "UploadBase64Title": "Base64 格式文件", - "UploadBase64Intro": "使用 data:image/xxx;base64,XXXXX 格式图片内容字符串作为预览文件路径", + "AvatarUploadValidateTitle": "ValidateForm", + "AvatarUploadValidateIntro": "放置到 ValidateForm 内集成自动数据验证功能,详情可以参考 ValidateForm 组件,本例中上传文件扩展名仅限制为 .png, .jpg, .jpeg,上传其他格式时会有错误提示,文件大小限制为 5M 超过时也会有错误提示显示", + "AvatarUploadAcceptTitle": "Accept", + "AvatarUploadAcceptIntro": "组件提供了 Accept 属性用于设置上传文件过滤功能,本例中圆形头像框接受 GIF 和 JPEG 两种图像,设置 Accept='image/gif, image/jpeg',如果不限制图像的格式,可以写为:Accept='image/*',该属性并不安全还是应该是使用 服务器端验证 进行文件格式验证", + "UploadsWidth": "预览框宽度", + "UploadsHeight": "预览框高度", + "UploadsIsCircle": "是否为圆形头像模式", + "UploadsBorderRadius": "预览框圆角曲率", + "UploadsValidateFormTitle": "表单应用", + "UploadsValidateFormValidContent": "数据合规,保存成功", + "UploadsFormatError": "文件格式不正确", + "UploadsAvatarMsg": "头像上传" + }, + "BootstrapBlazor.Server.Components.Samples.UploadInputs": { + "UploadsTitle": "InputUpload 上传组件", + "UploadsSubTitle": "通过点击浏览按钮弹出选择文件框选择一个或者多个文件进行上传", + "UploadsNote": "如果上传文件过大,可能会触发 signalR 通讯中断问题,请自行调整 HubOptions 配置即可。", + "UploadNormalTitle": "基础用法", + "UploadNormalIntro": "可以通过设置 ShowDeleteButton=\"true\" 显示 删除 按钮", + "UploadNormalLabelPhoto": "选择一个或者多个文件", + "UploadFormSettingsTitle": "表单应用", + "UploadFormSettingsIntro": "放置到 ValidateForm 内集成自动数据验证功能,详情可以参考 ValidateForm 组件", + "UploadFormSettingsLi1": "使用 ValidateForm 表单组件,通过设置模型属性的 FileValidation 标签设置自定义验证,支持文件 扩展名 大小 验证,本例中设置扩展名为 .png .jpg .jpeg,文件大小限制为 5M", + "UploadFormSettingsLi2": "选择文件后并未开始上传文件,点击 提交 按钮数据验证合法后,再 OnSubmit 回调委托中进行上传文件操作,注意 Picture 属性类型为 IBrowserFile", + "UploadFormSettingsButtonText": "提交", + "UploadsIsDirectory": "是否上传整个目录", + "UploadsIsMultiple": "是否允许多文件上传", + "UploadsShowProgress": "是否显示上传进度", + "UploadsDefaultFileList": "已上传文件集合", "UploadsShowDeleteButton": "是否显示删除按钮", - "UploadsShowDownloadButton": "是否显示下载按钮", "UploadsIsDisabled": "是否禁用", "UploadsPlaceHolder": "占位字符串", - "UploadsAccept": "上传接收的文件格式", "UploadsBrowserButtonClass": "上传按钮样式", "UploadsBrowserButtonIcon": "浏览按钮图标", "UploadsBrowserButtonText": "浏览按钮显示文字", @@ -3521,34 +3510,47 @@ "UploadsDeleteButtonIcon": "删除按钮图标", "UploadsDeleteButtonText": "删除按钮文字", "UploadsDeleteButtonTextDefaultValue": "删除", + "UploadsAccept": "上传接收的文件格式", "UploadsOnDelete": "点击删除按钮时回调此方法", - "UploadsOnChange": "点击浏览按钮时回调此方法", - "UploadsOnDownload": "点击下载按钮时回调此方法", - "UploadsIsDirectory": "是否上传整个目录", - "UploadsIsMultiple": "是否允许多文件上传", - "UploadsIsSingle": "是否仅上传一次", - "UploadsShowProgress": "是否显示上传进度", - "UploadsDefaultFileList": "已上传文件集合", - "UploadsOnGetFileFormat": "设置文件格式图标回调委托", - "UploadsWidth": "预览框宽度", - "UploadsHeight": "预览框高度", - "UploadsIsCircle": "是否为圆形头像模式", - "UploadsSuccess": "上传成功", - "UploadsError": "模拟上传失败", - "UploadsFormatError": "文件格式不正确", - "UploadsAvatarMsg": "头像上传", - "UploadsFileMsg": "上传文件", - "UploadsFileError": "文件大小超过 5MB", - "UploadsSaveFileError": "保存文件失败", - "UploadFile": "上传文件", - "UploadsWasmError": "Wasm 模式未实现保存代码", - "UploadsSaveFile": "保存文件", - "UploadsSaveFileMsg": "当前模式为 WebAssembly 模式,请调用 Webapi 模式保存文件到服务器端或数据库中", - "UploadsRemoveMsg": "成功移除", - "UploadsIconTemplate": "文件图标模板", - "DropUploadTitle": "拖拽上传", - "DropUploadIntro": "将文件拖拽到特定区域以进行上传", - "DropUploadFooterText": "文件大小不超过 5Mb" + "UploadsOnChange": "点击浏览按钮时回调此方法" + }, + "BootstrapBlazor.Server.Components.Samples.UploadButtons": { + "UploadsTitle": "ButtonUpload 按钮上传组件", + "UploadsSubTitle": "通过点击按钮弹出选择文件框选择一个或者多个文件,通常用作上传文件附件", + "UploadsNote": "如果上传文件过大,可能会触发 signalR 通讯中断问题,请自行调整 HubOptions 配置即可。", + "ButtonUploadTitle": "基础用法", + "ButtonUploadIntro": "通过设置 ShowUploadFileList=\"true\" 可以显示上传文件列表,设置 ShowDeleteButton=\"true\" 显示 删除 按钮" + }, + "BootstrapBlazor.Server.Components.Samples.UploadCards": { + "UploadsTitle": "CardUpload 卡片上传组件", + "UploadsSubTitle": "通过点击按钮弹出选择文件框选择一个或者多个文件,呈现为卡片式带预览模式", + "UploadsNote": "如果上传文件过大,可能会触发 signalR 通讯中断问题,请自行调整 HubOptions 配置即可。", + "ButtonUploadTitle": "基础用法", + "ButtonUploadIntro": "使用 DefaultFileList 设置已上传的内容", + "UploadPreCardStyleTitle": "预览卡片式", + "UploadPreCardStyleIntro": "CardUpload 组件,呈现为卡片式带预览模式", + "UploadFileIconTitle": "文件图标", + "UploadFileIconIntro": "不同文件格式显示的图标不同", + "UploadFileIconTemplateTitle": "自定义文件图标", + "UploadFileIconTemplateIntro": "通过设置 IconTemplate 参数,使用 FileIcon 组件可以对文件图标进行进一步自定义 [FileIcon 示例]", + "UploadBase64Title": "Base64 格式文件", + "UploadBase64Intro": "通过设置 UploadFile 实例的 PrevUrl 参数值使用 data:image/xxx;base64,XXXXX 格式图片内容字符串作为预览文件路径" + }, + "BootstrapBlazor.Server.Components.Samples.UploadDrops": { + "UploadsTitle": "CardDrop 拖拽上传组件", + "UploadsSubTitle": "通过点击组件或者拖拽或者粘贴上传一个或者多个文件", + "UploadsNote": "如果上传文件过大,可能会触发 signalR 通讯中断问题,请自行调整 HubOptions 配置即可。", + "DropUploadTitle": "基础用法", + "DropUploadIntro": "通过 OnChange 回调处理所有上传文件", + "DropUploadFooterText": "文件大小不超过 5Mb", + "UploadsBodyTemplate": "Body 模板", + "UploadsIconTemplate": "图标模板", + "UploadsTextTemplate": "文字模板", + "UploadsUploadIcon": "图标", + "UploadsUploadText": "上传文字", + "UploadsShowFooter": "是否显示 Footer", + "UploadsFooterTemplate": "Footer 字符串模板", + "UploadsFooterText": "Footer 字符串信息" }, "BootstrapBlazor.Server.Components.Samples.ValidateForms": { "ChangeButtonText": "更改组件", @@ -4949,7 +4951,12 @@ "VideoDevice": "视频设备服务 IVideoDevice", "AudioDevice": "音频设备服务 IAudioDevice", "FullScreenButton": "全屏按钮 FullScreenButton", - "Meet": "视频会议组件 Meet" + "Meet": "视频会议组件 Meet", + "InputUpload": "上传组件 InputUpload", + "ButtonUpload": "按钮上传组件 ButtonUpload", + "AvatarUpload": "头像上传组件 AvatarUpload", + "CardUpload": "卡片上传组件 CardUpload", + "DropUpload": "拖动上传组件 DropUpload" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "表头分组功能", diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index 5b95caa0a72..624bc52b36b 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -179,7 +179,11 @@ "transfer": "Transfers", "transition": "Transitions", "tree-view": "TreeViews", - "upload": "Uploads", + "upload-input": "UploadInputs", + "upload-button": "UploadButtons", + "upload-card": "UploadCards", + "upload-avatar": "UploadAvatars", + "upload-drop": "UploadDrops", "validate-form": "ValidateForms", "video-player": "VideoPlayers", "table": "Table\\Tables", diff --git a/src/BootstrapBlazor/Attributes/FileValidationAttribute.cs b/src/BootstrapBlazor/Attributes/FileValidationAttribute.cs index acfed7bec48..3e3d601027c 100644 --- a/src/BootstrapBlazor/Attributes/FileValidationAttribute.cs +++ b/src/BootstrapBlazor/Attributes/FileValidationAttribute.cs @@ -56,10 +56,5 @@ public class FileValidationAttribute : ValidationAttribute return ret; } - private static IEnumerable? GetMemberNames(ValidationContext validationContext) - { - return validationContext == null ? [] : GetMemberNames(); - - IEnumerable GetMemberNames() => string.IsNullOrEmpty(validationContext.MemberName) ? [] : [validationContext.MemberName]; - } + private static IEnumerable? GetMemberNames(ValidationContext validationContext) => validationContext == null || string.IsNullOrEmpty(validationContext.MemberName) ? [] : [validationContext.MemberName]; } diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 3496eb35f13..8b68a932561 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.7.1-beta03 + 9.7.1-beta04 diff --git a/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor b/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor index 7ca69bc97a5..f803d0d2739 100644 --- a/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor +++ b/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor @@ -1,6 +1,6 @@ @namespace BootstrapBlazor.Components @typeparam TValue -@inherits SingleUploadBase +@inherits UploadBase @if (IsShowLabel) { @@ -8,28 +8,25 @@ }
- @if (IsUploadButtonAtFirst && CheckCanUpload()) + @if (IsUploadButtonAtFirst && ShowAddButton()) { @RenderAdd } - @foreach (var item in GetUploadFiles()) + @foreach (var item in Files) { -
+
- @if (!IsDisabled) - { -
- - +
+ + + + @if (GetShowProgress(item)) + { + + - @if (GetShowProgress(item)) - { - - - - } -
- } + } +
@if (!IsCircle) { @@ -39,7 +36,7 @@ }
} - @if (!IsUploadButtonAtFirst && CheckCanUpload()) + @if (!IsUploadButtonAtFirst && ShowAddButton()) { @RenderAdd } @@ -47,11 +44,10 @@
- @code { RenderFragment RenderAdd => - @
-
+ @
+
diff --git a/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs index ee2ca42c940..6c45f79c416 100644 --- a/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs @@ -3,54 +3,14 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using Microsoft.AspNetCore.Components.Forms; - namespace BootstrapBlazor.Components; /// /// 头像上传组件 +/// AvatarUpload Component /// public partial class AvatarUpload { - /// - /// - /// - /// - /// - protected new string? GetItemClassString(UploadFile item) => CssBuilder.Default(ItemClassString) - .AddClass("is-valid", !IsDisabled && item.IsValid.HasValue && item.IsValid.Value) - .AddClass("is-invalid", !IsDisabled && item.IsValid.HasValue && !item.IsValid.Value) - .AddClass("is-valid", !IsDisabled && !item.IsValid.HasValue && item.Uploaded && item.Code == 0) - .AddClass("is-invalid", !IsDisabled && !item.IsValid.HasValue && item.Code != 0) - .AddClass("disabled", IsDisabled) - .Build(); - - /// - /// - /// - protected override string? ItemClassString => CssBuilder.Default(base.ItemClassString) - .AddClass("is-circle", IsCircle) - .AddClass("is-single", IsSingle) - .AddClass("disabled", IsDisabled) - .Build(); - - /// - /// 获得/设置 预览框 Style 属性 - /// - private string? PrevStyleString => CssBuilder.Default() - .AddClass($"width: {Width}px;", Width > 0) - .AddClass($"height: {Height}px;", Height > 0 && !IsCircle) - .AddClass($"height: {Width}px;", IsCircle) - .Build(); - - private string? ValidStatusIconString => CssBuilder.Default("valid-icon valid") - .AddClass(ValidStatusIcon) - .Build(); - - private string? InvalidStatusIconString => CssBuilder.Default("valid-icon invalid") - .AddClass(InvalidStatusIcon) - .Build(); - /// /// 获得/设置 文件预览框宽度 /// @@ -69,6 +29,18 @@ public partial class AvatarUpload [Parameter] public bool IsCircle { get; set; } + /// + /// Gets or sets the border radius. Default is null. + /// + [Parameter] + public string? BorderRadius { get; set; } + + /// + /// 获得/设置 图标文件扩展名集合 ".png" + /// + [Parameter] + public List? AllowExtensions { get; set; } + /// /// 获得/设置 删除图标 /// @@ -109,6 +81,37 @@ public partial class AvatarUpload [NotNull] private IIconTheme? IconTheme { get; set; } + private string? ClassString => CssBuilder.Default("upload") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + private string? GetItemClassString() => CssBuilder.Default("upload-item") + .AddClass("is-circle", IsCircle) + .AddClass("disabled", IsDisabled) + .Build(); + + /// + /// 获得/设置 预览框 Style 属性 + /// + private string? ItemStyleString => CssBuilder.Default() + .AddClass($"width: {Width}px;", Width > 0) + .AddClass($"height: {Height}px;", Height > 0 && !IsCircle) + .AddClass($"height: {Width}px;", IsCircle) + .AddClass($"--bb-upload-item-border-radius: {BorderRadius};", IsCircle && !string.IsNullOrEmpty(BorderRadius)) + .Build(); + + private string? ActionClassString => CssBuilder.Default("upload-item-actions") + .AddClass("btn-browser", IsDisabled == false) + .Build(); + + private string? ValidStatusIconString => CssBuilder.Default("valid-icon valid") + .AddClass(ValidStatusIcon) + .Build(); + + private string? InvalidStatusIconString => CssBuilder.Default("valid-icon invalid") + .AddClass(InvalidStatusIcon) + .Build(); + /// /// /// @@ -126,46 +129,92 @@ protected override void OnParametersSet() /// /// /// - /// + /// /// - protected override async Task OnFileChange(InputFileChangeEventArgs args) + protected override async Task TriggerOnChanged(UploadFile file) { - CurrentFile = new UploadFile() + if (OnChange == null) { - OriginFileName = args.File.Name, - Size = args.File.Size, - File = args.File, - Uploaded = false - }; - CurrentFile.ValidateId = $"{Id}_{CurrentFile.GetHashCode()}"; - - if (IsSingle) + await file.RequestBase64ImageFileAsync(allowExtensions: AllowExtensions); + } + else { - // 单图片模式 - DefaultFileList?.Clear(); - UploadFiles.Clear(); + await OnChange(file); } + } - UploadFiles.Add(CurrentFile); + private IReadOnlyCollection _results = []; - await base.OnFileChange(args); + /// + /// + /// + /// + public override async Task ToggleMessage(IReadOnlyCollection results) + { + _results = results; + IsValid = results.Count == 0; - // ValidateFile 后 IsValid 才有值 - CurrentFile.IsValid = IsValid; + ValidateModule ??= await LoadValidateModule(); - if (OnChange != null) - { - await OnChange(CurrentFile); - } - else + var invalidItems = IsInValiadOnAddItem + ? [new { Id = AddId, _results.First().ErrorMessage }] + : _results.Select(i => new { Id = i.MemberNames.FirstOrDefault(), i.ErrorMessage }).ToList(); + + var items = IsInValiadOnAddItem + ? [AddId] + : Files.Select(i => i.ValidateId).ToList(); + + var addId = IsInValiadOnAddItem ? null : AddId; + await ValidateModule.InvokeVoidAsync("executeUpload", items, invalidItems, addId); + } + + private bool IsInValiadOnAddItem => Files.Count == 0 && _results.Count > 0; + + /// + /// + /// + /// + protected override ValueTask ShowValidResult() => ValueTask.CompletedTask; + + /// + /// + /// + /// + /// + protected override async ValueTask RemoveValidResult(string? validateId = null) + { + if (!string.IsNullOrEmpty(validateId)) { - await CurrentFile.RequestBase64ImageFileAsync(CurrentFile.File.ContentType, 320, 240); + await base.RemoveValidResult(validateId); } } + private string? AddId => $"{Id}_new"; + /// - /// 获得 弹窗客户端 ID + /// /// + /// /// - protected override string? RetrieveId() => CurrentFile?.ValidateId; + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + if (ValidateForm != null && FieldIdentifier.HasValue) + { + ValidateForm.TryRemoveValidator((FieldIdentifier.Value.FieldName, FieldIdentifier.Value.Model.GetType()), out _); + } + + if (ValidateModule != null) + { + var items = IsInValiadOnAddItem + ? [AddId] + : Files.Select(i => i.ValidateId).ToList(); + + await ValidateModule.InvokeVoidAsync("disposeUpload", items); + } + } + + await base.DisposeAsync(false); + } } diff --git a/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor b/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor index 601c34b1f2d..66815687591 100644 --- a/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor +++ b/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor @@ -1,60 +1,23 @@ @namespace BootstrapBlazor.Components @typeparam TValue -@inherits ButtonUploadBase +@inherits FileListUploadBase @if (IsShowLabel) { }
- @if (ShowUploadFileList) { -
- @foreach (var item in GetUploadFiles()) - { -
- -
- @item.GetFileName() - (@item.Size.ToFileSizeString()) -
- @if (GetShowProgress(item)) - { - - - - } - else - { -
- @if (ShowDownloadButton) - { - - } - @if (item.Code == 0) - { - - @if (!IsDisabled) - { - - } - } - else - { - @if (!IsDisabled) - { - - - } - } -
- } -
- } -
+ + }
diff --git a/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor.cs index 6e5ea7e1610..701efe23ff3 100644 --- a/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/ButtonUpload.razor.cs @@ -9,51 +9,16 @@ namespace BootstrapBlazor.Components; /// /// 按钮上传组件 +/// ButtonUpload Component /// public partial class ButtonUpload { - private bool IsUploadButtonDisabled => IsDisabled || (IsSingle && UploadFiles.Any()); - - private string? BrowserButtonClassString => CssBuilder.Default("btn-browser") - .AddClass(BrowserButtonClass, !string.IsNullOrEmpty(BrowserButtonClass)) - .Build(); - - private string? LoadingIconString => CssBuilder.Default("loading-icon") - .AddClass(LoadingIcon) - .Build(); - - private string? DeleteIconString => CssBuilder.Default("delete-icon") - .AddClass(DeleteIcon) - .Build(); - - private string? ValidStatusIconString => CssBuilder.Default("valid-icon") - .AddClass(ValidStatusIcon) - .Build(); - - private string? InvalidStatusIconString => CssBuilder.Default("invalid-icon") - .AddClass(InvalidStatusIcon) - .Build(); - - private string? DownloadIconString => CssBuilder.Default("download-icon") - .AddClass(DownloadIcon) - .Build(); - - private string? CancelIconString => CssBuilder.Default("cancel-icon") - .AddClass(CancelIcon) - .Build(); - /// - /// 获得/设置 浏览按钮图标 + /// 获得/设置 浏览按钮加载中图标 /// [Parameter] public string? LoadingIcon { get; set; } - /// - /// 获得/设置 下载按钮图标 - /// - [Parameter] - public string? DownloadIcon { get; set; } - /// /// 获得/设置 上传失败状态图标 /// @@ -66,12 +31,6 @@ public partial class ButtonUpload [Parameter] public string? ValidStatusIcon { get; set; } - /// - /// 获得/设置 删除按钮图标 - /// - [Parameter] - public string? DeleteIcon { get; set; } - /// /// 获得/设置 浏览按钮图标 /// @@ -101,7 +60,6 @@ public partial class ButtonUpload /// 获得/设置 浏览按钮颜色 /// [Parameter] - [NotNull] public Color BrowserButtonColor { get; set; } = Color.Primary; /// @@ -116,16 +74,26 @@ public partial class ButtonUpload [Parameter] public RenderFragment? ChildContent { get; set; } + /// + /// 获得/设置 设置文件格式图标回调委托 + /// + [Parameter] + public Func? OnGetFileFormat { get; set; } + [Inject] [NotNull] private IStringLocalizer>? Localizer { get; set; } - [Inject] - [NotNull] - private IIconTheme? IconTheme { get; set; } + private string? ClassString => CssBuilder.Default("upload") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + private string? BrowserButtonClassString => CssBuilder.Default("btn-browser") + .AddClass(BrowserButtonClass, !string.IsNullOrEmpty(BrowserButtonClass)) + .Build(); /// - /// OnParametersSet 方法 + /// /// protected override void OnParametersSet() { @@ -133,10 +101,5 @@ protected override void OnParametersSet() BrowserButtonText ??= Localizer[nameof(BrowserButtonText)]; BrowserButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.ButtonUploadBrowserButtonIcon); - LoadingIcon ??= IconTheme.GetIconByKey(ComponentIcons.ButtonUploadLoadingIcon); - InvalidStatusIcon ??= IconTheme.GetIconByKey(ComponentIcons.ButtonUploadInvalidStatusIcon); - ValidStatusIcon ??= IconTheme.GetIconByKey(ComponentIcons.ButtonUploadValidStatusIcon); - DownloadIcon ??= IconTheme.GetIconByKey(ComponentIcons.ButtonUploadDownloadIcon); - DeleteIcon ??= IconTheme.GetIconByKey(ComponentIcons.ButtonUploadDeleteIcon); } } diff --git a/src/BootstrapBlazor/Components/Upload/CardUpload.razor b/src/BootstrapBlazor/Components/Upload/CardUpload.razor index 9a02c28b95f..3f0cbef8246 100644 --- a/src/BootstrapBlazor/Components/Upload/CardUpload.razor +++ b/src/BootstrapBlazor/Components/Upload/CardUpload.razor @@ -1,6 +1,6 @@ @namespace BootstrapBlazor.Components @typeparam TValue -@inherits ButtonUploadBase +@inherits FileListUploadBase @if (IsShowLabel) { @@ -8,15 +8,15 @@ }
- @if (IsUploadButtonAtFirst && CheckCanUpload()) + @if (IsUploadButtonAtFirst && ShowAddButton()) { @RenderAdd } - @foreach (var item in GetUploadFiles()) + @foreach (var item in Files) {
- @if (IsImage(item)) + @if (item.IsImage(AllowExtensions, CanPreviewCallback)) { prevUrl } @@ -51,7 +51,7 @@ }
- @if (ShowDeletedButton) + @if (ShowDeleteButton) { @if (GetShowProgress(item)) { - + } @@ -70,29 +70,25 @@
} - @if (!IsUploadButtonAtFirst && CheckCanUpload()) + @if (!IsUploadButtonAtFirst && ShowAddButton()) { @RenderAdd }
- - @if (ShowPreviewList) { } +
@code { RenderFragment RenderAdd => @
-
- @if (!IsDisabled) - { +
- }
; } diff --git a/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs index 90b847dab8d..989082efb76 100644 --- a/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs @@ -6,19 +6,31 @@ namespace BootstrapBlazor.Components; /// -/// 卡片式上传组件 +/// CardUpload component /// public partial class CardUpload { + private string? ClassString => CssBuilder.Default("upload") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); + + private string? GetItemClassString(UploadFile item) => CssBuilder.Default(ItemClassString) + .AddClass("is-valid", item is { Uploaded: true, Code: 0 }) + .AddClass("is-invalid", item.Code != 0) + .Build(); + private string? ItemClassString => CssBuilder.Default("upload-item") + .AddClass("disabled", CanUpload() == false) + .Build(); + private string? BodyClassString => CssBuilder.Default("upload-body is-card") - .AddClass("is-single", IsSingle) + .AddClass("is-single", IsMultiple == false) .Build(); - private string? GetDisabledString(UploadFile item) => (!IsDisabled && item.Uploaded && item.Code == 0) ? null : "disabled"; + private string? GetDisabledString(UploadFile item) => (!IsDisabled && item is { Uploaded: true, Code: 0 }) ? null : "disabled"; - private bool ShowPreviewList => GetUploadFiles().Count != 0; + private bool ShowPreviewList => Files.Count != 0; - private List PreviewList => GetUploadFiles().Select(i => i.PrevUrl).ToList(); + private List PreviewList => [.. Files.Select(i => i.PrevUrl)]; private string? GetDeleteButtonDisabledString(UploadFile item) => (!IsDisabled && item.Uploaded) ? null : "disabled"; @@ -60,24 +72,12 @@ public partial class CardUpload [Parameter] public string? StatusIcon { get; set; } - /// - /// 获得/设置 删除图标 - /// - [Parameter] - public string? DeleteIcon { get; set; } - /// /// 获得/设置 移除图标 /// [Parameter] public string? RemoveIcon { get; set; } - /// - /// 获得/设置 下载图标 - /// - [Parameter] - public string? DownloadIcon { get; set; } - /// /// 获得/设置 放大图标 /// @@ -94,6 +94,8 @@ public partial class CardUpload /// 获得/设置 是否显示删除按钮 默认 true 显示 ///
[Parameter] + [Obsolete("已弃用,请使用 ShowDeleteButton 参数。Deprecated, please use the ShowDeleteButton parameter")] + [ExcludeFromCodeCoverage] public bool ShowDeletedButton { get; set; } = true; /// @@ -102,9 +104,21 @@ public partial class CardUpload [Parameter] public bool IsUploadButtonAtFirst { get; set; } - [Inject] - [NotNull] - private IIconTheme? IconTheme { get; set; } + /// + /// 获得/设置 点击 Zoom 图标回调方法 + /// + [Parameter] + public Func? OnZoomAsync { get; set; } + + /// + /// 获得/设置 图标文件扩展名集合 ".png" + /// + [Parameter] + public List? AllowExtensions { get; set; } + + private string? ActionClassString => CssBuilder.Default("upload-item-actions") + .AddClass("btn-browser", IsDisabled == false) + .Build(); /// /// @@ -115,44 +129,10 @@ protected override void OnParametersSet() AddIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadAddIcon); StatusIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadStatusIcon); - DeleteIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadDeleteIcon); - RemoveIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadRemoveIcon); - DownloadIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadDownloadIcon); ZoomIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadZoomIcon); + RemoveIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadRemoveIcon); } - private bool IsImage(UploadFile item) - { - bool ret; - if (item.File != null) - { - ret = item.File.ContentType.Contains("image", StringComparison.OrdinalIgnoreCase) || CheckExtensions(item.File.Name); - } - else if (CanPreviewCallback != null) - { - ret = CanPreviewCallback(item); - } - else - { - ret = IsBase64Format() || CheckExtensions(item.FileName ?? item.PrevUrl ?? ""); - } - - bool IsBase64Format() => !string.IsNullOrEmpty(item.PrevUrl) && item.PrevUrl.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase); - - bool CheckExtensions(string fileName) => Path.GetExtension(fileName).ToLowerInvariant() switch - { - ".jpg" or ".jpeg" or ".png" or ".bmp" or ".gif" or ".webp" => true, - _ => false - }; - return ret; - } - - /// - /// 获得/设置 点击 Zoom 图标回调方法 - /// - [Parameter] - public Func? OnZoomAsync { get; set; } - private async Task OnCardFileDelete(UploadFile item) { await OnFileDelete(item); @@ -166,4 +146,20 @@ private async Task OnClickZoom(UploadFile item) await OnZoomAsync(item); } } + + private async Task OnClickDownload(UploadFile item) + { + if (OnDownload != null) + { + await OnDownload(item); + } + } + + private async Task OnClickCancel(UploadFile item) + { + if (OnCancel != null) + { + await OnCancel(item); + } + } } diff --git a/src/BootstrapBlazor/Components/Upload/DropUpload.razor b/src/BootstrapBlazor/Components/Upload/DropUpload.razor index e78bb0ee8f3..166cd4d6788 100644 --- a/src/BootstrapBlazor/Components/Upload/DropUpload.razor +++ b/src/BootstrapBlazor/Components/Upload/DropUpload.razor @@ -1,12 +1,12 @@ @namespace BootstrapBlazor.Components -@inherits SingleUploadBase +@inherits FileListUploadBase @if (IsShowLabel) { } -
-
+
+
@if (BodyTemplate != null) { @BodyTemplate @@ -23,7 +23,7 @@ }
-
+
@if (TextTemplate != null) { @TextTemplate @@ -33,35 +33,29 @@ @(new MarkupString(UploadText)) }
+ @if (ShowFooter) + { + + } }
- @if (ShowFooter) + @if (ShowUploadFileList) { - + + } -
    - @foreach (var item in GetUploadFiles()) - { - @if (GetShowProgress(item)) - { -
  • -
    - @item.GetFileName() - (@item.Size.ToFileSizeString()) -
    - -
  • - } - } -
diff --git a/src/BootstrapBlazor/Components/Upload/DropUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/DropUpload.razor.cs index f508f531d5b..90ce8b9c0dc 100644 --- a/src/BootstrapBlazor/Components/Upload/DropUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/DropUpload.razor.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Localization; namespace BootstrapBlazor.Components; @@ -64,19 +63,53 @@ public partial class DropUpload [NotNull] public string? FooterText { get; set; } - [Inject] - [NotNull] - private IIconTheme? IconTheme { get; set; } + /// + /// 获得/设置 是否显示上传列表 默认 true + /// + [Parameter] + public bool ShowUploadFileList { get; set; } = true; + + /// + /// 获得/设置 设置文件格式图标回调委托 + /// + [Parameter] + public Func? OnGetFileFormat { get; set; } + + /// + /// 获得/设置 加载中图标 + /// + [Parameter] + public string? LoadingIcon { get; set; } + + /// + /// 获得/设置 上传失败状态图标 + /// + [Parameter] + public string? InvalidStatusIcon { get; set; } + + /// + /// 获得/设置 上传成功状态图标 + /// + [Parameter] + public string? ValidStatusIcon { get; set; } [Inject] [NotNull] private IStringLocalizer>? Localizer { get; set; } - private string? DropUploadClassString => CssBuilder.Default(ClassString) - .AddClass("is-drop") + private string? ClassString => CssBuilder.Default("upload is-drop") + .AddClass("disabled", CheckStatus()) .AddClassFromAttributes(AdditionalAttributes) .Build(); + private string? BodyClassString => CssBuilder.Default("upload-drop-body") + .AddClass("btn-browser", CheckStatus() == false) + .Build(); + + private string? TextClassString => CssBuilder.Default("upload-drop-text") + .AddClass("text-muted", CheckStatus()) + .Build(); + /// /// /// @@ -86,29 +119,5 @@ protected override void OnParametersSet() UploadIcon ??= IconTheme.GetIconByKey(ComponentIcons.DropUploadIcon); UploadText ??= Localizer["DropUploadText"]; - FooterText ??= Localizer["DropFooterText"]; - } - - /// - /// - /// - /// - /// - protected override async Task OnFileChange(InputFileChangeEventArgs args) - { - var file = new UploadFile() - { - OriginFileName = args.File.Name, - Size = args.File.Size, - File = args.File, - Uploaded = false, - UpdateCallback = Update - }; - UploadFiles.Add(file); - if (OnChange != null) - { - await OnChange(file); - } - file.Uploaded = true; } } diff --git a/src/BootstrapBlazor/Components/Upload/FileListUploadBase.cs b/src/BootstrapBlazor/Components/Upload/FileListUploadBase.cs new file mode 100644 index 00000000000..8a29dda16c0 --- /dev/null +++ b/src/BootstrapBlazor/Components/Upload/FileListUploadBase.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// PreviewListUploadBase 基类 +/// +/// +public class FileListUploadBase : UploadBase +{ + /// + /// 获得/设置 是否显示删除按钮 默认 false + /// + [Parameter] + public bool ShowDeleteButton { get; set; } + + /// + /// 获得/设置 删除按钮图标 + /// + [Parameter] + public string? DeleteIcon { get; set; } + + /// + /// 获得/设置 是否显示下载按钮 默认 false + /// + [Parameter] + public bool ShowDownloadButton { get; set; } + + /// + /// 获得/设置 下载按钮图标 + /// + [Parameter] + public string? DownloadIcon { get; set; } + + /// + /// 获得/设置 点击下载按钮回调方法 默认 null + /// + [Parameter] + public Func? OnDownload { get; set; } + + /// + /// 获得/设置 取消图标 + /// + [Parameter] + public string? CancelIcon { get; set; } + + /// + /// 获得/设置 点击取消按钮回调此方法 默认 null + /// + [Parameter] + public Func? OnCancel { get; set; } + + /// + /// 服务实例 + /// + [Inject] + [NotNull] + protected IIconTheme? IconTheme { get; set; } + + /// + /// + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + + DeleteIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadDeleteIcon); + DownloadIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadDownloadIcon); + CancelIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadCancelIcon); + } +} diff --git a/src/BootstrapBlazor/Components/Upload/InputUpload.razor b/src/BootstrapBlazor/Components/Upload/InputUpload.razor index 4d408f48589..3697b11c73e 100644 --- a/src/BootstrapBlazor/Components/Upload/InputUpload.razor +++ b/src/BootstrapBlazor/Components/Upload/InputUpload.razor @@ -8,12 +8,16 @@ }
- + @if (ShowDeleteButton) { - } -
- +
diff --git a/src/BootstrapBlazor/Components/Upload/InputUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/InputUpload.razor.cs index cf1df406861..9a3f43de1a1 100644 --- a/src/BootstrapBlazor/Components/Upload/InputUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/InputUpload.razor.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Localization; namespace BootstrapBlazor.Components; @@ -13,22 +12,6 @@ namespace BootstrapBlazor.Components; ///
public partial class InputUpload { - private string? InputValueClassString => CssBuilder.Default("form-control") - .AddClass(CssClass).AddClass(ValidCss) - .Build(); - - private string? RemoveButtonClassString => CssBuilder.Default() - .AddClass(DeleteButtonClass) - .Build(); - - private bool IsDeleteButtonDisabled => IsDisabled || CurrentFile == null; - - private string? BrowserButtonClassString => CssBuilder.Default("btn-browser") - .AddClass(BrowserButtonClass) - .Build(); - - private string? GetFileName() => CurrentFile?.GetFileName() ?? Value?.ToString(); - /// /// 获得/设置 浏览按钮图标 /// @@ -87,16 +70,23 @@ public partial class InputUpload [NotNull] private IIconTheme? IconTheme { get; set; } - /// - /// - /// - protected override void OnInitialized() - { - base.OnInitialized(); + private string? InputValueClassString => CssBuilder.Default("form-control") + .AddClass(CssClass).AddClass(ValidCss) + .Build(); - DeleteButtonText ??= Localizer[nameof(DeleteButtonText)]; - BrowserButtonText ??= Localizer[nameof(BrowserButtonText)]; - } + private string? RemoveButtonClassString => CssBuilder.Default() + .AddClass(DeleteButtonClass) + .Build(); + + private bool IsDeleteButtonDisabled => IsDisabled || UploadFiles.Count == 0; + + private string? BrowserButtonClassString => CssBuilder.Default("btn-browser") + .AddClass(BrowserButtonClass) + .Build(); + + private string? ClassString => CssBuilder.Default("upload") + .AddClassFromAttributes(AdditionalAttributes) + .Build(); /// /// @@ -105,67 +95,20 @@ protected override void OnParametersSet() { base.OnParametersSet(); + DeleteButtonText ??= Localizer[nameof(DeleteButtonText)]; + BrowserButtonText ??= Localizer[nameof(BrowserButtonText)]; + BrowserButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.InputUploadBrowserButtonIcon); DeleteButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.InputUploadDeleteButtonIcon); } - /// - /// 上传文件改变时回调方法 - /// - /// - /// - protected override async Task OnFileChange(InputFileChangeEventArgs args) + private async Task TriggerDeleteFile() { - CurrentFile = new UploadFile() - { - OriginFileName = args.File.Name, - Size = args.File.Size, - File = args.File, - Uploaded = false - }; - - UploadFiles.Clear(); - UploadFiles.Add(CurrentFile); - - await base.OnFileChange(args); - - if (OnChange != null) - { - await OnChange(CurrentFile); - } - CurrentFile.Uploaded = true; - } - - private async Task OnDeleteFile() - { - if (CurrentFile != null) - { - var ret = await OnFileDelete(CurrentFile); - if (ret) - { - CurrentFile = null; - CurrentValue = default; - } - } - } - - /// - /// - /// - /// - public override Task ToggleMessage(IReadOnlyCollection results) - { - if (results.Any()) - { - ErrorMessage = results.First().ErrorMessage; - IsValid = false; - } - else + for (var index = Files.Count; index > 0; index--) { - ErrorMessage = null; - IsValid = true; + var item = Files[index - 1]; + await OnFileDelete(item); } - OnValidate(IsValid); - return Task.CompletedTask; + CurrentValue = default; } } diff --git a/src/BootstrapBlazor/Components/Upload/UploadBase.razor.scss b/src/BootstrapBlazor/Components/Upload/InputUpload.razor.scss similarity index 89% rename from src/BootstrapBlazor/Components/Upload/UploadBase.razor.scss rename to src/BootstrapBlazor/Components/Upload/InputUpload.razor.scss index da18bd474f7..89294fbba09 100644 --- a/src/BootstrapBlazor/Components/Upload/UploadBase.razor.scss +++ b/src/BootstrapBlazor/Components/Upload/InputUpload.razor.scss @@ -1,18 +1,19 @@ -.upload { +.upload { --bb-upload-body-margin-top: #{$bb-upload-body-margin-top}; --bb-upload-body-list-max-height: #{$bb-upload-body-list-max-height}; --bb-upload-body-list-item-padding: #{$bb-upload-body-list-item-padding}; --bb-upload-body-list-item-body-padding: #{$bb-upload-body-list-item-body-padding}; --bb-upload-body-list-item-hover-color: #{$bb-upload-body-list-item-hover-color}; + --bb-upload-body-list-grap: #{$bb-upload-body-list-grap}; --bb-upload-card-width: #{$bb-upload-card-width}; --bb-upload-card-height: #{$bb-upload-card-height}; --bb-upload-card-shadow: #{$bb-upload-card-shadow}; --bb-upload-card-padding: #{$bb-upload-card-padding}; - --bb-upload-card-margin: #{$bb-upload-card-margin}; --bb-upload-card-item-width: #{$bb-upload-card-item-width}; --bb-upload-drop-height: #{$bb-upload-drop-height}; --bb-upload-drop-footer-font-size: #{$bb-upload-drop-footer-font-size}; --bb-upload-drop-footer-margin-top: #{$bb-upload-drop-footer-margin-top}; + --bb-upload-item-border-radius: #{$bb-upload-item-border-radius}; } .upload .upload-body { @@ -42,10 +43,6 @@ background-color: var(--bs-secondary-bg); } -.upload .upload-body.is-list .upload-item:hover .fa-trash-can { - display: inline-block; -} - .upload .upload-body.is-list .upload-item .upload-item-body { flex: 1; padding: var(--bb-upload-body-list-item-body-padding); @@ -62,9 +59,6 @@ padding-right: 0.25rem; } -.upload .upload-body.is-list .upload-item .fa-trash-can, -.upload .upload-body.is-list .upload-item:not(.disabled):hover .fa-circle-check, -.upload .upload-body.is-list .upload-item:hover .fa-xmark-circle, .upload .upload-body.is-avatar .upload-item .upload-item-delete, .upload .upload-body.is-avatar .upload-item.is-invalid .upload-item-spin, .upload .upload-body.is-avatar .upload-item.is-valid .upload-item-spin, @@ -98,6 +92,7 @@ margin: 0; display: flex; flex-wrap: wrap; + gap: 1rem; } .upload .upload-body.is-avatar .upload-item { @@ -105,26 +100,20 @@ position: relative; border: 1px dashed var(--bs-border-color); border-radius: 6px; - margin-inline-end: 1rem; - margin-block-end: 1rem; overflow: hidden; cursor: pointer; } -.upload .upload-body.is-avatar .upload-item.is-single { - margin: 0; -} - .upload .upload-body.is-avatar .upload-item.is-invalid { border-color: var(--bs-danger); border-style: solid; } .upload .upload-body.is-avatar .upload-item.is-circle { - border-radius: 50%; + border-radius: var(--bb-upload-item-border-radius); } -.upload .upload-body.is-avatar .upload-item:not(.is-form):hover, +.upload .upload-body.is-avatar .upload-item:not(.is-form):not(.is-valid):not(.is-invalid):hover, .upload .upload-body.is-avatar .upload-item:not(.is-form).is-valid, .upload .upload-body.is-card .upload-item.is-valid, .upload .upload-body.is-card .upload-item:not(.disabled):hover { @@ -146,7 +135,7 @@ } .upload .upload-body.is-avatar .upload-item .upload-item-actions, -.upload .upload-body.is-card .upload-item .upload-item-actions.btn-browser { +.upload .upload-body.is-card .upload-item .upload-item-actions > .upload-item-plus { position: absolute; top: 0; left: 0; @@ -179,7 +168,6 @@ height: var(--bb-upload-card-height); position: relative; cursor: pointer; - margin: var(--bb-upload-card-margin); overflow: hidden; } @@ -301,6 +289,29 @@ } .upload.is-drop { + + &.dropping { + .upload-drop-body { + border-color: var(--bs-success); + + * { + pointer-events: none; + } + } + + &.disabled { + .upload-drop-body { + border-color: var(--bs-danger); + } + } + } + + &.disabled { + .upload-drop-body { + border-color: var(--bs-border-color); + } + } + .upload-drop-body { border: 1px dashed var(--bs-primary); border-radius: var(--bs-border-radius); @@ -317,7 +328,7 @@ } .upload-drop-text { - margin-top: 1rem; + margin-block-start: 1rem; } em { @@ -328,7 +339,7 @@ .upload-drop-footer { font-size: var(--bb-upload-drop-footer-font-size); - margin-top: var(--bb-upload-drop-footer-margin-top); + margin-block-start: var(--bb-upload-drop-footer-margin-top); } .upload-drop-list { diff --git a/src/BootstrapBlazor/Components/Upload/MultipleUploadBase.cs b/src/BootstrapBlazor/Components/Upload/MultipleUploadBase.cs deleted file mode 100644 index eef0a2756d7..00000000000 --- a/src/BootstrapBlazor/Components/Upload/MultipleUploadBase.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License -// See the LICENSE file in the project root for more information. -// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone - -namespace BootstrapBlazor.Components; - -/// -/// MultipleUploadBase 基类 -/// -public abstract class MultipleUploadBase : UploadBase -{ - /// - /// - /// - /// - /// - protected string? GetItemClassString(UploadFile item) => CssBuilder.Default(ItemClassString) - .AddClass("is-valid", item.Uploaded && item.Code == 0) - .AddClass("is-invalid", item.Code != 0) - .AddClass("disabled", IsDisabled) - .Build(); - - /// - /// - /// - protected virtual string? ItemClassString => CssBuilder.Default("upload-item") - .Build(); - - /// - /// 获得/设置 已上传文件集合 - /// - [Parameter] - public List? DefaultFileList { get; set; } - - /// - /// 获得/设置 是否显示上传进度 默认为 false - /// - [Parameter] - public bool ShowProgress { get; set; } - - /// - /// OnFileDelete 回调委托 - /// - /// - /// - protected override async Task OnFileDelete(UploadFile item) - { - var ret = await base.OnFileDelete(item); - if (ret) - { - UploadFiles.Remove(item); - if (!string.IsNullOrEmpty(item.ValidateId)) - { - await RemoveValidResult(item.ValidateId); - } - DefaultFileList?.Remove(item); - } - return ret; - } - - /// - /// 是否显示进度条方法 - /// - /// - /// - protected bool GetShowProgress(UploadFile item) => ShowProgress && !item.Uploaded; - - /// - /// 清空上传列表方法 - /// - public override void Reset() - { - DefaultFileList?.Clear(); - base.Reset(); - } -} diff --git a/src/BootstrapBlazor/Components/Upload/SingleUploadBase.cs b/src/BootstrapBlazor/Components/Upload/SingleUploadBase.cs deleted file mode 100644 index 4cfac7b9513..00000000000 --- a/src/BootstrapBlazor/Components/Upload/SingleUploadBase.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License -// See the LICENSE file in the project root for more information. -// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone - -namespace BootstrapBlazor.Components; - -/// -/// SingleUploadBase 基类 -/// -/// -public abstract class SingleUploadBase : MultipleUploadBase -{ - /// - /// 获得/设置 是否仅上传一次 默认 false - /// - [Parameter] - public bool IsSingle { get; set; } - - /// - /// 获得/设置 最大上传个数 默认为最大值 - /// - [Parameter] - public int Max { get; set; } = int.MaxValue; - - /// - /// 是否显示上传组件 - /// - protected bool CheckCanUpload() - { - var count = GetUploadFiles().Count; - return IsSingle ? count < 1 : count < Max; - } - - /// - /// 获得当前图片集合 - /// - /// - protected virtual List GetUploadFiles() - { - var ret = new List(); - if (IsSingle) - { - if (DefaultFileList != null && DefaultFileList.Count != 0) - { - ret.Add(DefaultFileList.First()); - } - if (ret.Count == 0 && UploadFiles.Count != 0) - { - ret.Add(UploadFiles.First()); - } - } - else - { - if (DefaultFileList != null) - { - ret.AddRange(DefaultFileList); - } - ret.AddRange(UploadFiles); - } - return ret; - } - - /// - /// OnFileDelete 回调委托 - /// - /// - /// - protected override async Task OnFileDelete(UploadFile item) - { - var ret = await base.OnFileDelete(item); - if (ret) - { - if (IsSingle) - { - UploadFiles.Clear(); - } - else - { - UploadFiles.Remove(item); - } - if (!string.IsNullOrEmpty(item.ValidateId)) - { - await RemoveValidResult(item.ValidateId); - } - RemoveItem(); - } - - void RemoveItem() - { - if (DefaultFileList != null) - { - if (IsSingle) - { - DefaultFileList.Clear(); - } - else - { - DefaultFileList.Remove(item); - } - } - } - return ret; - } - - /// - /// 更新上传进度方法 - /// - /// - protected void Update(UploadFile file) - { - if (GetShowProgress(file)) - { - StateHasChanged(); - } - } -} diff --git a/src/BootstrapBlazor/Components/Upload/UploadBase.cs b/src/BootstrapBlazor/Components/Upload/UploadBase.cs index bcbac5ad920..77c9cc61b05 100644 --- a/src/BootstrapBlazor/Components/Upload/UploadBase.cs +++ b/src/BootstrapBlazor/Components/Upload/UploadBase.cs @@ -14,23 +14,44 @@ namespace BootstrapBlazor.Components; public abstract class UploadBase : ValidateBase, IUpload { /// - /// 获得 组件样式 + /// 获得/设置 是否仅上传一次 默认 false /// - protected string? ClassString => CssBuilder.Default("upload") - .AddClassFromAttributes(AdditionalAttributes) - .Build(); + [Parameter] + [Obsolete("已弃用 通过 IsMultiple 参数实现此功能; Deprecated. please use IsMultiple parameter.")] + [ExcludeFromCodeCoverage] + public bool IsSingle { get; set; } + + /// + /// 获得/设置 最大上传个数 默认为最大值 + /// + [Parameter] + [Obsolete("已弃用 通过 MaxFileCount 参数实现此功能; Deprecated. please use MaxFileCount parameter.")] + [ExcludeFromCodeCoverage] + public int Max { get; set; } = int.MaxValue; + + /// + /// 获得/设置 最大上传个数 默认为 null + /// + [Parameter] + public int? MaxFileCount { get; set; } /// - /// 获得/设置 当前上传文件 + /// 获得/设置 所有文件上传完毕回调方法 默认 null /// - protected UploadFile? CurrentFile { get; set; } + [Parameter] + public Func, Task>? OnAllFileUploaded { get; set; } /// - /// 获得/设置 上传文件集合 + /// 获得/设置 已上传文件集合,可用于组件初始化 /// - protected List UploadFiles { get; } = []; + [Parameter] + public List? DefaultFileList { get; set; } - List IUpload.UploadFiles { get => UploadFiles; } + /// + /// 获得/设置 是否显示上传进度 默认为 false + /// + [Parameter] + public bool ShowProgress { get; set; } /// /// 获得/设置 上传接收的文件格式 默认为 null 接收任意格式 @@ -44,6 +65,18 @@ public abstract class UploadBase : ValidateBase, IUpload [Parameter] public string? Capture { get; set; } + /// + /// 获得/设置 是否上传整个目录 默认为 false + /// + [Parameter] + public bool IsDirectory { get; set; } + + /// + /// 获得/设置 是否允许多文件上传 默认 false 不允许 + /// + [Parameter] + public bool IsMultiple { get; set; } + /// /// 获得/设置 点击删除按钮时回调此方法 默认 null /// @@ -51,44 +84,145 @@ public abstract class UploadBase : ValidateBase, IUpload public Func>? OnDelete { get; set; } /// - /// 获得/设置 点击浏览按钮时回调此方法 默认 null + /// 获得/设置 点击浏览按钮时回调此方法,如果多文件上传此回调会触发多次 默认 null /// [Parameter] public Func? OnChange { get; set; } + /// + /// 获得/设置 已上传文件集合,此集合中数据是用户上传文件集合 + /// + public List UploadFiles { get; } = []; + + /// + /// Gets the collection of files to be uploaded. + /// + protected List Files => GetUploadFiles(); + /// /// /// - /// - public override Task ToggleMessage(IReadOnlyCollection results) + protected override void OnParametersSet() + { + base.OnParametersSet(); + + _filesCache = null; + } + + /// + /// + /// + protected override string? FormatValueAsString(TValue? value) + { + if (value is null) + { + return null; + } + else if (value is IEnumerable files) + { + return string.Join(";", files.Select(i => i.Name)); + } + else if (value is IBrowserFile file) + { + return file.Name; + } + else if (value is IEnumerable strings) + { + return string.Join(";", strings); + } + else + { + return base.FormatValueAsString(value); + } + } + + /// + /// User selects files callback method + /// + /// + /// + protected async Task OnFileChange(InputFileChangeEventArgs args) { - if (FieldIdentifier != null) + var fileCount = args.FileCount; + if (MaxFileCount.HasValue) { - var messages = results.Where(item => item.MemberNames.Any(m => UploadFiles.Any(f => f.ValidateId?.Equals(m, StringComparison.OrdinalIgnoreCase) ?? false))); - if (messages.Any()) + fileCount = MaxFileCount.Value; + + // 计算剩余可上传数量 + fileCount = fileCount - Files.Count; + if (fileCount <= 0) { - IsValid = false; - if (CurrentFile != null) - { - var msg = messages.FirstOrDefault(m => m.MemberNames.Any(f => f.Equals(CurrentFile.ValidateId, StringComparison.OrdinalIgnoreCase))); - if (msg != null) - { - ErrorMessage = msg.ErrorMessage; - } - } + // 如果剩余可上传数量小于等于 0 则不允许继续上传 + return; } - else + } + + var items = args.GetMultipleFiles(args.FileCount).Take(fileCount).Select(f => + { + var file = new UploadFile() { - ErrorMessage = null; - IsValid = true; - } - OnValidate(IsValid); + OriginFileName = f.Name, + Size = f.Size, + File = f, + FileCount = args.FileCount, + Uploaded = false, + UpdateCallback = Update + }; + file.ValidateId = $"{Id}_{file.GetHashCode()}"; + return file; + }).ToList(); + + foreach (var item in items) + { + _filesCache = null; + UploadFiles.Add(item); + + // trigger OnChange event callback + // 回调给用户,用于存储文件并生成预览地址给 PreUrl + await TriggerOnChanged(item); + item.Uploaded = true; + StateHasChanged(); + } + + // trigger OnAllFileUploaded event callback + if (OnAllFileUploaded != null) + { + await OnAllFileUploaded(items); + } + + if (ValueType.IsAssignableTo(typeof(IEnumerable))) + { + CurrentValue = (TValue)(object)items.Select(f => f.File).ToList(); + } + else if (ValueType.IsAssignableTo(typeof(IEnumerable))) + { + CurrentValue = (TValue)(object)items.Select(f => f.OriginFileName).ToList(); + } + else if (ValueType == typeof(IBrowserFile)) + { + CurrentValue = (TValue)items[0].File!; + } + else if (ValueType == typeof(string)) + { + CurrentValue = (TValue)(object)string.Join(";", items.Select(f => f.OriginFileName)); } - return Task.CompletedTask; } /// - /// + /// 触发 OnChanged 事件回调方法 + /// + /// + /// + protected virtual async Task TriggerOnChanged(UploadFile file) + { + if (OnChange != null) + { + await OnChange(file); + } + } + + /// + /// Delete file method. /// /// /// @@ -100,56 +234,137 @@ protected virtual async Task OnFileDelete(UploadFile item) ret = await OnDelete(item); } ErrorMessage = null; + + if (ret) + { + if (!string.IsNullOrEmpty(item.ValidateId)) + { + await RemoveValidResult(item.ValidateId); + } + UploadFiles.Remove(item); + DefaultFileList?.Remove(item); + _filesCache = null; + } + StateHasChanged(); return ret; } /// - /// 上传文件改变时回调此方法 + /// 是否显示进度条方法 /// - /// + /// /// - protected virtual Task OnFileChange(InputFileChangeEventArgs args) + protected bool GetShowProgress(UploadFile item) => ShowProgress && !item.Uploaded; + + /// + /// 更新上传进度方法 + /// + /// + protected void Update(UploadFile file) { - // 判定可为空 - var type = NullableUnderlyingType ?? typeof(TValue); - if (type.IsAssignableTo(typeof(IBrowserFile))) + if (GetShowProgress(file)) { - CurrentValue = (TValue)args.File; + StateHasChanged(); } - if (type.IsAssignableTo(typeof(List))) + } + + private List? _filesCache; + /// + /// 获得当前文件集合 + /// Get the files collection. + /// + /// + protected List GetUploadFiles() + { + if (_filesCache == null) { - CurrentValue = (TValue)(object)UploadFiles.Select(f => f.File).ToList(); + _filesCache = []; + if (DefaultFileList != null) + { + _filesCache.AddRange(DefaultFileList); + } + _filesCache.AddRange(UploadFiles); } - return Task.CompletedTask; + return _filesCache; } /// - /// + /// 检查是否可以继续上传文件 + /// Check whether can upload file. /// /// - protected virtual IDictionary GetUploadAdditionalAttributes() + protected bool CanUpload() { - var ret = new Dictionary - { - { "hidden", "hidden" } - }; - if (!string.IsNullOrEmpty(Accept)) + // 允许多上传 + if (IsMultiple) { - ret.Add("accept", Accept); + return !MaxFileCount.HasValue || Files.Count < MaxFileCount; } - if (!string.IsNullOrEmpty(Capture)) + + // 只允许单个上传 + return Files.Count == 0; + } + + /// + /// 检查上传按钮是否可用方法 不可用时返回 true + /// + /// + protected bool CheckStatus() => IsDisabled || !CanUpload(); + + /// + /// 判断是否显示新建按钮 + /// + /// + protected bool ShowAddButton() + { + if (IsDisabled) { - ret.Add("capture", Capture); + return Files.Count == 0; } - return ret; + + return CanUpload(); } /// /// 清空上传列表方法 + /// Clear the upload files collection. /// public virtual void Reset() { + DefaultFileList?.Clear(); UploadFiles.Clear(); + _filesCache = null; + CurrentValue = default; StateHasChanged(); } + + /// + /// append html attribute method. + /// + /// + protected IDictionary GetUploadAdditionalAttributes() + { + var ret = new Dictionary + { + { "hidden", "hidden" } + }; + if (!string.IsNullOrEmpty(Accept)) + { + ret.Add("accept", Accept); + } + if (!string.IsNullOrEmpty(Capture)) + { + ret.Add("capture", Capture); + } + if (IsMultiple) + { + ret.Add("multiple", "multiple"); + } + if (IsDirectory) + { + ret.Add("directory", "directory"); + ret.Add("webkitdirectory", "webkitdirectory"); + } + return ret; + } } diff --git a/src/BootstrapBlazor/Components/Upload/UploadFile.cs b/src/BootstrapBlazor/Components/Upload/UploadFile.cs index ee9c2763e08..8c985d92c83 100644 --- a/src/BootstrapBlazor/Components/Upload/UploadFile.cs +++ b/src/BootstrapBlazor/Components/Upload/UploadFile.cs @@ -13,7 +13,7 @@ namespace BootstrapBlazor.Components; public class UploadFile { /// - /// 获得/设置 文件名 + /// 获得/设置 文件名 由用户指定 上传文件时此参数未设置 默认为 null /// public string? FileName { get; set; } @@ -48,7 +48,7 @@ public class UploadFile public IBrowserFile? File { get; set; } /// - /// 获得/设置 上传文件数量 + /// 获得/设置 上传文件总数量 /// public int FileCount { get; init; } = 1; @@ -72,16 +72,11 @@ public class UploadFile /// internal string? ValidateId { get; set; } - /// - /// 获得/设置 组件是否合规 默认为 null 未检查 - /// - internal bool? IsValid { get; set; } - /// /// 获得 UploadFile 文件名 /// /// - public string? GetFileName() => OriginFileName ?? FileName; + public string? GetFileName() => FileName ?? OriginFileName ?? File?.Name; /// /// 获得 UploadFile 文件扩展名 diff --git a/src/BootstrapBlazor/Components/Upload/UploadPreviewList.razor b/src/BootstrapBlazor/Components/Upload/UploadPreviewList.razor new file mode 100644 index 00000000000..6678f3f64f3 --- /dev/null +++ b/src/BootstrapBlazor/Components/Upload/UploadPreviewList.razor @@ -0,0 +1,41 @@ +@namespace BootstrapBlazor.Components + +@if (Items != null) +{ +
+ @foreach (var item in Items) + { +
+ +
+ @item.GetFileName() + (@item.Size.ToFileSizeString()) +
+ @if (GetShowProgress(item)) + { + + + + } + else + { +
+ @if (item.Code == 0) + { + @if (ShowDownloadButton) + { + + } + + } + else + { + + } + +
+ } +
+ } +
+} diff --git a/src/BootstrapBlazor/Components/Upload/ButtonUploadBase.cs b/src/BootstrapBlazor/Components/Upload/UploadPreviewList.razor.cs similarity index 52% rename from src/BootstrapBlazor/Components/Upload/ButtonUploadBase.cs rename to src/BootstrapBlazor/Components/Upload/UploadPreviewList.razor.cs index eb37f1f6aeb..05806121dce 100644 --- a/src/BootstrapBlazor/Components/Upload/ButtonUploadBase.cs +++ b/src/BootstrapBlazor/Components/Upload/UploadPreviewList.razor.cs @@ -3,34 +3,81 @@ // See the LICENSE file in the project root for more information. // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone -using Microsoft.AspNetCore.Components.Forms; - namespace BootstrapBlazor.Components; /// -/// 按钮上传组件基类 +/// UploadPreviewList component /// -public abstract class ButtonUploadBase : SingleUploadBase +public partial class UploadPreviewList { /// - /// 获得/设置 是否上传整个目录 默认为 false + /// Gets or sets the collection of files to be uploaded. + /// + [Parameter] + [NotNull] + public List? Items { get; set; } + + /// + /// Gets or sets the disable status of the upload list. /// [Parameter] - public bool IsDirectory { get; set; } + public bool IsDisabled { get; set; } /// - /// 获得/设置 是否允许多文件上传 默认 false 不允许 + /// Gets or sets a value indicating whether progress should be displayed during the operation. /// - /// 多选文件时,所有文件处理完毕后,会额外触发一次 回调 [Parameter] - public bool IsMultiple { get; set; } + public bool ShowProgress { get; set; } /// - /// 获得/设置 设置文件格式图标回调委托 + /// Gets or sets the upload file format callback method. /// [Parameter] public Func? OnGetFileFormat { get; set; } + /// + /// Gets or sets the callback method for the cancel button click event. Default is null + /// 获得/设置 点击取消按钮回调此方法 默认 null + /// + [Parameter] + public Func? OnCancel { get; set; } + + /// + /// 获得/设置 取消图标 + /// + [Parameter] + public string? CancelIcon { get; set; } + + /// + /// 获得/设置 浏览按钮图标 + /// + [Parameter] + public string? LoadingIcon { get; set; } + + /// + /// 获得/设置 下载按钮图标 + /// + [Parameter] + public string? DownloadIcon { get; set; } + + /// + /// 获得/设置 上传失败状态图标 + /// + [Parameter] + public string? InvalidStatusIcon { get; set; } + + /// + /// 获得/设置 上传成功状态图标 + /// + [Parameter] + public string? ValidStatusIcon { get; set; } + + /// + /// 获得/设置 删除按钮图标 + /// + [Parameter] + public string? DeleteIcon { get; set; } + /// /// 获得/设置 是否显示下载按钮 默认 false /// @@ -43,6 +90,12 @@ public abstract class ButtonUploadBase : SingleUploadBase [Parameter] public Func? OnDownload { get; set; } + /// + /// 获得/设置 点击删除按钮时回调此方法 默认 null + /// + [Parameter] + public Func>? OnDelete { get; set; } + /// /// 获得/设置 Excel 类型文件图标 /// @@ -109,41 +162,42 @@ public abstract class ButtonUploadBase : SingleUploadBase [Parameter] public string? FileIconFile { get; set; } - /// - /// 获得/设置 取消图标 - /// - [Parameter] - public string? CancelIcon { get; set; } - - /// - /// 获得/设置 点击取消按钮回调此方法 默认 null - /// - [Parameter] - public Func? OnCancel { get; set; } - - /// - /// 获得/设置 所有文件上传完毕回调方法 默认 null - /// - [Parameter] - public Func, Task>? OnAllFileUploaded { get; set; } - [Inject] [NotNull] private IIconTheme? IconTheme { get; set; } - /// - /// OnInitialized 方法 - /// - protected override void OnInitialized() - { - base.OnInitialized(); + private string? ItemClassString => CssBuilder.Default("upload-item") + .AddClass("disabled", IsDisabled) + .Build(); - // 上传文件夹时 开启 Multiple 属性 - if (IsDirectory) - { - IsMultiple = true; - } - } + private string? GetItemClassString(UploadFile item) => CssBuilder.Default(ItemClassString) + .AddClass("is-valid", item is { Uploaded: true, Code: 0 }) + .AddClass("is-invalid", item.Code != 0) + .Build(); + + private string? LoadingIconString => CssBuilder.Default("loading-icon") + .AddClass(LoadingIcon) + .Build(); + + private string? CancelIconString => CssBuilder.Default("cancel-icon") + .AddClass(CancelIcon) + .Build(); + + private string? DownloadIconString => CssBuilder.Default("download-icon") + .AddClass(DownloadIcon) + .Build(); + + private string? DeleteIconString => CssBuilder.Default("delete-icon") + .AddClass(DeleteIcon) + .Build(); + + private string? ValidStatusIconString => CssBuilder.Default("valid-icon") + .AddClass(ValidStatusIcon) + .Build(); + + private string? InvalidStatusIconString => CssBuilder.Default("invalid-icon") + .AddClass(InvalidStatusIcon) + .Build(); /// /// @@ -152,6 +206,10 @@ protected override void OnParametersSet() { base.OnParametersSet(); + LoadingIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadLoadingIcon); + InvalidStatusIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadInvalidStatusIcon); + ValidStatusIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadValidStatusIcon); + FileIconExcel ??= IconTheme.GetIconByKey(ComponentIcons.FileIconExcel); FileIconDocx ??= IconTheme.GetIconByKey(ComponentIcons.FileIconDocx); FileIconPPT ??= IconTheme.GetIconByKey(ComponentIcons.FileIconPPT); @@ -163,143 +221,59 @@ protected override void OnParametersSet() FileIconArchive ??= IconTheme.GetIconByKey(ComponentIcons.FileIconArchive); FileIconImage ??= IconTheme.GetIconByKey(ComponentIcons.FileIconImage); FileIconFile ??= IconTheme.GetIconByKey(ComponentIcons.FileIconFile); - CancelIcon ??= IconTheme.GetIconByKey(ComponentIcons.UploadCancelIcon); } - /// - /// - /// - /// - /// - protected override async Task OnFileChange(InputFileChangeEventArgs args) + private async Task OnClickDownload(UploadFile item) { - if (IsMultiple) - { - var items = args.GetMultipleFiles(args.FileCount).Select(f => new UploadFile() - { - OriginFileName = f.Name, - Size = f.Size, - File = f, - FileCount = args.FileCount, - Uploaded = OnChange == null, - UpdateCallback = Update - }).ToList(); - UploadFiles.AddRange(items); - if (OnChange != null) - { - foreach (var item in items) - { - await OnChange(item); - item.Uploaded = true; - StateHasChanged(); - } - } - if (OnAllFileUploaded != null) - { - await OnAllFileUploaded(UploadFiles); - } - } - else + if (OnDownload != null) { - var file = new UploadFile() - { - OriginFileName = args.File.Name, - Size = args.File.Size, - File = args.File, - Uploaded = false, - UpdateCallback = Update - }; - UploadFiles.Add(file); - if (OnChange != null) - { - await OnChange(file); - } - file.Uploaded = true; + await OnDownload(item); } - - //触发 ValueChange,以支持 bind-value - await base.OnFileChange(args); } - /// - /// - /// - /// - /// - protected string? GetFileFormatClassString(UploadFile item) + private async Task OnFileDelete(UploadFile item) { - var builder = CssBuilder.Default("file-icon"); - var fileExtension = Path.GetExtension(item.OriginFileName ?? item.FileName); - if (!string.IsNullOrEmpty(fileExtension)) + if (OnDelete != null) { - fileExtension = fileExtension.ToLowerInvariant(); + await OnDelete(item); } - var icon = OnGetFileFormat?.Invoke(fileExtension) ?? GetFileExtensions(); - builder.AddClass(icon); - return builder.Build(); - - // switch 关键字导致无法 100% 覆盖 - [ExcludeFromCodeCoverage] - string? GetFileExtensions() => fileExtension switch - { - ".csv" or ".xls" or ".xlsx" => FileIconExcel, - ".doc" or ".docx" or ".dot" or ".dotx" => FileIconDocx, - ".ppt" or ".pptx" => FileIconPPT, - ".wav" or ".mp3" => FileIconAudio, - ".mp4" or ".mov" or ".mkv" => FileIconVideo, - ".cs" or ".html" or ".vb" => FileIconCode, - ".pdf" => FileIconPdf, - ".zip" or ".rar" or ".iso" => FileIconZip, - ".txt" or ".log" => FileIconArchive, - ".jpg" or ".jpeg" or ".png" or ".bmp" or ".gif" => FileIconImage, - _ => FileIconFile - }; } - /// - /// - /// - /// - protected override IDictionary GetUploadAdditionalAttributes() - { - var ret = base.GetUploadAdditionalAttributes(); - - if (IsMultiple) - { - ret.Add("multiple", "multiple"); - } + private bool GetShowProgress(UploadFile item) => ShowProgress && !item.Uploaded; - if (IsDirectory) + private async Task OnClickCancel(UploadFile item) + { + if (OnCancel != null) { - ret.Add("directory", "dicrectory"); - ret.Add("webkitdirectory", "webkitdirectory"); + await OnCancel(item); } - return ret; } - /// - /// 点击下载按钮回调此方法 - /// - /// - /// - protected async Task OnClickDownload(UploadFile item) + private string? GetFileFormatClassString(UploadFile item) { - if (OnDownload != null) + var builder = CssBuilder.Default("file-icon"); + var fileExtension = Path.GetExtension(item.OriginFileName ?? item.FileName); + if (!string.IsNullOrEmpty(fileExtension)) { - await OnDownload(item); + fileExtension = fileExtension.ToLowerInvariant(); } + var icon = OnGetFileFormat?.Invoke(fileExtension) ?? GetFileExtensions(fileExtension); + builder.AddClass(icon); + return builder.Build(); } - /// - /// 点击取消按钮回调此方法 - /// - /// - /// - protected async Task OnClickCancel(UploadFile item) + private string? GetFileExtensions(string? fileExtension) => fileExtension switch { - if (OnCancel != null) - { - await OnCancel(item); - } - } + ".csv" or ".xls" or ".xlsx" => FileIconExcel, + ".doc" or ".docx" or ".dot" or ".dotx" => FileIconDocx, + ".ppt" or ".pptx" => FileIconPPT, + ".wav" or ".mp3" => FileIconAudio, + ".mp4" or ".mov" or ".mkv" => FileIconVideo, + ".cs" or ".html" or ".vb" => FileIconCode, + ".pdf" => FileIconPdf, + ".zip" or ".rar" or ".iso" => FileIconZip, + ".txt" or ".log" => FileIconArchive, + ".jpg" or ".jpeg" or ".png" or ".bmp" or ".gif" => FileIconImage, + _ => FileIconFile + }; } diff --git a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs index 1f4ca2eb312..bc6b72bb54e 100644 --- a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs +++ b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs @@ -327,7 +327,7 @@ protected override void OnParametersSet() } /// - /// OnAfterRender 方法 + /// /// /// protected override async Task OnAfterRenderAsync(bool firstRender) @@ -464,8 +464,8 @@ public virtual Task ToggleMessage(IReadOnlyCollection results) { if (FieldIdentifier != null) { - var messages = results.Where(item => item.MemberNames.Any(m => m == FieldIdentifier.Value.FieldName)); - if (messages.Any()) + var messages = results.Where(item => item.MemberNames.Any(m => m == FieldIdentifier.Value.FieldName)).ToList(); + if (messages.Count > 0) { ErrorMessage = messages.First().ErrorMessage; IsValid = false; @@ -484,9 +484,16 @@ public virtual Task ToggleMessage(IReadOnlyCollection results) return Task.CompletedTask; } - private JSModule? ValidateModule { get; set; } + /// + /// Gets or sets the module of validate instance. + /// + protected JSModule? ValidateModule { get; set; } - private Task LoadValidateModule() => JSRuntime.LoadModuleByName("validate"); + /// + /// 加载 validate 模块方法 + /// + /// + protected Task LoadValidateModule() => JSRuntime.LoadModuleByName("validate"); /// /// 增加客户端 Tooltip 方法 diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index 4e4c18250ad..7787dd1308d 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -510,6 +510,9 @@ private async Task ValidateAsync(IValidateComponent validator, ValidationContext // 未选择文件 ValidateDataAnnotations(propertyValue, context, messages, pi); } + + _tcs = new TaskCompletionSource(); + _tcs.TrySetResult(messages.Count == 0); } else { diff --git a/src/BootstrapBlazor/Enums/ComponentIcons.cs b/src/BootstrapBlazor/Enums/ComponentIcons.cs index a401bf07393..4a465fbdd47 100644 --- a/src/BootstrapBlazor/Enums/ComponentIcons.cs +++ b/src/BootstrapBlazor/Enums/ComponentIcons.cs @@ -55,31 +55,6 @@ public enum ComponentIcons /// AutoFillIcon, - /// - /// ButtonUpload 组件 LoadingIcon 属性图标 - /// - ButtonUploadLoadingIcon, - - /// - /// ButtonUpload 组件 FailedInvalidIcon 属性图标 - /// - ButtonUploadInvalidStatusIcon, - - /// - /// ButtonUpload 组件 FailedValidIcon 属性图标 - /// - ButtonUploadValidStatusIcon, - - /// - /// ButtonUpload 组件 DownloadIcon 属性图标 - /// - ButtonUploadDownloadIcon, - - /// - /// ButtonUpload 组件 DeleteIcon 属性图标 - /// - ButtonUploadDeleteIcon, - /// /// ButtonUpload 组件 BrowserButtonIcon 属性图标 /// @@ -116,30 +91,45 @@ public enum ComponentIcons CardUploadStatusIcon, /// - /// CardUpload 组件 DeleteIcon 图标 + /// CardUpload 组件 RemoveIcon 图标 + /// + CardUploadRemoveIcon, + + /// + /// CardUpload 组件 ZoomIcon 图标 /// - CardUploadDeleteIcon, + CardUploadZoomIcon, /// - /// CardUpload 组件 RemoveIcon 图标 + /// ButtonUpload 组件 LoadingIcon 属性图标 /// - CardUploadRemoveIcon, + UploadLoadingIcon, /// - /// CardUpload 组件 DownloadIcon 图标 + /// ButtonUpload 组件 FailedInvalidIcon 属性图标 /// - CardUploadDownloadIcon, + UploadInvalidStatusIcon, /// - /// CardUpload 组件 ZoomIcon 图标 + /// ButtonUpload 组件 FailedValidIcon 属性图标 /// - CardUploadZoomIcon, + UploadValidStatusIcon, /// /// Upload 组件 CancelIcon 图标 /// UploadCancelIcon, + /// + /// CardUpload 组件 DeleteIcon 图标 + /// + UploadDeleteIcon, + + /// + /// CardUpload 组件 DownloadIcon 图标 + /// + UploadDownloadIcon, + /// /// Upload 组件 UploadIcon 图标 /// diff --git a/src/BootstrapBlazor/Extensions/UploadFileExtensions.cs b/src/BootstrapBlazor/Extensions/UploadFileExtensions.cs index 54d84ac82e0..9897efed35d 100644 --- a/src/BootstrapBlazor/Extensions/UploadFileExtensions.cs +++ b/src/BootstrapBlazor/Extensions/UploadFileExtensions.cs @@ -20,19 +20,26 @@ public static class UploadFileExtensions /// /// /// + /// /// [ExcludeFromCodeCoverage] - public static async Task RequestBase64ImageFileAsync(this UploadFile upload, string format, int maxWidth, int maxHeight, long maxAllowedSize = 512000, CancellationToken token = default) + public static async Task RequestBase64ImageFileAsync(this UploadFile upload, string? format = null, int maxWidth = 320, int maxHeight = 240, long? maxAllowedSize = null, List? allowExtensions = null, CancellationToken token = default) { if (upload.File != null) { try { - var imageFile = await upload.File.RequestImageFileAsync(format, maxWidth, maxHeight); - using var fileStream = imageFile.OpenReadStream(maxAllowedSize, token); - using var memoryStream = new MemoryStream(); - await fileStream.CopyToAsync(memoryStream, token); - upload.PrevUrl = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}"; + format ??= upload.File.ContentType; + if (upload.IsImage(allowExtensions)) + { + var imageFile = await upload.File.RequestImageFileAsync(format, maxWidth, maxHeight); + + maxAllowedSize ??= upload.File.Size; + using var fileStream = imageFile.OpenReadStream(maxAllowedSize.Value, token); + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, token); + upload.PrevUrl = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}"; + } upload.Uploaded = true; } catch (Exception ex) @@ -181,4 +188,54 @@ public static async Task SaveToFileAsync(this UploadFile upload, string fi } return ret; } + + /// + /// Check item whether is image extension method. + /// + /// + /// + /// + /// + public static bool IsImage(this UploadFile item, List? allowExtensions = null, Func? _callback = null) + { + bool ret; + if (_callback != null) + { + ret = _callback(item); + } + else if (item.File != null) + { + ret = item.File.ContentType.Contains("image", StringComparison.OrdinalIgnoreCase) || item.IsAllowExtensions(allowExtensions); + } + else + { + ret = item.IsBase64Format() || item.IsAllowExtensions(allowExtensions); + } + return ret; + } + + /// + /// Check item whether is base64 format image extension method. + /// + /// + /// + public static bool IsBase64Format(this UploadFile item) => !string.IsNullOrEmpty(item.PrevUrl) && item.PrevUrl.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase); + + /// + /// Check the extension whether in the allowExtensions list. + /// + /// + /// + /// + public static bool IsAllowExtensions(this UploadFile item, List? allowExtensions = null) + { + var ret = false; + allowExtensions ??= [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"]; + var fileName = item.File?.Name ?? item.FileName ?? item.PrevUrl; + if (!string.IsNullOrEmpty(fileName)) + { + ret = allowExtensions.Contains(Path.GetExtension(fileName), StringComparer.OrdinalIgnoreCase); + } + return ret; + } } diff --git a/src/BootstrapBlazor/Icons/BootstrapIcons.cs b/src/BootstrapBlazor/Icons/BootstrapIcons.cs index 641d0182b37..62b89e20f24 100644 --- a/src/BootstrapBlazor/Icons/BootstrapIcons.cs +++ b/src/BootstrapBlazor/Icons/BootstrapIcons.cs @@ -194,23 +194,21 @@ internal static class BootstrapIcons { ComponentIcons.AvatarUploadInvalidStatusIcon, "bi bi-x bi-rotate-315" }, { ComponentIcons.ButtonUploadBrowserButtonIcon, "bi bi-folder2-open" }, - { ComponentIcons.ButtonUploadLoadingIcon, "bi bi-arrow-clockwise bi-spin" }, - { ComponentIcons.ButtonUploadInvalidStatusIcon, "bi bi-x-circle" }, - { ComponentIcons.ButtonUploadValidStatusIcon, "bi bi-check-circle" }, - { ComponentIcons.ButtonUploadDeleteIcon, "bi bi-trash3" }, - { ComponentIcons.ButtonUploadDownloadIcon, "bi bi-cloud-download" }, { ComponentIcons.InputUploadBrowserButtonIcon, "bi bi-folder-open" }, { ComponentIcons.InputUploadDeleteButtonIcon, "bi bi-trash3" }, { ComponentIcons.CardUploadAddIcon, "bi bi-plus" }, { ComponentIcons.CardUploadStatusIcon, "bi bi-check bi-rotate-315" }, - { ComponentIcons.CardUploadDeleteIcon, "bi bi-x bi-rotate-315" }, { ComponentIcons.CardUploadRemoveIcon, "bi bi-trash3" }, - { ComponentIcons.CardUploadDownloadIcon, "bi bi-cloud-download" }, { ComponentIcons.CardUploadZoomIcon, "bi bi-search" }, { ComponentIcons.UploadCancelIcon, "bi bi-cancel" }, { ComponentIcons.DropUploadIcon, "bi bi-cloud-arrow-up-fill" }, + { ComponentIcons.UploadLoadingIcon, "bi bi-arrow-clockwise bi-spin" }, + { ComponentIcons.UploadInvalidStatusIcon, "bi bi-x-circle" }, + { ComponentIcons.UploadValidStatusIcon, "bi bi-check-circle" }, + { ComponentIcons.UploadDownloadIcon, "bi bi-cloud-download" }, + { ComponentIcons.UploadDeleteIcon, "bi bi-x bi-rotate-315" }, { ComponentIcons.FileIconExcel, "bi bi-filetype-xlsx" }, { ComponentIcons.FileIconDocx, "bi bi-filetype-docx" }, diff --git a/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs b/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs index 893d038e89a..8e17be0655d 100644 --- a/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs +++ b/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs @@ -194,23 +194,21 @@ internal static class FontAwesomeIcons { ComponentIcons.AvatarUploadInvalidStatusIcon, "fa-solid fa-xmark" }, { ComponentIcons.ButtonUploadBrowserButtonIcon, "fa-regular fa-folder-open" }, - { ComponentIcons.ButtonUploadLoadingIcon, "fa-solid fa-spinner fa-spin" }, - { ComponentIcons.ButtonUploadInvalidStatusIcon, "fa-regular fa-circle-xmark" }, - { ComponentIcons.ButtonUploadValidStatusIcon, "fa-regular fa-circle-check" }, - { ComponentIcons.ButtonUploadDeleteIcon, "fa-regular fa-trash-can" }, - { ComponentIcons.ButtonUploadDownloadIcon, "fa-solid fa-download" }, { ComponentIcons.InputUploadBrowserButtonIcon, "fa-regular fa-folder-open" }, { ComponentIcons.InputUploadDeleteButtonIcon, "fa-regular fa-trash-can" }, { ComponentIcons.CardUploadAddIcon, "fa-solid fa-plus" }, { ComponentIcons.CardUploadStatusIcon, "fa-solid fa-check" }, - { ComponentIcons.CardUploadDeleteIcon, "fa-solid fa-xmark" }, { ComponentIcons.CardUploadRemoveIcon, "fa-regular fa-trash-can" }, - { ComponentIcons.CardUploadDownloadIcon, "fa-solid fa-download" }, { ComponentIcons.CardUploadZoomIcon, "fa-solid fa-magnifying-glass-plus" }, { ComponentIcons.UploadCancelIcon, "fa-solid fa-ban" }, { ComponentIcons.DropUploadIcon, "fa-solid fa-cloud-arrow-up" }, + { ComponentIcons.UploadLoadingIcon, "fa-solid fa-spinner fa-spin" }, + { ComponentIcons.UploadInvalidStatusIcon, "fa-regular fa-circle-xmark" }, + { ComponentIcons.UploadValidStatusIcon, "fa-regular fa-circle-check" }, + { ComponentIcons.UploadDownloadIcon, "fa-solid fa-download" }, + { ComponentIcons.UploadDeleteIcon, "fa-solid fa-xmark" }, { ComponentIcons.FileIconExcel, "fa-regular fa-file-excel" }, { ComponentIcons.FileIconDocx, "fa-regular fa-file-word" }, diff --git a/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs b/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs index a75eb1597ea..7ac9631d0db 100644 --- a/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs +++ b/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs @@ -194,23 +194,21 @@ internal static class MaterialDesignIcons { ComponentIcons.AvatarUploadInvalidStatusIcon, "mdi mdi-close mdi-rotate-315" }, { ComponentIcons.ButtonUploadBrowserButtonIcon, "mdi mdi-folder-open" }, - { ComponentIcons.ButtonUploadLoadingIcon, "mdi mdi-loading mdi-spin" }, - { ComponentIcons.ButtonUploadInvalidStatusIcon, "mdi mdi-close-circle-outline" }, - { ComponentIcons.ButtonUploadValidStatusIcon, "mdi mdi-check-circle-outline" }, - { ComponentIcons.ButtonUploadDeleteIcon, "mdi mdi-trash-can-outline" }, - { ComponentIcons.ButtonUploadDownloadIcon, "mdi mdi-cloud-download-outline" }, { ComponentIcons.InputUploadBrowserButtonIcon, "mdi mdi-folder-open" }, { ComponentIcons.InputUploadDeleteButtonIcon, "mdi mdi-trash-can-outline" }, { ComponentIcons.CardUploadAddIcon, "mdi mdi-plus" }, { ComponentIcons.CardUploadStatusIcon, "mdi mdi-check mdi-rotate-315" }, - { ComponentIcons.CardUploadDeleteIcon, "mdi mdi-close mdi-rotate-315" }, { ComponentIcons.CardUploadRemoveIcon, "mdi mdi-trash-can-outline" }, - { ComponentIcons.CardUploadDownloadIcon, "mdi mdi-cloud-download-outline" }, { ComponentIcons.CardUploadZoomIcon, "mdi mdi-magnify-plus-outline" }, { ComponentIcons.UploadCancelIcon, "mdi mdi-cancel" }, { ComponentIcons.DropUploadIcon, "mdi mdi-cloud-upload" }, + { ComponentIcons.UploadLoadingIcon, "mdi mdi-loading mdi-spin" }, + { ComponentIcons.UploadInvalidStatusIcon, "mdi mdi-close-circle-outline" }, + { ComponentIcons.UploadValidStatusIcon, "mdi mdi-check-circle-outline" }, + { ComponentIcons.UploadDeleteIcon, "mdi mdi-close mdi-rotate-315" }, + { ComponentIcons.UploadDownloadIcon, "mdi mdi-cloud-download-outline" }, { ComponentIcons.FileIconExcel, "mdi mdi-file-excel-outline" }, { ComponentIcons.FileIconDocx, "mdi mdi-file-word-outline" }, diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json index 70420663e99..4ac10f71911 100644 --- a/src/BootstrapBlazor/Locales/en.json +++ b/src/BootstrapBlazor/Locales/en.json @@ -323,8 +323,7 @@ "BrowserButtonText": "Browser", "FileExtensions": "File must have one of the following extensions: {0}", "FileSizeValidation": "File size must less than {0}", - "DropUploadText": "Drop files here or click to upload", - "DropFooterText": "" + "DropUploadText": "Drop files here or click to upload" }, "BootstrapBlazor.Components.Handwritten": { "SaveButtonText": "Save", diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json index 2997ab392b7..84ba7dc3a27 100644 --- a/src/BootstrapBlazor/Locales/zh.json +++ b/src/BootstrapBlazor/Locales/zh.json @@ -323,8 +323,7 @@ "BrowserButtonText": "浏览", "FileExtensions": "文件扩展名必须为以下几种格式: {0}", "FileSizeValidation": "文件太大,文件限制大小为 {0}", - "DropUploadText": "拖拽文件到此处上传", - "DropFooterText": "" + "DropUploadText": "拖拽文件到此处或者点击上传" }, "BootstrapBlazor.Components.Handwritten": { "SaveButtonText": "保存", diff --git a/src/BootstrapBlazor/wwwroot/modules/upload.js b/src/BootstrapBlazor/wwwroot/modules/upload.js index 006eb250a6b..28334a53267 100644 --- a/src/BootstrapBlazor/wwwroot/modules/upload.js +++ b/src/BootstrapBlazor/wwwroot/modules/upload.js @@ -7,7 +7,8 @@ export function init(id) { return } const preventHandler = e => e.preventDefault() - const upload = { el, preventHandler } + const body = el.querySelector('.upload-drop-body'); + const upload = { el, body, preventHandler } Data.set(id, upload) const inputFile = el.querySelector('[type="file"]') @@ -15,16 +16,25 @@ export function init(id) { inputFile.click() }) - EventHandler.on(el, 'click', '.upload-drop-body', () => { - inputFile.click() - }) - EventHandler.on(document, "dragleave", preventHandler) EventHandler.on(document, 'drop', preventHandler) EventHandler.on(document, 'dragenter', preventHandler) EventHandler.on(document, 'dragover', preventHandler) - EventHandler.on(el, 'drop', e => { + EventHandler.on(body, 'dragenter', e => { + el.classList.add('dropping'); + }) + + EventHandler.on(body, 'dragleave', e => { + el.classList.remove('dropping'); + }); + + EventHandler.on(body, 'drop', e => { + el.classList.remove('dropping'); + + if (el.classList.contains('disabled')) { + return; + } try { const fileList = e.dataTransfer.files if (fileList.length === 0) { @@ -40,6 +50,10 @@ export function init(id) { }) EventHandler.on(el, 'paste', e => { + if (el.classList.contains('disabled')) { + return; + } + inputFile.files = e.clipboardData.files const event = new Event('change', { bubbles: true }) inputFile.dispatchEvent(event) @@ -70,14 +84,18 @@ export function dispose(id) { Data.remove(id) if (upload) { - const { el, preventHandler } = upload; + const { el, body, preventHandler } = upload; - EventHandler.off(el, 'click') - EventHandler.off(el, 'drop') - EventHandler.off(el, 'paste') EventHandler.off(document, 'dragleave', preventHandler) EventHandler.off(document, 'drop', preventHandler) EventHandler.off(document, 'dragenter', preventHandler) EventHandler.off(document, 'dragover', preventHandler) + + EventHandler.off(el, 'click') + EventHandler.off(el, 'drop') + EventHandler.off(el, 'paste') + EventHandler.off(body, 'dragleave') + EventHandler.off(body, 'drop') + EventHandler.off(body, 'dragenter') } } diff --git a/src/BootstrapBlazor/wwwroot/modules/validate.js b/src/BootstrapBlazor/wwwroot/modules/validate.js index f06138ae67a..776a5e1a7e0 100644 --- a/src/BootstrapBlazor/wwwroot/modules/validate.js +++ b/src/BootstrapBlazor/wwwroot/modules/validate.js @@ -37,3 +37,40 @@ export function dispose(id) { } } } + +export function executeUpload(items, invalidItems, addId) { + items.forEach(id => { + const el = document.getElementById(id); + if (el) { + const item = invalidItems.find(i => i.id === id); + if (item) { + const { id, errorMessage } = item; + execute(id, errorMessage); + el.classList.remove('is-valid'); + el.classList.add('is-invalid'); + } + else { + dispose(id); + el.classList.remove('is-invalid'); + el.classList.add('is-valid'); + } + } + }); + + if (addId) { + const el = document.getElementById(addId); + if (el) { + el.classList.remove('is-valid', 'is-invalid'); + dispose(addId); + } + } +} + +export function disposeUpload(items) { + items.forEach(id => { + const el = document.getElementById(id); + if (el) { + dispose(id); + } + }); +} diff --git a/src/BootstrapBlazor/wwwroot/scss/components.scss b/src/BootstrapBlazor/wwwroot/scss/components.scss index cb52a9e2678..da15073286f 100644 --- a/src/BootstrapBlazor/wwwroot/scss/components.scss +++ b/src/BootstrapBlazor/wwwroot/scss/components.scss @@ -72,7 +72,7 @@ @import "../../Components/Pagination/Pagination.razor.scss"; @import "../../Components/Popover/Popover.razor.scss"; @import "../../Components/QueryBuilder/QueryBuilder.razor.scss"; -@import "../../Components/Upload/UploadBase.razor.scss"; +@import "../../Components/Upload/InputUpload.razor.scss"; @import "../../Components/ValidateForm/ValidateForm.razor.scss"; @import "../../Components/Radio/RadioList.razor.scss"; @import "../../Components/Rate/Rate.razor.scss"; diff --git a/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss b/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss index ba4954600d9..90d59186914 100644 --- a/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss +++ b/src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss @@ -689,6 +689,7 @@ $bb-tree-disabled-opacity: .5; // Upload $bb-upload-body-margin-top: 10px; +$bb-upload-body-list-grap: 1rem; $bb-upload-body-list-max-height: 240px; $bb-upload-body-list-item-padding: 3px 5px; $bb-upload-body-list-item-body-padding: 0 5px; @@ -697,11 +698,11 @@ $bb-upload-card-width: 240px; $bb-upload-card-height: 280px; $bb-upload-card-shadow: 0 0 10px 0 rgba(0,0,0,.2); $bb-upload-card-padding: 1rem; -$bb-upload-card-margin: 0 1rem 1rem 0; $bb-upload-card-item-width: 168px; $bb-upload-drop-height: 140px; $bb-upload-drop-footer-font-size: 12px; $bb-upload-drop-footer-margin-top: .25rem; +$bb-upload-item-border-radius: 50%; // ValidateForm $bb-form-control-padding: 0.375rem 0.75rem; diff --git a/test/UnitTest/Attributes/FileValidationAttributeTest.cs b/test/UnitTest/Attributes/FileValidationAttributeTest.cs new file mode 100644 index 00000000000..4be0f49a5cf --- /dev/null +++ b/test/UnitTest/Attributes/FileValidationAttributeTest.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Forms; +using System.ComponentModel.DataAnnotations; + +namespace UnitTest.Attributes; + +public class FileValidationAttributeTest : BootstrapBlazorTestBase +{ + [Fact] + public void FileSize_Ok() + { + var validator = new FileValidationAttribute() + { + FileSize = 5 + }; + var p = new Person() + { + Picture = new MockBrowserFile("test.log") + }; + var result = validator.GetValidationResult(p.Picture, new ValidationContext(p)); + Assert.NotEqual(ValidationResult.Success, result); + } + + [Fact] + public void FileExtensions_Ok() + { + var validator = new FileValidationAttribute() + { + Extensions = ["jpg"] + }; + var p = new Person() + { + Picture = new MockBrowserFile("test.log") + }; + var result = validator.GetValidationResult(p.Picture, new ValidationContext(p)); + Assert.NotEqual(ValidationResult.Success, result); + + result = validator.GetValidationResult(p.Picture, new ValidationContext(p) { MemberName = "Pic" }); + Assert.NotEqual(ValidationResult.Success, result); + } + + [Fact] + public void IsValid_Ok() + { + var validator = new FileValidationAttribute() + { + Extensions = ["jpg"] + }; + var p = new Person() + { + Picture = new MockBrowserFile("test.log") + }; + Assert.False(validator.IsValid(p.Picture)); + } + + [Fact] + public void Validate_Ok() + { + var validator = new FileValidationAttribute() + { + Extensions = ["jpg"] + }; + var p = new Person() + { + Picture = new MockBrowserFile("test.log") + }; + Assert.Throws(() => validator.Validate(p.Picture, "Picture")); + Assert.Throws(() => validator.Validate(p.Picture, new ValidationContext(p))); + } + + private class Person + { + [Required] + [FileValidation(Extensions = [".png", ".jpg", ".jpeg"])] + + public IBrowserFile? Picture { get; set; } + } + + private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text") : IBrowserFile + { + public string Name { get; } = name; + + public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; + + public long Size { get; } = 10; + + public string ContentType { get; } = contentType; + + public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) + { + return new MemoryStream([0x01, 0x02]); + } + } +} diff --git a/test/UnitTest/Components/UploadAvatarTest.cs b/test/UnitTest/Components/UploadAvatarTest.cs new file mode 100644 index 00000000000..6afe95d8472 --- /dev/null +++ b/test/UnitTest/Components/UploadAvatarTest.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Forms; +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; + +namespace UnitTest.Components; + +public class UploadAvatarTest : BootstrapBlazorTestBase +{ + [Fact] + public async Task AvatarUpload_Ok() + { + UploadFile? uploadFile = null; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsMultiple, true); + pb.Add(a => a.OnChange, file => + { + uploadFile = file; + return Task.CompletedTask; + }); + }); + Assert.Contains("upload-item-plus", cut.Markup); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + + // Height/Width + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Height, 40); + pb.Add(a => a.Width, 50); + }); + cut.Contains("width: 50px;"); + cut.Contains("height: 40px;"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsCircle, true); + }); + cut.Contains("height: 50px;"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Height, 0); + }); + cut.Contains("height: 50px;"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.BorderRadius, "10px"); + }); + cut.Contains("--bb-upload-item-border-radius: 10px;"); + + // DefaultFileList + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.OnChange, null); + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "Test-File" } + ]); + }); + input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + + // upload-item-delete + var button = cut.Find(".upload-item-delete"); + await cut.InvokeAsync(() => button.Click()); + + cut.Contains("upload-item-actions btn-browser"); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsDisabled, true); + }); + cut.Contains("upload-item-actions"); + + // IsUploadButtonAtFirst + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsUploadButtonAtFirst, true); + pb.Add(a => a.IsDisabled, false); + pb.Add(a => a.IsMultiple, true); + }); + } + + [Fact] + public async Task AvatarUpload_ValidateForm_Ok() + { + var invalid = false; + var foo = new Foo(); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Accept, "Image"); + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + pb.Add(a => a.AllowExtensions, [".jpg"]); + }); + pb.Add(a => a.OnValidSubmit, context => + { + invalid = false; + return Task.CompletedTask; + }); + pb.Add(a => a.OnInvalidSubmit, context => + { + invalid = true; + return Task.CompletedTask; + }); + }); + + // 提交表单 + var form = cut.Find("form"); + await cut.InvokeAsync(() => form.Submit()); + Assert.True(invalid); + + var input = cut.FindComponent(); + await cut.InvokeAsync(async () => + { + await input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + })); + form.Submit(); + }); + Assert.False(invalid); + + // 设置 Disabled 取消校验 + var upload = cut.FindComponent>(); + upload.SetParametersAndRender(pb => + { + pb.Add(a => a.IsDisabled, true); + }); + + Assert.DoesNotContain("is-invalid", upload.Markup); + + upload.SetParametersAndRender(pb => + { + pb.Add(a => a.IsDisabled, false); + }); + // 清空所有文件 + var items = cut.FindAll(".upload-item-delete"); + Assert.Single(items); + await cut.InvokeAsync(() => items[0].Click()); + form.Submit(); + } + + [Fact] + public async Task DropUpload_ShowProgress_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, async file => + { + await Task.Delay(100); + await file.SaveToFileAsync("1.txt"); + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(() => + { + _ = input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + })); + }); + } + + [Fact] + public void IsImage_Ok() + { + var file = new UploadFile + { + File = new MockBrowserFile("test.text") + }; + Assert.True(file.IsImage([".text"])); + + file.File = new MockBrowserFile("test.jpg", "image/jpeg"); + Assert.True(file.IsImage()); + } + + [Fact] + public async Task ValidateForm_ToggleMessage() + { + bool? invalid = null; + var foo = new Person(); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>>(pb => + { + pb.Add(a => a.IsMultiple, true); + pb.Add(a => a.OnChange, async file => + { + await Task.Delay(10); + }); + pb.Add(a => a.OnDelete, async file => + { + await Task.Delay(1); + return true; + }); + pb.Add(a => a.Value, foo.Picture); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, nameof(Person.Picture), typeof(List))); + }); + pb.Add(a => a.OnValidSubmit, context => + { + invalid = false; + return Task.CompletedTask; + }); + pb.Add(a => a.OnInvalidSubmit, context => + { + invalid = true; + return Task.CompletedTask; + }); + }); + + // 直接提交表单 + var form = cut.Find("form"); + await cut.InvokeAsync(() => form.Submit()); + Assert.True(invalid); + + // 上传合规图片 + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test3.png"), + }))); + form = cut.Find("form"); + await cut.InvokeAsync(() => form.Submit()); + Assert.False(invalid); + + // 上传不合规图片 + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test3.text"), + }))); + form = cut.Find("form"); + await cut.InvokeAsync(() => form.Submit()); + Assert.True(invalid); + + // 删除不合规图片调用 RemoveValidResult 方法 + var items = cut.FindAll(".upload-item-delete"); + Assert.Equal(2, items.Count); + await cut.InvokeAsync(() => items[1].Click()); + } + + private class Person + { + [Required] + [FileValidation(Extensions = [".png", ".jpg", ".jpeg"])] + public List? Picture { get; set; } + } + + private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text") : IBrowserFile + { + public string Name { get; } = name; + + public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; + + public long Size { get; } = 10; + + public string ContentType { get; } = contentType; + + public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) + { + return new MemoryStream([0x01, 0x02]); + } + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_Uploaded")] + static extern void SetUploaded(UploadFile @this, bool v); +} diff --git a/test/UnitTest/Components/UploadButtonTest.cs b/test/UnitTest/Components/UploadButtonTest.cs new file mode 100644 index 00000000000..73394a539d8 --- /dev/null +++ b/test/UnitTest/Components/UploadButtonTest.cs @@ -0,0 +1,434 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using AngleSharp.Dom; +using Microsoft.AspNetCore.Components.Forms; +using System.ComponentModel.DataAnnotations; + +namespace UnitTest.Components; + +public class UploadButtonTest : BootstrapBlazorTestBase +{ + [Fact] + public async Task ButtonUpload_Ok() + { + UploadFile? uploadFile = null; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.BrowserButtonClass, "browser-class"); + pb.Add(a => a.BrowserButtonIcon, "fa-solid fa-chrome"); + pb.Add(a => a.BrowserButtonColor, Color.Success); + }); + cut.Contains("fa-solid fa-chrome"); + cut.Contains("browser-class"); + cut.Contains("btn btn-success"); + cut.DoesNotContain("form-label"); + + // DefaultFileList + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, file => + { + uploadFile = file; + return Task.CompletedTask; + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + cut.DoesNotContain("cancel-icon"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Size, Size.ExtraSmall); + }); + cut.Contains("btn-xs"); + } + + [Fact] + public void ButtonUpload_ChildContent() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ChildContent, builder => builder.AddContent(0, new MarkupString("
test-child-content
"))); + }); + cut.Contains("
test-child-content
"); + } + + [Fact] + public void ButtonUpload_IsDisabled_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsDisabled, true); + }); + var button = cut.Find(".btn-browser"); + Assert.Contains("disabled=\"disabled\"", button.ToMarkup()); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsDisabled, false); + pb.Add(a => a.IsMultiple, true); + }); + Assert.DoesNotContain("disabled=\"disabled\"", button.ToMarkup()); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsDisabled, false); + pb.Add(a => a.IsMultiple, false); + }); + Assert.DoesNotContain("disabled=\"disabled\"", button.ToMarkup()); + } + + [Fact] + public void InputUpload_IsMultiple() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsMultiple, false); + }); + + // 禁用多选功能 + cut.DoesNotContain("multiple=\"multiple\""); + + // 给定已上传文件后上传按钮应该被禁用 + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, + [ + new UploadFile() { FileName = "test1.png" }, + new UploadFile() { FileName = "test2.png" } + ]); + }); + var button = cut.Find(".btn-browser"); + Assert.True(button.IsDisabled()); + + // 开启多选功能 + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsMultiple, true); + }); + cut.Contains("multiple=\"multiple\""); + + // 给定已上传文件后上传按钮不应该被禁用 + button = cut.Find(".btn-browser"); + Assert.False(button.IsDisabled()); + } + + [Fact] + public async Task ButtonUpload_ValidateForm_Ok() + { + var foo = new Foo(); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + }); + }); + cut.Contains("form-label"); + + // ValidateId 为空情况 + var uploader = cut.FindComponent>(); + var pi = typeof(ButtonUpload).GetProperty("UploadFiles", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + Assert.NotNull(pi); + var filesValue = pi.GetValue(uploader.Instance); + + if (filesValue is List fs) + { + fs.Add(new UploadFile()); + } + + var results = new List() + { + new("test", ["bb_validate_123"]) + }; + await cut.InvokeAsync(() => uploader.Instance.ToggleMessage(results)); + } + + [Fact] + public async Task ButtonUpload_ShowDownload() + { + var clicked = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowDownloadButton, true); + pb.Add(a => a.OnDownload, file => + { + clicked = true; + return Task.CompletedTask; + }); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "Test-File1.text" } + ]); + }); + + var button = cut.Find(".fa-download"); + await cut.InvokeAsync(() => button.Click()); + Assert.True(clicked); + } + + [Fact] + public async Task ButtonUpload_Validate_Ok() + { + var invalid = true; + var foo = new Foo + { + Name = "abc" + }; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Accept, "Image"); + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + pb.Add(a => a.ShowUploadFileList, true); + }); + pb.Add(a => a.OnValidSubmit, context => + { + invalid = false; + return Task.CompletedTask; + }); + }); + + // 由于设置了属性 Name 值 Validate 方法通过 + var form = cut.Find("form"); + await cut.InvokeAsync(() => form.Submit()); + Assert.False(invalid); + } + + [Fact] + public void ShowUploadList_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Accept, "Image"); + }); + + cut.Contains("upload-body is-list"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowUploadFileList, false); + }); + cut.WaitForState(() => !cut.Markup.Contains("upload-body is-list")); + cut.DoesNotContain("upload-body is-list"); + } + + [Fact] + public async Task ButtonUpload_OnDeleteFile_Ok() + { + UploadFile? deleteFile = null; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsMultiple, true); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "Test-File" } + ]); + pb.Add(a => a.OnDelete, file => + { + deleteFile = file; + return Task.FromResult(true); + }); + }); + await cut.InvokeAsync(() => cut.Find(".delete-icon").Click()); + Assert.NotNull(deleteFile); + Assert.Null(deleteFile!.Error); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, null); + }); + // 增加代码覆盖率 + var ins = cut.Instance; + var pi = ins.GetType().GetMethod("OnFileDelete", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!; + pi.Invoke(ins, [new UploadFile()]); + + deleteFile = null; + // 上传失败测试 + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "Test-File2", Code = 1001, Error = "Error" } + ]); + }); + await cut.InvokeAsync(() => cut.Find(".delete-icon").Click()); + Assert.NotNull(deleteFile); + } + + [Fact] + public async Task ButtonUpload_ShowProgress_Ok() + { + var cancel = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, async file => + { + await Task.Delay(100); + await file.SaveToFileAsync("1.txt"); + }); + pb.Add(a => a.OnCancel, file => + { + cancel = true; + return Task.CompletedTask; + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(async () => + { + _ = input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + })); + + var button = cut.Find(".cancel-icon"); + Assert.NotNull(button); + await cut.InvokeAsync(() => button.Click()); + Assert.True(cancel); + }); + } + + [Fact] + public async Task ButtonUpload_IsDirectory_Ok() + { + var fileCount = 0; + var fileNames = new List(); + List fileList = []; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsDirectory, true); + pb.Add(a => a.OnChange, file => + { + fileCount = file.FileCount; + fileNames.Add(file.OriginFileName!); + return Task.CompletedTask; + }); + pb.Add(a => a.OnAllFileUploaded, files => + { + fileList.AddRange(files); + return Task.CompletedTask; + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new(), + new("UploadTestFile2") + }))); + Assert.Equal(2, fileCount); + Assert.Equal(2, fileNames.Count); + Assert.Equal(2, fileList.Count); + } + + [Fact] + public void ButtonUpload_Accept_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Accept, ".jpg"); + }); + cut.Contains("accept=\".jpg\""); + } + + [Fact] + public void ButtonUpload_OnGetFileFormat_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.LoadingIcon, "fa-loading"); + pb.Add(a => a.DeleteIcon, "fa-delte"); + pb.Add(a => a.CancelIcon, "fa-cancel"); + pb.Add(a => a.DownloadIcon, "fa-download"); + pb.Add(a => a.InvalidStatusIcon, "fa-invalid"); + pb.Add(a => a.ValidStatusIcon, "fa-valid"); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "1.csv" }, + new() { FileName = "1.xls" }, + new() { FileName = "1.xlsx" }, + new() { FileName = "1.doc" }, + new() { FileName = "1.docx" }, + new() { FileName = "1.dot" }, + new() { FileName = "1.ppt" }, + new() { FileName = "1.pptx" }, + new() { FileName = "1.wav" }, + new() { FileName = "1.mp3" }, + new() { FileName = "1.mp4" }, + new() { FileName = "1.mov" }, + new() { FileName = "1.mkv" }, + new() { FileName = "1.cs" }, + new() { FileName = "1.html" }, + new() { FileName = "1.vb" }, + new() { FileName = "1.pdf" }, + new() { FileName = "1.zip" }, + new() { FileName = "1.rar" }, + new() { FileName = "1.iso" }, + new() { FileName = "1.txt" }, + new() { FileName = "1.log" }, + new() { FileName = "1.jpg" }, + new() { FileName = "1.jpeg" }, + new() { FileName = "1.png" }, + new() { FileName = "1.bmp" }, + new() { FileName = "1.gif" }, + new() { FileName = "1.test" }, + new() { FileName = "1" } + ]); + }); + cut.Contains("fa-file-excel"); + cut.Contains("fa-file-word"); + cut.Contains("fa-file-powerpoint"); + cut.Contains("fa-file-audio"); + cut.Contains("fa-file-video"); + cut.Contains("fa-file-code"); + cut.Contains("fa-file-pdf"); + cut.Contains("fa-file-archive"); + cut.Contains("fa-file-text"); + cut.Contains("fa-file-image"); + cut.Contains("fa-file-archive"); + cut.Contains("fa-file"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.OnGetFileFormat, extensions => + { + return "fa-format-test"; + }); + }); + cut.Contains("fa-format-test"); + + // Empty Items + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, null); + }); + } + + private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text") : IBrowserFile + { + public string Name { get; } = name; + + public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; + + public long Size { get; } = 10; + + public string ContentType { get; } = contentType; + + public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) + { + return new MemoryStream([0x01, 0x02]); + } + } +} diff --git a/test/UnitTest/Components/UploadCardTest.cs b/test/UnitTest/Components/UploadCardTest.cs new file mode 100644 index 00000000000..5cf60a2142c --- /dev/null +++ b/test/UnitTest/Components/UploadCardTest.cs @@ -0,0 +1,241 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Forms; + +namespace UnitTest.Components; + +public class UploadCardTest : BootstrapBlazorTestBase +{ + [Fact] + public async Task CardUpload_Ok() + { + var zoom = false; + var deleted = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowZoomButton, true); + pb.Add(a => a.ShowDeleteButton, true); + pb.Add(a => a.OnDelete, file => + { + deleted = true; + return Task.FromResult(true); + }); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "Test-File1.text" }, + new() { FileName = "Test-File2.jpg" }, + new() { PrevUrl = "Test-File3.png" }, + new() { PrevUrl = "Test-File4.bmp" }, + new() { PrevUrl = "Test-File5.jpeg" }, + new() { PrevUrl = "Test-File6.gif" }, + new() { PrevUrl = "data:image/png;base64,iVBORw0KGgoAAAANS=" }, + new() { FileName = null! } + ]); + }); + cut.Contains("bb-previewer collapse active"); + cut.Contains("aria-label=\"zoom\""); + cut.Contains("aria-label=\"delete\""); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IconTemplate, file => builder => + { + builder.AddContent(0, "custom-file-icon-template"); + }); + }); + cut.Contains("custom-file-icon-template"); + + // OnZoom + await cut.InvokeAsync(() => cut.Find(".btn-zoom").Click()); + Assert.False(zoom); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.OnZoomAsync, file => + { + zoom = true; + return Task.CompletedTask; + }); + }); + await cut.InvokeAsync(() => cut.Find(".btn-zoom").Click()); + Assert.True(zoom); + + zoom = false; + await cut.InvokeAsync(() => cut.Find(".upload-item-body-image").Click()); + Assert.True(zoom); + + // ShowDownload + var clicked = false; + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowDownloadButton, true); + pb.Add(a => a.OnDownload, file => + { + clicked = true; + return Task.CompletedTask; + }); + + }); + Assert.Contains("btn-download", cut.Markup); + var button = cut.Find(".btn-download"); + await cut.InvokeAsync(() => button.Click()); + Assert.True(clicked); + + // OnDelete + await cut.InvokeAsync(() => cut.Find(".btn-outline-danger").Click()); + Assert.True(deleted); + + // CanPreviewCallback + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.CanPreviewCallback, p => + { + return false; + }); + }); + await cut.InvokeAsync(() => cut.Find(".btn-zoom").Click()); + + // ShowProgress + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, async file => + { + await file.SaveToFileAsync("1.txt"); + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test.txt", "Image-Png") + }))); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test.png") + }))); + + // IsUploadButtonAtFirst + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsUploadButtonAtFirst, true); + pb.Add(a => a.IsMultiple, true); + pb.Add(a => a.ShowZoomButton, false); + pb.Add(a => a.ShowDeleteButton, false); + }); + cut.DoesNotContain("aria-label=\"zoom\""); + cut.DoesNotContain("aria-label=\"delete\""); + } + + [Fact] + public void CardUpload_ValidateForm_Ok() + { + var foo = new Foo(); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + }); + }); + cut.Contains("form-label"); + } + + [Fact] + public void AllowExtensions_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.AllowExtensions, [".dba"]); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "test.dba" } + ]); + }); + cut.Contains("test.dba (0 B)"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, + [ + new() { File = new MockBrowserFile("demo.dba") } + ]); + }); + cut.Contains("demo.dba (0 B)"); + } + + [Fact] + public async Task CardUpload_ShowProgress_Ok() + { + var cancel = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, async file => + { + await Task.Delay(100); + await file.SaveToFileAsync("1.txt"); + }); + pb.Add(a => a.OnCancel, file => + { + cancel = true; + return Task.CompletedTask; + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(() => + { + input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + })); + var button = cut.Find(".btn-cancel"); + Assert.NotNull(button); + button.Click(); + }); + Assert.True(cancel); + } + + [Fact] + public async Task ShowDeleteButton_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowDeleteButton, true); + pb.Add(a => a.IsDisabled, true); + }); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test3.png", delay: TimeSpan.FromMilliseconds(300)), + }))); + + var btn = cut.Find(".btn-outline-danger"); + btn.InnerHtml.Contains("disabled=\"disabled\""); + } + + private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text", TimeSpan? delay = null) : IBrowserFile + { + public string Name { get; } = name; + + public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; + + public long Size { get; } = 10; + + public string ContentType { get; } = contentType; + + public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) + { + if (delay != null) + { + Thread.Sleep(delay.Value.Milliseconds); + } + return new MemoryStream([0x01, 0x02]); + } + } +} diff --git a/test/UnitTest/Components/UploadDropTest.cs b/test/UnitTest/Components/UploadDropTest.cs new file mode 100644 index 00000000000..bd06fb28ce7 --- /dev/null +++ b/test/UnitTest/Components/UploadDropTest.cs @@ -0,0 +1,205 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Forms; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; + +namespace UnitTest.Components; + +public class UploadDropTest : BootstrapBlazorTestBase +{ + [Fact] + public void DropUpload_BodyTemplate_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.BodyTemplate, b => b.AddContent(0, "drop-upload-body-template")); + }); + cut.MarkupMatches("
drop-upload-body-template
    "); + } + + [Fact] + public void DropUpload_IconTemplate_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.IconTemplate, b => b.AddContent(0, "drop-upload-icon-template")); + }); + cut.Contains("
    drop-upload-icon-template
    "); + } + + [Fact] + public void DropUpload_TextTemplate_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.TextTemplate, b => b.AddContent(0, "drop-upload-text-template")); + }); + cut.Contains("
    drop-upload-text-template
    "); + } + + [Fact] + public void DropUpload_Footer_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.ShowFooter, true); + pb.Add(a => a.FooterText, "drop-upload-footer-text1"); + }); + cut.Contains("
    drop-upload-footer-text1
    "); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.FooterTemplate, b => b.AddContent(0, "drop-upload-footer-text")); + }); + cut.Contains("
    drop-upload-footer-text
    "); + } + + [Fact] + public async Task MaxFileCount_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.IsMultiple, true); + pb.Add(a => a.MaxFileCount, 2); + }); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test1.png"), + new("test2.png") + }))); + cut.Contains("test1.png"); + cut.Contains("test2.png"); + + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test3.png") + }))); + cut.DoesNotContain("test3.png"); + } + + [Fact] + public async Task DropUpload_OnChanged_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.ShowLabel, true); + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, async files => + { + await files.SaveToFileAsync("1.text"); + }); + }); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + } + + [Fact] + public async Task ShowUploadList_Ok() + { + UploadFile? file = null; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.ShowUploadFileList, true); + pb.Add(a => a.ShowDownloadButton, true); + pb.Add(a => a.OnDownload, f => + { + file = f; + return Task.CompletedTask; + }); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "Test-File1.text" }, + new() { FileName = "Test-File2.jpg" }, + ]); + }); + cut.Contains("upload-body is-list"); + + var button = cut.Find(".download-icon"); + await cut.InvokeAsync(() => button.Click()); + Assert.NotNull(file); + } + + [Fact] + public async Task DropUpload_ShowProgress_Ok() + { + var cancel = false; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.ShowProgress, true); + pb.Add(a => a.OnChange, async file => + { + await Task.Delay(100); + await file.SaveToFileAsync("1.txt"); + }); + pb.Add(a => a.OnCancel, file => + { + cancel = true; + return Task.CompletedTask; + }); + }); + var input = cut.FindComponent(); + await cut.InvokeAsync(async () => + { + _ = input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + })); + + var button = cut.Find(".cancel-icon"); + Assert.NotNull(button); + await cut.InvokeAsync(() => button.Click()); + Assert.True(cancel); + }); + } + + + [Fact] + public void OnGetFileFormat_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.LoadingIcon, "fa-loading"); + pb.Add(a => a.DeleteIcon, "fa-delte"); + pb.Add(a => a.CancelIcon, "fa-cancel"); + pb.Add(a => a.DownloadIcon, "fa-download"); + pb.Add(a => a.InvalidStatusIcon, "fa-invalid"); + pb.Add(a => a.ValidStatusIcon, "fa-valid"); + pb.Add(a => a.ShowUploadFileList, true); + pb.Add(a => a.OnGetFileFormat, extensions => + { + return "fa-format-test"; + }); + pb.Add(a => a.DefaultFileList, + [ + new() { FileName = "1.csv" } + ]); + }); + cut.Contains("fa-format-test"); + } + + private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text") : IBrowserFile + { + public string Name { get; } = name; + + public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; + + public long Size { get; } = 10; + + public string ContentType { get; } = contentType; + + public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) + { + return new MemoryStream([0x01, 0x02]); + } + } +} diff --git a/test/UnitTest/Components/UploadInputTest.cs b/test/UnitTest/Components/UploadInputTest.cs new file mode 100644 index 00000000000..0d1e83d1f54 --- /dev/null +++ b/test/UnitTest/Components/UploadInputTest.cs @@ -0,0 +1,269 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using AngleSharp.Dom; +using Microsoft.AspNetCore.Components.Forms; +using System.ComponentModel.DataAnnotations; + +namespace UnitTest.Components; + +public class UploadInputTest : BootstrapBlazorTestBase +{ + [Fact] + public async Task InputUpload_Ok() + { + UploadFile? uploadFile = null; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Capture, "capture"); + pb.Add(a => a.PlaceHolder, "TestPlaceHolder"); + pb.Add(a => a.OnChange, file => + { + uploadFile = file; + return Task.CompletedTask; + }); + pb.Add(a => a.Value, "test.jpg"); + }); + cut.Contains("value=\"test.jpg\""); + cut.Contains("capture=\"capture\""); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + Assert.Equal("UploadTestFile", uploadFile!.OriginFileName); + cut.Contains("fa-regular fa-folder-open"); + cut.Contains("btn-primary"); + cut.Contains("TestPlaceHolder"); + + // 参数 + cut.SetParametersAndRender(pb => pb.Add(a => a.BrowserButtonIcon, "fa-solid fa-chrome")); + cut.Contains("fa-solid fa-chrome"); + + cut.SetParametersAndRender(pb => pb.Add(a => a.BrowserButtonClass, "btn btn-browser")); + cut.Contains("btn btn-browser"); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowDeleteButton, true); + pb.Add(a => a.DeleteButtonText, "Delete-Test"); + pb.Add(a => a.DeleteButtonIcon, "fa-solid fa-trash"); + }); + cut.WaitForAssertion(() => cut.Contains("fa-solid fa-trash")); + cut.Contains("btn-danger"); + + // 删除逻辑 + var deleted = false; + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DeleteButtonClass, "btn btn-delete"); + pb.Add(a => a.OnDelete, file => + { + deleted = true; + return Task.FromResult(true); + }); + }); + cut.WaitForAssertion(() => cut.Contains("btn btn-delete")); + + var button = cut.Find(".input-group button"); + await cut.InvokeAsync(() => button.Click()); + Assert.True(deleted); + + // IsDisable + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsDisabled, true); + }); + cut.WaitForAssertion(() => cut.Contains("btn btn-delete")); + } + + [Fact] + public async Task InputUpload_ValidateForm_Ok() + { + var invalid = false; + var foo = new Foo(); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + }); + pb.Add(a => a.OnValidSubmit, context => + { + invalid = false; + return Task.CompletedTask; + }); + pb.Add(a => a.OnInvalidSubmit, context => + { + invalid = true; + return Task.CompletedTask; + }); + }); + + // 提交表单 + var form = cut.Find("form"); + await cut.InvokeAsync(() => form.Submit()); + Assert.True(invalid); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + await cut.InvokeAsync(() => form.Submit()); + Assert.False(invalid); + } + + [Fact] + public void InputUpload_FileValidate_OK() + { + var foo = new Person(); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Picture); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, nameof(Person.Picture), typeof(IBrowserFile))); + }); + }); + + var input = cut.FindComponent(); + cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new() + }))); + + // 提交表单 + var form = cut.Find("form"); + cut.InvokeAsync(() => form.Submit()); + } + + [Fact] + public async Task InputUpload_Value() + { + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Value, + [ + "test1.png", + "test2.png" + ]); + }); + Assert.Contains("test1.png;test2.png", cut.Markup); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test3.png"), + new("test4.png") + }))); + Assert.Contains("test3.png;test4.png", cut.Markup); + } + + [Fact] + public async Task InputUpload_Files() + { + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.Value, + [ + new MockBrowserFile("test1.png"), + new MockBrowserFile("test2.png") + ]); + }); + Assert.Contains("test1.png;test2.png", cut.Markup); + + var input = cut.FindComponent(); + await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() + { + new("test3.png"), + new("test4.png") + }))); + Assert.Contains("test3.png;test4.png", cut.Markup); + + // 重置后不应该包含新上传的文件 + await cut.InvokeAsync(() => cut.Instance.Reset()); + Assert.DoesNotContain("test3.png;test4.png", cut.Markup); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, + [ + new UploadFile() { FileName = "test5.png" }, + new UploadFile() { FileName = "test6.png" } + ]); + pb.Add(a => a.Value, + [ + new MockBrowserFile("test5.png"), + new MockBrowserFile("test6.png") + ]); + }); + Assert.Contains("test5.png;test6.png", cut.Markup); + await cut.InvokeAsync(() => cut.Instance.Reset()); + Assert.DoesNotContain("test5.png;test6.png", cut.Markup); + } + + [Fact] + public void InputUpload_IsMultiple() + { + var cut = Context.RenderComponent>>(pb => + { + pb.Add(a => a.IsMultiple, false); + }); + + // 禁用多选功能 + cut.DoesNotContain("multiple=\"multiple\""); + + // 给定已上传文件后上传按钮应该被禁用 + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.DefaultFileList, + [ + new UploadFile() { FileName = "test1.png" }, + new UploadFile() { FileName = "test2.png" } + ]); + }); + var button = cut.Find(".btn-browser"); + Assert.True(button.IsDisabled()); + + // 开启多选功能 + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsMultiple, true); + }); + cut.Contains("multiple=\"multiple\""); + + // 给定已上传文件后上传按钮不应该被禁用 + button = cut.Find(".btn-browser"); + Assert.False(button.IsDisabled()); + } + + private class Person + { + [Required] + [FileValidation(Extensions = [".png", ".jpg", ".jpeg"])] + public IBrowserFile? Picture { get; set; } + } + + private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text") : IBrowserFile + { + public string Name { get; } = name; + + public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; + + public long Size { get; } = 10; + + public string ContentType { get; } = contentType; + + public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) + { + return new MemoryStream([0x01, 0x02]); + } + } +} diff --git a/test/UnitTest/Components/UploadTest.cs b/test/UnitTest/Components/UploadTest.cs deleted file mode 100644 index cbe006eb510..00000000000 --- a/test/UnitTest/Components/UploadTest.cs +++ /dev/null @@ -1,1126 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License -// See the LICENSE file in the project root for more information. -// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone - -using Microsoft.AspNetCore.Components.Forms; -using System.ComponentModel.DataAnnotations; - -namespace UnitTest.Components; - -public class UploadTest : BootstrapBlazorTestBase -{ - [Fact] - public async Task InputUpload_Ok() - { - UploadFile? uploadFile = null; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.PlaceHolder, "TestPlaceHolder"); - pb.Add(a => a.OnChange, file => - { - uploadFile = file; - return Task.CompletedTask; - }); - pb.Add(a => a.Value, "test.jpg"); - }); - cut.Contains("value=\"test.jpg\""); - - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - Assert.Equal("UploadTestFile", uploadFile!.OriginFileName); - cut.Contains("fa-regular fa-folder-open"); - cut.Contains("btn-primary"); - cut.Contains("TestPlaceHolder"); - - // 参数 - cut.SetParametersAndRender(pb => pb.Add(a => a.BrowserButtonIcon, "fa-solid fa-chrome")); - cut.Contains("fa-solid fa-chrome"); - - cut.SetParametersAndRender(pb => pb.Add(a => a.BrowserButtonClass, "btn btn-browser")); - cut.Contains("btn btn-browser"); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowDeleteButton, true); - pb.Add(a => a.DeleteButtonText, "Delete-Test"); - pb.Add(a => a.DeleteButtonIcon, "fa-solid fa-trash"); - }); - cut.WaitForAssertion(() => cut.Contains("fa-solid fa-trash")); - cut.Contains("btn-danger"); - - // 删除逻辑 - var deleted = false; - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.DeleteButtonClass, "btn btn-delete"); - pb.Add(a => a.OnDelete, file => - { - deleted = true; - return Task.FromResult(true); - }); - }); - cut.WaitForAssertion(() => cut.Contains("btn btn-delete")); - - var button = cut.Find(".input-group button"); - await cut.InvokeAsync(() => button.Click()); - Assert.True(deleted); - - // IsDisable - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsDisabled, true); - }); - cut.WaitForAssertion(() => cut.Contains("btn btn-delete")); - } - - [Fact] - public async Task InputUpload_ValidateForm_Ok() - { - var invalid = false; - var foo = new Foo(); - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Value, foo.Name); - pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); - }); - pb.Add(a => a.OnValidSubmit, context => - { - invalid = false; - return Task.CompletedTask; - }); - pb.Add(a => a.OnInvalidSubmit, context => - { - invalid = true; - return Task.CompletedTask; - }); - }); - - // 提交表单 - var form = cut.Find("form"); - await cut.InvokeAsync(() => form.Submit()); - Assert.True(invalid); - - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - await cut.InvokeAsync(() => form.Submit()); - Assert.False(invalid); - } - - [Fact] - public void InputUpload_FileValidate_OK() - { - var foo = new Person(); - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Value, foo.Picture); - pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, nameof(Person.Picture), typeof(IBrowserFile))); - }); - }); - - var input = cut.FindComponent(); - cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - - // 提交表单 - var form = cut.Find("form"); - cut.InvokeAsync(() => form.Submit()); - } - - [Fact] - public void AvatarUpload_Ok() - { - UploadFile? uploadFile = null; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsSingle, true); - pb.Add(a => a.OnChange, file => - { - uploadFile = file; - return Task.CompletedTask; - }); - }); - var input = cut.FindComponent(); - cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - - // Height/Width - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.Height, 40); - pb.Add(a => a.Width, 50); - }); - cut.WaitForAssertion(() => cut.Contains("width: 50px;")); - cut.WaitForAssertion(() => cut.Contains("height: 40px;")); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsCircle, true); - }); - cut.WaitForAssertion(() => cut.Contains("height: 50px;")); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.Height, 0); - }); - cut.WaitForAssertion(() => cut.Contains("height: 50px;")); - - // DefaultFileList - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.OnChange, null); - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File" } - ]); - }); - input = cut.FindComponent(); - cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - - // upload-item-delete - var button = cut.Find(".upload-item-delete"); - cut.InvokeAsync(() => button.Click()); - - // isdisable - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsDisabled, true); - }); - - // IsUploadButtonAtFirst - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsUploadButtonAtFirst, true); - }); - } - - [Fact] - public void AvatarUpload_Value_Ok() - { - var cut = Context.RenderComponent>(pb => pb.Add(a => a.IsSingle, true)); - var input = cut.FindComponent(); - cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - cut.WaitForAssertion(() => Assert.Equal("UploadTestFile", cut.Instance.Value!.Name)); - } - - [Fact] - public async Task AvatarUpload_ListValue_Ok() - { - var cut = Context.RenderComponent>>(); - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - Assert.Single(cut.Instance.Value); - } - - [Fact] - public async Task AvatarUpload_ValidateForm_Ok() - { - var invalid = false; - var foo = new Foo(); - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Accept, "Image"); - pb.Add(a => a.Value, foo.Name); - pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); - }); - pb.Add(a => a.OnValidSubmit, context => - { - invalid = false; - return Task.CompletedTask; - }); - pb.Add(a => a.OnInvalidSubmit, context => - { - invalid = true; - return Task.CompletedTask; - }); - }); - - // 提交表单 - var form = cut.Find("form"); - await cut.InvokeAsync(() => form.Submit()); - Assert.True(invalid); - - var input = cut.FindComponent(); - await cut.InvokeAsync(async () => - { - await input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - })); - form.Submit(); - }); - Assert.False(invalid); - } - - [Fact] - public async Task AvatarUpload_Validate_Ok() - { - var invalid = true; - var foo = new Foo - { - Name = "abc" - }; - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Accept, "Image"); - pb.Add(a => a.Value, foo.Name); - pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); - }); - pb.Add(a => a.OnValidSubmit, context => - { - invalid = false; - return Task.CompletedTask; - }); - }); - - // 由于设置了属性 Name 值 Validate 方法通过 - var form = cut.Find("form"); - await cut.InvokeAsync(() => form.Submit()); - Assert.False(invalid); - } - - [Fact] - public void AvatarUpload_FileValidate_Ok() - { - var foo = new Person(); - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Value, foo.Picture); - pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, nameof(Person.Picture), typeof(IBrowserFile))); - }); - }); - - var input = cut.FindComponent(); - cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - - // 提交表单 - var form = cut.Find("form"); - cut.InvokeAsync(() => form.Submit()); - } - - [Fact] - public async Task ButtonUpload_Ok() - { - UploadFile? uploadFile = null; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsSingle, true); - pb.Add(a => a.BrowserButtonClass, "browser-class"); - pb.Add(a => a.BrowserButtonIcon, "fa-solid fa-chrome"); - pb.Add(a => a.BrowserButtonColor, Color.Success); - }); - cut.Contains("fa-solid fa-chrome"); - cut.Contains("browser-class"); - cut.Contains("btn btn-success"); - cut.DoesNotContain("form-label"); - - // DefaultFileList - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.OnChange, file => - { - uploadFile = file; - return Task.CompletedTask; - }); - }); - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - cut.DoesNotContain("cancel-icon"); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.Size, Size.ExtraSmall); - }); - cut.Contains("btn-xs"); - } - - [Fact] - public void ButtonUpload_ChildContent() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ChildContent, builder => builder.AddContent(0, new MarkupString("
    test-child-content
    "))); - }); - cut.Contains("
    test-child-content
    "); - } - - [Fact] - public void ButtonUpload_IsDisabled_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsDisabled, true); - }); - var button = cut.Find(".btn-browser"); - Assert.Contains("disabled=\"disabled\"", button.ToMarkup()); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsDisabled, false); - pb.Add(a => a.IsSingle, false); - }); - Assert.DoesNotContain("disabled=\"disabled\"", button.ToMarkup()); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsDisabled, false); - pb.Add(a => a.IsSingle, true); - }); - Assert.DoesNotContain("disabled=\"disabled\"", button.ToMarkup()); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsDisabled, false); - pb.Add(a => a.IsSingle, true); - }); - var input = cut.FindComponent(); - cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - } - - private static readonly string[] memberNames = ["bb_validate_123"]; - - [Fact] - public void ButtonUpload_ValidateForm_Ok() - { - var foo = new Foo(); - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Value, foo.Name); - pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); - }); - }); - cut.Contains("form-label"); - - // ValidateId 为空情况 - var uploader = cut.FindComponent>(); - var pi = typeof(ButtonUpload).GetProperty("UploadFiles", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.NotNull(pi); - var filesValue = pi.GetValue(uploader.Instance); - - if (filesValue is List fs) - { - fs.Add(new UploadFile()); - } - var results = new List() - { - new("test", memberNames) - }; - uploader.Instance.ToggleMessage(results); - } - - [Fact] - public async Task ButtonUpload_ShowDownload() - { - var clicked = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowDownloadButton, true); - pb.Add(a => a.OnDownload, file => - { - clicked = true; - return Task.CompletedTask; - }); - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File1.text" } - ]); - }); - - var button = cut.Find(".fa-download"); - await cut.InvokeAsync(() => button.Click()); - Assert.True(clicked); - } - - [Fact] - public async Task ButtonUpload_Validate_Ok() - { - var invalid = true; - var foo = new Foo - { - Name = "abc" - }; - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Accept, "Image"); - pb.Add(a => a.Value, foo.Name); - pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); - }); - pb.Add(a => a.OnValidSubmit, context => - { - invalid = false; - return Task.CompletedTask; - }); - }); - - // 由于设置了属性 Name 值 Validate 方法通过 - var form = cut.Find("form"); - await cut.InvokeAsync(() => form.Submit()); - Assert.False(invalid); - } - - [Fact] - public void ShowUploadList_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Accept, "Image"); - }); - - cut.Contains("upload-body is-list"); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowUploadFileList, false); - }); - cut.WaitForState(() => !cut.Markup.Contains("upload-body is-list")); - cut.DoesNotContain("upload-body is-list"); - } - - [Fact] - public async Task ButtonUpload_OnDeleteFile_Ok() - { - UploadFile? deleteFile = null; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsSingle, false); - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File" } - ]); - pb.Add(a => a.OnDelete, file => - { - deleteFile = file; - return Task.FromResult(true); - }); - }); - await cut.InvokeAsync(() => cut.Find(".delete-icon").Click()); - Assert.NotNull(deleteFile); - Assert.Null(deleteFile!.Error); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.DefaultFileList, null); - }); - // 增加代码覆盖率 - var ins = cut.Instance; - var pi = ins.GetType().GetMethod("OnFileDelete", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!; - pi.Invoke(ins, [new UploadFile()]); - - deleteFile = null; - // 上传失败测试 - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File2", Code = 1001 } - ]); - }); - await cut.InvokeAsync(() => cut.Find(".delete-icon").Click()); - Assert.NotNull(deleteFile); - } - - [Fact] - public async Task ButtonUpload_ShowProgress_Ok() - { - var cancel = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.OnChange, async file => - { - await Task.Delay(100); - await file.SaveToFileAsync("1.txt"); - }); - pb.Add(a => a.OnCancel, file => - { - cancel = true; - return Task.CompletedTask; - }); - }); - var input = cut.FindComponent(); - await cut.InvokeAsync(async () => - { - _ = input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - })); - - var button = cut.Find(".cancel-icon"); - Assert.NotNull(button); - await cut.InvokeAsync(() => button.Click()); - Assert.True(cancel); - }); - } - - [Fact] - public async Task ButtonUpload_IsDirectory_Ok() - { - var fileCount = 0; - var fileNames = new List(); - List fileList = []; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsDirectory, true); - pb.Add(a => a.OnChange, file => - { - fileCount = file.FileCount; - fileNames.Add(file.OriginFileName!); - return Task.CompletedTask; - }); - pb.Add(a => a.OnAllFileUploaded, files => - { - fileList.AddRange(files); - return Task.CompletedTask; - }); - }); - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new(), - new("UploadTestFile2") - }))); - Assert.Equal(2, fileCount); - Assert.Equal(2, fileNames.Count); - Assert.Equal(2, fileList.Count); - } - - [Fact] - public void ButtonUpload_Accept_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Accept, ".jpg"); - }); - cut.Contains("accept=\".jpg\""); - } - - [Fact] - public void ButtonUpload_OnGetFileFormat_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "1.csv" }, - new() { FileName = "1.xls" }, - new() { FileName = "1.xlsx" }, - new() { FileName = "1.doc" }, - new() { FileName = "1.docx" }, - new() { FileName = "1.dot" }, - new() { FileName = "1.ppt" }, - new() { FileName = "1.pptx" }, - new() { FileName = "1.wav" }, - new() { FileName = "1.mp3" }, - new() { FileName = "1.mp4" }, - new() { FileName = "1.mov" }, - new() { FileName = "1.mkv" }, - new() { FileName = "1.cs" }, - new() { FileName = "1.html" }, - new() { FileName = "1.vb" }, - new() { FileName = "1.pdf" }, - new() { FileName = "1.zip" }, - new() { FileName = "1.rar" }, - new() { FileName = "1.iso" }, - new() { FileName = "1.txt" }, - new() { FileName = "1.log" }, - new() { FileName = "1.jpg" }, - new() { FileName = "1.jpeg" }, - new() { FileName = "1.png" }, - new() { FileName = "1.bmp" }, - new() { FileName = "1.gif" }, - new() { FileName = "1.test" }, - new() { FileName = "1" } - ]); - }); - cut.Contains("fa-regular fa-file-excel"); - cut.Contains("fa-regular fa-file-word"); - cut.Contains("fa-regular fa-file-powerpoint"); - cut.Contains("fa-regular fa-file-audio"); - cut.Contains("fa-regular fa-file-video"); - cut.Contains("fa-regular fa-file-code"); - cut.Contains("fa-regular fa-file-pdf"); - cut.Contains("fa-regular fa-file-archive"); - cut.Contains("fa-regular fa-file-text"); - cut.Contains("fa-regular fa-file-image"); - cut.Contains("fa-regular fa-file"); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.OnGetFileFormat, extensions => - { - return "fa-format-test"; - }); - }); - cut.Contains("fa-format-test"); - } - - [Fact] - public async Task CardUpload_Ok() - { - var zoom = false; - var deleted = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.OnDelete, file => - { - deleted = true; - return Task.FromResult(true); - }); - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File1.text" }, - new() { FileName = "Test-File2.jpg" }, - new() { PrevUrl = "Test-File3.png" }, - new() { PrevUrl = "Test-File4.bmp" }, - new() { PrevUrl = "Test-File5.jpeg" }, - new() { PrevUrl = "Test-File6.gif" }, - new() { PrevUrl = "data:image/png;base64,iVBORw0KGgoAAAANS=" }, - new() { FileName = null! } - ]); - }); - cut.Contains("bb-previewer collapse active"); - - // OnZoom - await cut.InvokeAsync(() => cut.Find(".btn-zoom").Click()); - Assert.False(zoom); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.OnZoomAsync, file => - { - zoom = true; - return Task.CompletedTask; - }); - }); - await cut.InvokeAsync(() => cut.Find(".btn-zoom").Click()); - Assert.True(zoom); - - zoom = false; - await cut.InvokeAsync(() => cut.Find(".upload-item-body-image").Click()); - Assert.True(zoom); - - // OnDelete - await cut.InvokeAsync(() => cut.Find(".btn-outline-danger").Click()); - Assert.True(deleted); - - // CanPreviewCallback - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.CanPreviewCallback, p => - { - return false; - }); - }); - await cut.InvokeAsync(() => cut.Find(".btn-zoom").Click()); - - // ShowProgress - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.OnChange, async file => - { - await file.SaveToFileAsync("1.txt"); - }); - }); - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new("test.txt", "Image-Png") - }))); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new("test.png") - }))); - - // IsUploadButtonAtFirst - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsUploadButtonAtFirst, true); - }); - } - - [Fact] - public async Task CardUpload_Reset() - { - var cut = Context.RenderComponent>(); - await cut.InvokeAsync(() => cut.Instance.Reset()); - Assert.Null(cut.Instance.DefaultFileList); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.DefaultFileList, - [ - new UploadFile() { FileName = "Test-File1.text" } - ]); - }); - await cut.InvokeAsync(() => cut.Instance.Reset()); - Assert.NotNull(cut.Instance.DefaultFileList); - Assert.Empty(cut.Instance.DefaultFileList); - } - - [Fact] - public async Task CardUpload_ShowDownload() - { - var clicked = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowDownloadButton, true); - pb.Add(a => a.OnDownload, file => - { - clicked = true; - return Task.CompletedTask; - }); - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File1.text" } - ]); - }); - - var button = cut.Find(".btn-download"); - await cut.InvokeAsync(() => button.Click()); - Assert.True(clicked); - } - - [Fact] - public void CardUpload_ShowZoom() - { - var clicked = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowZoomButton, true); - pb.Add(a => a.OnZoomAsync, file => - { - clicked = true; - return Task.CompletedTask; - }); - pb.Add(a => a.DefaultFileList, - [ - new UploadFile() { FileName = "Test-File1.text" } - ]); - }); - - var button = cut.Find(".btn-zoom"); - button.Click(); - cut.WaitForState(() => clicked); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowZoomButton, false); - }); - cut.WaitForAssertion(() => cut.DoesNotContain("btn-zoom")); - } - - [Fact] - public void ShowDeletedButton_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowDeletedButton, true); - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File1.text" } - ]); - }); - cut.Contains("aria-label=\"delete\""); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowDeletedButton, false); - }); - cut.WaitForAssertion(() => cut.DoesNotContain("aria-label=\"delete\"")); - } - - [Fact] - public void CardUpload_ValidateForm_Ok() - { - var foo = new Foo(); - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.Model, foo); - pb.AddChildContent>(pb => - { - pb.Add(a => a.Value, foo.Name); - pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); - }); - }); - cut.Contains("form-label"); - } - - [Fact] - public void CardUpload_IconTemplate_Ok() - { - var foo = new Foo(); - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.DefaultFileList, - [ - new() { FileName = "Test-File1.text" } - ]); - pb.Add(a => a.IconTemplate, file => builder => - { - builder.AddContent(0, "custom-file-icon-template"); - }); - }); - cut.Contains("custom-file-icon-template"); - } - - [Fact] - public async Task CardUpload_ShowProgress_Ok() - { - var cancel = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.OnChange, async file => - { - await Task.Delay(100); - await file.SaveToFileAsync("1.txt"); - }); - pb.Add(a => a.OnCancel, file => - { - cancel = true; - return Task.CompletedTask; - }); - }); - var input = cut.FindComponent(); - await cut.InvokeAsync(() => - { - input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - })); - var button = cut.Find(".btn-cancel"); - Assert.NotNull(button); - button.Click(); - }); - Assert.True(cancel); - } - - [Fact] - public async Task CardUpload_Max_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.Max, 1); - }); - var input = cut.FindComponent(); - await cut.InvokeAsync(async () => - { - await input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - })); - }); - } - - [Fact] - public void FileSize_Ok() - { - var validator = new FileValidationAttribute() - { - FileSize = 5 - }; - var p = new Person() - { - Picture = new MockBrowserFile("test.log") - }; - var result = validator.GetValidationResult(p.Picture, new ValidationContext(p)); - Assert.NotEqual(ValidationResult.Success, result); - } - - [Fact] - public void FileExtensions_Ok() - { - var validator = new FileValidationAttribute() - { - Extensions = ["jpg"] - }; - var p = new Person() - { - Picture = new MockBrowserFile("test.log") - }; - var result = validator.GetValidationResult(p.Picture, new ValidationContext(p)); - Assert.NotEqual(ValidationResult.Success, result); - } - - [Fact] - public void IsValid_Ok() - { - var validator = new FileValidationAttribute() - { - Extensions = ["jpg"] - }; - var p = new Person() - { - Picture = new MockBrowserFile("test.log") - }; - Assert.False(validator.IsValid(p.Picture)); - } - - [Fact] - public void Validate_Ok() - { - var validator = new FileValidationAttribute() - { - Extensions = ["jpg"] - }; - var p = new Person() - { - Picture = new MockBrowserFile("test.log") - }; - Assert.Throws(() => validator.Validate(p.Picture, "Picture")); - Assert.Throws(() => validator.Validate(p.Picture, new ValidationContext(p))); - } - - [Fact] - public void Capture_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Capture, "camera"); - }); - cut.Contains("capture=\"camera\""); - } - - [Fact] - public void DropUpload_BodyTemplate_Ok() - { - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.BodyTemplate, b => b.AddContent(0, "drop-upload-body-template")); - }); - cut.MarkupMatches("
    drop-upload-body-template
      "); - } - - [Fact] - public void DropUpload_IconTemplate_Ok() - { - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.IconTemplate, b => b.AddContent(0, "drop-upload-icon-template")); - }); - cut.Contains("
      drop-upload-icon-template
      "); - } - - [Fact] - public void DropUpload_TextTemplate_Ok() - { - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.TextTemplate, b => b.AddContent(0, "drop-upload-text-template")); - }); - cut.Contains("
      drop-upload-text-template
      "); - } - - [Fact] - public void DropUpload_Footer_Ok() - { - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.ShowFooter, true); - }); - cut.Contains("
      "); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.FooterTemplate, b => b.AddContent(0, "drop-upload-footer-text")); - }); - cut.Contains("
      drop-upload-footer-text
      "); - } - - [Fact] - public async Task DropUpload_OnChanged_Ok() - { - var cut = Context.RenderComponent(pb => - { - pb.Add(a => a.ShowLabel, true); - pb.Add(a => a.ShowProgress, true); - pb.Add(a => a.OnChange, async files => - { - await files.SaveToFileAsync("1.text"); - }); - }); - - var input = cut.FindComponent(); - await cut.InvokeAsync(() => input.Instance.OnChange.InvokeAsync(new InputFileChangeEventArgs(new List() - { - new() - }))); - } - - private class Person - { - [Required] - [FileValidation(Extensions = [".png", ".jpg", ".jpeg"])] - - public IBrowserFile? Picture { get; set; } - } - - private class MockBrowserFile(string name = "UploadTestFile", string contentType = "text") : IBrowserFile - { - public string Name { get; } = name; - - public DateTimeOffset LastModified { get; } = DateTimeOffset.Now; - - public long Size { get; } = 10; - - public string ContentType { get; } = contentType; - - public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default) - { - return new MemoryStream([0x01, 0x02]); - } - } -}