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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@
<DemoBlock Title="@Localizer["AvatarUploadAcceptTitle"]"
Introduction="@Localizer["AvatarUploadAcceptIntro"]"
Name="Accept">
<AvatarUpload TValue="string" Accept="image/*" OnChange="@OnChange"></AvatarUpload>
<AvatarUpload TValue="string" Accept="image/*"></AvatarUpload>
</DemoBlock>

<DemoBlock Title="@Localizer["AvatarUploadValidateTitle"]"
Introduction="@Localizer["AvatarUploadValidateIntro"]"
Name="ValidateForm">
<ValidateForm Model="@_foo" OnValidSubmit="OnAvatarValidSubmit">
<ValidateForm Model="@_foo" OnValidSubmit="OnAvatarValidSubmit" OnInValidSubmit="OnAvatarInValidSubmit">
<div class="row g-3">
<div class="col-12">
<BootstrapInput @bind-Value="@_foo.Name"></BootstrapInput>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,14 @@ protected override void OnInitialized()
]);
}

private async Task OnChange(UploadFile file)
private Task OnAvatarValidSubmit(EditContext context)
{
// 示例代码,使用 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}");
}
}
return ToastService.Success(Localizer["UploadsValidateFormTitle"], Localizer["UploadsValidateFormValidContent"]);
}

private Task OnAvatarValidSubmit(EditContext context)
private Task OnAvatarInValidSubmit(EditContext context)
{
return ToastService.Error(Localizer["UploadsValidateFormTitle"], Localizer["UploadsValidateFormValidContent"]);
return ToastService.Error(Localizer["UploadsValidateFormTitle"], Localizer["UploadsValidateFormInValidContent"]);
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3481,6 +3481,7 @@
"UploadsBorderRadius": "Border radius",
"UploadsValidateFormTitle": "ValidateForm",
"UploadsValidateFormValidContent": "Saved successfully",
"UploadsValidateFormInValidContent": "Please correct it and submit the form again",
"UploadsFormatError": "The file format is incorrect",
"UploadsAvatarMsg": "Avatar upload"
},
Expand Down
1 change: 1 addition & 0 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3481,6 +3481,7 @@
"UploadsBorderRadius": "预览框圆角曲率",
"UploadsValidateFormTitle": "表单应用",
"UploadsValidateFormValidContent": "数据合规,保存成功",
"UploadsValidateFormInValidContent": "数据不合规,请更正后再提交表单",
"UploadsFormatError": "文件格式不正确",
"UploadsAvatarMsg": "头像上传"
},
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.7.1-beta04</Version>
<Version>9.7.1-beta05</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
16 changes: 10 additions & 6 deletions src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ public partial class AvatarUpload<TValue>
[Parameter]
public bool IsUploadButtonAtFirst { get; set; }

/// <summary>
/// 获得/设置 是否允许预览回调方法 默认 null
/// </summary>
[Parameter]
public Func<UploadFile, bool>? CanPreviewCallback { get; set; }

[Inject]
[NotNull]
private IIconTheme? IconTheme { get; set; }
Expand Down Expand Up @@ -133,14 +139,12 @@ protected override void OnParametersSet()
/// <returns></returns>
protected override async Task TriggerOnChanged(UploadFile file)
{
if (OnChange == null)
{
await file.RequestBase64ImageFileAsync(allowExtensions: AllowExtensions);
}
else
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
await OnChange(file);
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
await base.TriggerOnChanged(file);
Comment on lines +142 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Non-image files retain previous PrevUrl

Reset PrevUrl for non-image files to prevent showing outdated previews.

Suggested change
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
await OnChange(file);
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
await base.TriggerOnChanged(file);
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
else
{
// 非图片文件重置预览地址,防止显示过期预览
file.PrevUrl = null;
}
await base.TriggerOnChanged(file);

}

private IReadOnlyCollection<ValidationResult> _results = [];
Expand Down
15 changes: 15 additions & 0 deletions src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ protected override void OnParametersSet()
RemoveIcon ??= IconTheme.GetIconByKey(ComponentIcons.CardUploadRemoveIcon);
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
protected override async Task TriggerOnChanged(UploadFile file)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Image preview logic duplicated here as well

Consider refactoring this logic into a shared base class or utility method to avoid duplication.

{
// 从客户端获得预览地址不使用 base64 编码
if (file.IsImage(AllowExtensions, CanPreviewCallback))
{
file.PrevUrl = await InvokeAsync<string?>("getPreviewUrl", Id, file.OriginFileName);
}
await base.TriggerOnChanged(file);
}

private async Task OnCardFileDelete(UploadFile item)
{
await OnFileDelete(item);
Expand Down
8 changes: 4 additions & 4 deletions src/BootstrapBlazor/Extensions/UploadFileExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,14 @@ public static async Task<bool> SaveToFileAsync(this UploadFile upload, string fi
/// </summary>
/// <param name="item"></param>
/// <param name="allowExtensions"></param>
/// <param name="_callback"></param>
/// <param name="callback"></param>
/// <returns></returns>
public static bool IsImage(this UploadFile item, List<string>? allowExtensions = null, Func<UploadFile, bool>? _callback = null)
public static bool IsImage(this UploadFile item, List<string>? allowExtensions = null, Func<UploadFile, bool>? callback = null)
{
bool ret;
if (_callback != null)
if (callback != null)
{
ret = _callback(item);
ret = callback(item);
}
else if (item.File != null)
{
Expand Down
29 changes: 25 additions & 4 deletions src/BootstrapBlazor/wwwroot/modules/upload.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import Data from "./data.js"
import EventHandler from "./event-handler.js"
import { readFileAsync } from "./utility.js"

export function init(id) {
const el = document.getElementById(id)
if (el === null) {
return
}
const preventHandler = e => e.preventDefault()
const preventHandler = e => e.preventDefault();
const body = el.querySelector('.upload-drop-body');
const upload = { el, body, preventHandler }
const inputFile = el.querySelector('[type="file"]');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): inputFile can be null; add a null-check before usage

Add a guard or early return to handle cases where inputFile is null to avoid runtime errors.

const upload = { el, body, preventHandler, inputFile };
Data.set(id, upload)

const inputFile = el.querySelector('[type="file"]')
EventHandler.on(el, 'click', '.btn-browser', () => {
inputFile.click()
})
EventHandler.on(inputFile, 'change', e => {
upload.files = e.delegateTarget.files;
});

EventHandler.on(document, "dragleave", preventHandler)
EventHandler.on(document, 'drop', preventHandler)
Expand Down Expand Up @@ -79,12 +83,28 @@ export function init(id) {
})
}

export async function getPreviewUrl(id, fileName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring getPreviewUrl to use early returns and optional chaining to reduce nesting and intermediate variables.

Here’s a way to flatten out the nesting in getPreviewUrl (and even drop the manual let url = '' accumulator) by using early-returns and optional chaining:

export async function getPreviewUrl(id, fileName) {
  const files = Data.get(id)?.files;
  if (!files) return '';

  // find the File object
  const file = Array.from(files).find(f => f.name === fileName);
  if (!file) return '';

  // readFileAsync → blob URL
  const blob = await readFileAsync(file);
  return blob ? URL.createObjectURL(blob) : '';
}

If you’re not actually transforming the file (just wrapping it in a blob URL), you can simplify further and drop readFileAsync entirely:

export function getPreviewUrl(id, fileName) {
  const files = Data.get(id)?.files || [];
  const file = files.find(f => f.name === fileName);
  return file ? URL.createObjectURL(file) : '';
}

Both versions preserve the current behavior while reducing indentation, intermediate variables, and nested if blocks.

let url = '';
const upload = Data.get(id);
const { files } = upload;
if (files) {
const file = [...files].find(v => v.name === fileName);
if (file) {
const data = await readFileAsync(file);
if (data) {
url = URL.createObjectURL(data);
}
}
}
return url;
}

export function dispose(id) {
const upload = Data.get(id)
Data.remove(id)

if (upload) {
const { el, body, preventHandler } = upload;
const { el, body, preventHandler, inputFile } = upload;

EventHandler.off(document, 'dragleave', preventHandler)
EventHandler.off(document, 'drop', preventHandler)
Expand All @@ -94,6 +114,7 @@ export function dispose(id) {
EventHandler.off(el, 'click')
EventHandler.off(el, 'drop')
EventHandler.off(el, 'paste')
EventHandler.off(inputFile, 'change')
EventHandler.off(body, 'dragleave')
EventHandler.off(body, 'drop')
EventHandler.off(body, 'dragenter')
Expand Down
21 changes: 21 additions & 0 deletions src/BootstrapBlazor/wwwroot/modules/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,27 @@ export function drawImage(canvas, image, offsetWidth, offsetHeight) {
context.drawImage(image, 0, 0, offsetWidth, offsetHeight);
}

/**
* @param {File} file
* @returns {Blob}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: JSDoc @returns annotation is inaccurate

Change the annotation to @returns {Promise} to accurately describe the return type.

*/
export function readFileAsync(file) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider removing or simplifying the readFileAsync helper to avoid unnecessary FileReader logic.

Suggested change
export function readFileAsync(file) {
The new `readFileAsync` helper isn’t needed since `File` already implements the `Blob` interface. You can either:
1. Drop the helper entirely and pass `file` wherever a `Blob` is expected, or
2. If you really need an immutable copy, use `File.prototype.slice` instead of a `FileReader` round‐trip.
Example (identity / no‐op helper):
```js
/** @param {File} file
* @returns {Promise<Blob>}
*/
export function readFileAsync(file) {
return Promise.resolve(file);
}

Or (clone via slice):

/** @param {File} file
 *  @returns {Promise<Blob>}
 */
export function readFileAsync(file) {
  const blobCopy = file.slice(0, file.size, file.type);
  return Promise.resolve(blobCopy);
}

Both eliminate the 20+ lines of FileReader logic while preserving all functionality.```

return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = () => {
const blob = new Blob([reader.result], { type: file.type });
resolve(blob);
};

reader.onerror = (error) => {
reject(error);
};

reader.readAsArrayBuffer(file);
});
}

export {
autoAdd,
autoRemove,
Expand Down
1 change: 1 addition & 0 deletions test/UnitTest/Components/UploadAvatarTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public async Task AvatarUpload_Ok()
UploadFile? uploadFile = null;
var cut = Context.RenderComponent<AvatarUpload<string>>(pb =>
{
pb.Add(a => a.CanPreviewCallback, null);
pb.Add(a => a.IsMultiple, true);
pb.Add(a => a.OnChange, file =>
{
Expand Down