Skip to content

feat(upload prop:crop,aspectRatio): from Upload component accept prop… - #7095

Merged
jinmao88 merged 4 commits into
vbenjs:mainfrom
jyqwq:feature/antd-Upload支持图片裁剪上传
Jan 14, 2026

Hidden character warning

The head ref may contain hidden characters: "feature/antd-Upload\u652f\u6301\u56fe\u7247\u88c1\u526a\u4e0a\u4f20"
Merged

jinmao88 merged 4 commits into
vbenjs:mainfrom
jyqwq:feature/antd-Upload支持图片裁剪上传

Conversation

@jyqwq

@jyqwq jyqwq commented Jan 12, 2026 •

Copy link
Copy Markdown
Contributor

… crop,aspectRatio

Description

antd Upload 组件支持上传前裁剪,新增支持参数 crop 和 aspectRatio ,多选或者非图片不唤起裁剪框。

image

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • [ ✅ ] New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.

Checklist

ℹ️ Check all checkboxes - this will indicate that you have done everything in accordance with the rules in CONTRIBUTING.

  • [ ✅ ] If you introduce new functionality, document it. You can run documentation with pnpm run docs:dev command.
  • [ ✅ ] Run the tests with pnpm test.
  • [ ✅ ] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.
  • [ ✅ ] My code follows the style guidelines of this project
  • [ ✅ ] I have performed a self-review of my own code
  • [ ✅ ] I have commented my code, particularly in hard-to-understand areas
  • [ ✅ ] I have made corresponding changes to the documentation
  • [ ✅ ] My changes generate no new warnings
  • [ ✅ ] I have added tests that prove my fix is effective or that my feature works
  • [ ✅ ] New and existing unit tests pass locally with my changes
  • [ ✅ ] Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • New Features

    • Interactive image cropping in uploads via a modal cropper with aspect-ratio support; cropped images are used for upload and previews.
    • Example form field demonstrating the crop workflow with separate success/error handling and inclusion of cropped image URLs in submissions.
  • Documentation

    • Added localization strings for the cropping UI in English and Chinese.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot

changeset-bot Bot commented Jan 12, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e4de650

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jan 12, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds an image-cropping flow to upload previews (Modal + VCropper) that converts cropped base64 to Blob for upload, refactors ApiSelect/ApiTreeSelect registrations to use ApiComponent with withDefaultPlaceholder, and adds locale and playground example strings and form usage for cropping.

Changes

Cohort / File(s) Summary
Image cropping & upload flow
apps/web-antd/src/adapter/component/index.ts, playground/src/adapter/component/index.ts
Added isImageFile, cropImage (Modal + VCropper), and base64ToBlob. handleBeforeUpload now optionally opens crop modal for single-image uploads when crop: true and returns a Blob from the cropped result. Temporary DOM and modal cleanup adjusted.
Component registration refactor
apps/web-antd/src/adapter/component/index.ts
Replaced inline { ...ApiComponent, name: ... } registrations with withDefaultPlaceholder(ApiComponent, 'select', {...}) for ApiSelect and ApiTreeSelect, aligning prop/event names and exported component map entries.
Locale additions
packages/locales/src/langs/en-US/ui.json, packages/locales/src/langs/zh-CN/ui.json
Added crop translation object (title, confirm, cancel, errorTip) to UI locales.
Playground example translations
playground/src/locales/langs/en-US/examples.json, playground/src/locales/langs/zh-CN/examples.json
Added form.crop-image translation entries.
Playground example form
playground/src/views/examples/form/basic.vue
Added cropImage Upload form item (crop enabled, aspectRatio: '1:1', single-file) and extended onSubmit to handle cropped uploads in parallel, collecting success/failure URLs and messages.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UploadComponent
    participant beforeUploadHook
    participant ModalCropper
    participant base64ToBlob
    participant Uploader
    participant Server

    User->>UploadComponent: select file
    UploadComponent->>beforeUploadHook: beforeUpload(file, attrs)
    beforeUploadHook->>beforeUploadHook: check isImageFile && crop enabled && single-file
    alt crop path
        beforeUploadHook->>ModalCropper: open crop modal (VCropper)
        ModalCropper->>User: show crop UI
        User->>ModalCropper: confirm crop
        ModalCropper-->>beforeUploadHook: return base64 dataURL
        beforeUploadHook->>base64ToBlob: convert base64 -> Blob
        base64ToBlob-->>beforeUploadHook: Blob
        beforeUploadHook-->>UploadComponent: provide Blob for upload
    else
        beforeUploadHook-->>UploadComponent: provide original file
    end
    UploadComponent->>Uploader: upload(file/blob)
    Uploader->>Server: POST file
    Server-->>Uploader: upload result (URL)
    Uploader-->>User: show upload result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • anncwb
  • vince292007
  • mynetfan
  • jinmao88

Poem

🐰 I nibbled pixels, trimmed the frame,

Modal opened — a cropping game.
Base64 softened, turned to Blob,
Hopped to upload, a tidy job.
Cheers for cropped images bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature added: support for crop and aspectRatio props in the Upload component.
Description check ✅ Passed The description provides clear context about the new image cropping feature, includes an illustrative screenshot, and the checklist is substantially completed by the author.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @apps/web-antd/src/adapter/component/index.ts:
- Around line 328-341: The blob URL created by URL.createObjectURL(file) inside
the CropperWrapper render causes a memory leak; fix by creating and storing the
object URL in an outer scope (e.g., a variable alongside cropperRef) instead of
in the render call that constructs VCropper, pass that stored URL to VCropper,
and ensure you call URL.revokeObjectURL(storedUrl) inside the closeModal cleanup
(the modal teardown that unmounts CropperWrapper) so the object URL is revoked
when the cropper is closed.

In @playground/src/adapter/component/index.ts:
- Around line 389-404: The code may resolve with undefined because base64ToBlob
can return undefined; modify the Promise in the crop branch (the block using
attrs.crop, originFileList, cropImage and base64ToBlob) to check the result of
base64ToBlob and reject if it's undefined (e.g., reject(new
Error($t('ui.crop.invalid')) or an appropriate error message) instead of
resolving undefined; ensure you only call resolve(blob) when base64ToBlob
returns a valid Blob and propagate the error/path consistently so downstream
upload handlers never receive undefined.
- Around line 328-341: The blob URL created by URL.createObjectURL(file) is
never revoked, causing a memory leak; change the code to assign the URL to a
local variable (e.g., blobUrl) and pass that to VCropper (ref: VCropper,
cropperRef), then revoke the URL via URL.revokeObjectURL(blobUrl) when the modal
closes or the cropper is disposed—specifically call revoke in the onOk and
onCancel handlers (where resolve/dataUrl and resolve('') are used) and also on
component unmount/cleanup to ensure the blob URL is always released.

In @playground/src/views/examples/form/basic.vue:
- Around line 390-394: The code calls toRaw(values.cropImage) and immediately
treats it as an array, which will throw if cropImage is undefined; update the
logic in the crop handling (the toRaw(values.cropImage) usage that defines
cropImage and the derived arrays doneCrop and failedCrop) to guard against
undefined by normalizing cropImage to an empty array when falsy (or check it
before calling .filter()), then compute doneCrop and failedCrop from that safe
array so .filter() never runs on undefined.
🧹 Nitpick comments (4)
playground/src/adapter/component/index.ts (2)

140-152: URL query strings may cause incorrect extension detection.

The extension extraction via file.url?.split('.').pop() will fail for URLs with query parameters (e.g., image.png?v=123 returns png?v=123). Consider parsing the URL pathname first.

♻️ Suggested fix
     if (file.url) {
-      const ext = file.url?.split('.').pop()?.toLowerCase();
+      try {
+        const pathname = new URL(file.url, 'http://localhost').pathname;
+        const ext = pathname.split('.').pop()?.toLowerCase();
+        return ext ? imageExtensions.has(ext) : false;
+      } catch {
+        const ext = file.url?.split('.').pop()?.toLowerCase();
+        return ext ? imageExtensions.has(ext) : false;
+      }
-      return ext ? imageExtensions.has(ext) : false;
     }

351-362: Prefer charCodeAt over codePointAt for base64 decoding.

codePointAt can return undefined and handles surrogate pairs differently than charCodeAt. For decoding base64 binary data (which produces Latin-1 characters 0-255), charCodeAt is the standard approach and avoids the need for the type assertion.

♻️ Suggested fix
   const base64ToBlob = (base64: Base64URLString) => {
     const [typeStr, encodeStr] = base64.split(',');
     if (!typeStr || !encodeStr) return;
     const mime = typeStr.match(/:(.*?);/)?.[1];
     const raw = window.atob(encodeStr);
     const rawLength = raw.length;
     const uInt8Array = new Uint8Array(rawLength);
     for (let i = 0; i < rawLength; ++i) {
-      uInt8Array[i] = raw.codePointAt(i) as number;
+      uInt8Array[i] = raw.charCodeAt(i);
     }
     return new Blob([uInt8Array], { type: mime });
   };
apps/web-antd/src/adapter/component/index.ts (2)

349-350: Prefer charCodeAt for base64 decoding.

Same issue as in the playground adapter. Use charCodeAt instead of codePointAt for proper binary data handling.

♻️ Suggested fix
     for (let i = 0; i < rawLength; ++i) {
-      uInt8Array[i] = raw.codePointAt(i) as number;
+      uInt8Array[i] = raw.charCodeAt(i);
     }

128-149: Significant code duplication between adapter files.

The isImageFile, cropImage, base64ToBlob functions and the cropping logic are duplicated between playground/src/adapter/component/index.ts and apps/web-antd/src/adapter/component/index.ts. Consider extracting these utilities to a shared module in @vben/common-ui or @vben/utils to reduce maintenance burden and ensure consistent behavior.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19b2d7a and 30827c7.

📒 Files selected for processing (7)
  • apps/web-antd/src/adapter/component/index.ts
  • packages/locales/src/langs/en-US/ui.json
  • packages/locales/src/langs/zh-CN/ui.json
  • playground/src/adapter/component/index.ts
  • playground/src/locales/langs/en-US/examples.json
  • playground/src/locales/langs/zh-CN/examples.json
  • playground/src/views/examples/form/basic.vue
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-01-21T05:03:44.466Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5446
File: packages/effects/common-ui/src/components/icon-picker/icons.ts:0-0
Timestamp: 2025-01-21T05:03:44.466Z
Learning: In the vue-vben-admin project's icon-picker component, requests to the Iconify API should be deduplicated using a PENDING_REQUESTS cache to prevent multiple simultaneous requests for the same prefix, and should include a 10-second timeout using AbortController.

Applied to files:

  • playground/src/adapter/component/index.ts
  • apps/web-antd/src/adapter/component/index.ts
📚 Learning: 2024-12-09T04:41:58.914Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5075
File: packages/effects/common-ui/src/components/api-select/api-select.vue:61-62
Timestamp: 2024-12-09T04:41:58.914Z
Learning: 在文件 `packages/effects/common-ui/src/components/api-select/api-select.vue` 的组件 `ApiSelect` 中,`childrenField` 的默认值设置为空字符串是有意的,因为并非所有组件都使用树形数据。

Applied to files:

  • apps/web-antd/src/adapter/component/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Check (ubuntu-latest)
  • GitHub Check: Check (windows-latest)
  • GitHub Check: Test (windows-latest)
  • GitHub Check: post-update (windows-latest)
  • GitHub Check: post-update (ubuntu-latest)
🔇 Additional comments (7)
packages/locales/src/langs/zh-CN/ui.json (1)

57-61: LGTM!

The crop-related locale strings are well-structured and consistent with the existing localization patterns. The translations appropriately match the en-US counterparts.

playground/src/locales/langs/zh-CN/examples.json (1)

29-29: LGTM!

The translation entry for the new crop-image form field is correctly added and consistent with the en-US locale.

playground/src/locales/langs/en-US/examples.json (1)

26-26: LGTM!

The translation entry is appropriately added for the new crop-image form field.

packages/locales/src/langs/en-US/ui.json (1)

57-61: LGTM!

The crop-related locale strings are correctly added and follow the established localization structure.

playground/src/adapter/component/index.ts (1)

29-39: LGTM!

The imports for VCropper and Modal are correctly added to support the new cropping functionality.

playground/src/views/examples/form/basic.vue (1)

361-382: LGTM!

The new cropImage upload field properly demonstrates the crop feature with appropriate configuration options. The inline comments provide useful context for the example.

apps/web-antd/src/adapter/component/index.ts (1)

497-510: LGTM!

The ApiSelect and ApiTreeSelect registrations are correctly refactored to use the withDefaultPlaceholder pattern with ApiComponent, consistent with the existing ApiCascader registration.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @playground/src/adapter/component/index.ts:
- Around line 333-343: The onOk handler uses cropperRef.value and calls
cropper.getCropImage() without safety checks; wrap the await
cropper.getCropImage() call in a try-catch and validate the returned dataUrl
before calling resolve, calling reject(err) on any thrown error or if the result
is invalid, and ensure closeModal() runs in a finally block so the modal always
closes; reference the onOk function, cropperRef.value / cropper.getCropImage(),
and the resolve/reject/closeModal calls when making the change.
- Around line 402-421: The branch uses isImageFile(file) but calls
cropImage(originFileList[0]), which is inconsistent and confusing; update the
crop flow to use the same file object (use file.originFileObj or ensure
originFileList[0] === file) so cropping operates on the checked file: replace
originFileList[0] with file.originFileObj when calling cropImage and
base64ToBlob (or add a short comment explaining why originFileList[0] is
intentionally used), keeping the guards attrs.crop && !attrs.multiple and the
existing error handling intact.

In @playground/src/views/examples/form/basic.vue:
- Around line 415-424: The success branch currently shows an upload-URLs message
even when no crop images exist; update the conditional around failedCrop to
first check that cropImage (or the list/array that holds uploaded crop images)
is non-empty before calling message.success, e.g., only call message.success
with msgCrop when cropImage.length > 0 (or equivalent) and skip the success
message when cropImage is empty; keep the existing message.error and return
behavior for the failedCrop path and continue to reference failedCrop, cropImage
and msgCrop in the fix.
🧹 Nitpick comments (2)
playground/src/adapter/component/index.ts (2)

138-148: Consider hoisting the image extensions Set.

The imageExtensions Set is recreated on every call to isImageFile. Since this is a constant, consider moving it outside the function for a minor performance improvement.

♻️ Suggested refactor
+const IMAGE_EXTENSIONS = new Set([
+  'bmp',
+  'gif',
+  'jpeg',
+  'jpg',
+  'png',
+  'svg',
+  'webp',
+]);
+
 const withPreviewUpload = () => {
-  // 检查是否为图片文件的辅助函数
-  const isImageFile = (file: UploadFile): boolean => {
-    const imageExtensions = new Set([
-      'bmp',
-      'gif',
-      'jpeg',
-      'jpg',
-      'png',
-      'svg',
-      'webp',
-    ]);
+  const isImageFile = (file: UploadFile): boolean => {
     if (file.url) {
       try {
         const pathname = new URL(file.url, 'http://localhost').pathname;
         const ext = pathname.split('.').pop()?.toLowerCase();
-        return ext ? imageExtensions.has(ext) : false;
+        return ext ? IMAGE_EXTENSIONS.has(ext) : false;

364-375: Prefer charCodeAt over codePointAt for base64 decoding.

Since base64 encoding only uses ASCII characters (code points 0-127), charCodeAt is more appropriate and semantically clearer than codePointAt. Both work identically for ASCII, but charCodeAt avoids the need for the type assertion.

♻️ Suggested refactor
   const base64ToBlob = (base64: Base64URLString) => {
     const [typeStr, encodeStr] = base64.split(',');
     if (!typeStr || !encodeStr) return;
     const mime = typeStr.match(/:(.*?);/)?.[1];
     const raw = window.atob(encodeStr);
     const rawLength = raw.length;
     const uInt8Array = new Uint8Array(rawLength);
     for (let i = 0; i < rawLength; ++i) {
-      uInt8Array[i] = raw.codePointAt(i) as number;
+      uInt8Array[i] = raw.charCodeAt(i);
     }
     return new Blob([uInt8Array], { type: mime });
   };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30827c7 and 3aab54a.

📒 Files selected for processing (4)
  • packages/locales/src/langs/en-US/ui.json
  • packages/locales/src/langs/zh-CN/ui.json
  • playground/src/adapter/component/index.ts
  • playground/src/views/examples/form/basic.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/locales/src/langs/zh-CN/ui.json
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-21T05:03:44.466Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5446
File: packages/effects/common-ui/src/components/icon-picker/icons.ts:0-0
Timestamp: 2025-01-21T05:03:44.466Z
Learning: In the vue-vben-admin project's icon-picker component, requests to the Iconify API should be deduplicated using a PENDING_REQUESTS cache to prevent multiple simultaneous requests for the same prefix, and should include a 10-second timeout using AbortController.

Applied to files:

  • playground/src/adapter/component/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Check (ubuntu-latest)
  • GitHub Check: Lint (windows-latest)
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Check (windows-latest)
  • GitHub Check: post-update (ubuntu-latest)
  • GitHub Check: post-update (windows-latest)
🔇 Additional comments (3)
packages/locales/src/langs/en-US/ui.json (1)

57-62: LGTM!

The crop-related locale entries are well-structured and consistent with the existing translation patterns in this file.

playground/src/adapter/component/index.ts (1)

29-39: LGTM!

The new imports for VCropper and Modal are appropriate for the image cropping feature.

playground/src/views/examples/form/basic.vue (1)

361-382: LGTM!

Good demonstration of the new crop feature with appropriate configuration. The crop: true and aspectRatio: '1:1' props clearly showcase the new functionality.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @apps/web-antd/src/adapter/component/index.ts:
- Around line 360-371: The base64ToBlob function uses String.codePointAt
(incorrect for byte values) and lacks error handling for window.atob and
malformed input; replace codePointAt with charCodeAt when populating the
Uint8Array, wrap the atob call in a try/catch to handle and log/return on decode
errors, validate that base64.split(',') produced both parts and that mime is
present (or default to 'application/octet-stream'), and ensure the function
returns undefined or throws consistently on invalid input; update function name
base64ToBlob accordingly.
🧹 Nitpick comments (3)
playground/src/adapter/component/index.ts (2)

286-367: Add explicit return type for cropImage and handle edge cases.

The function returns a Promise that resolves to a data URL string or empty string, but this isn't typed. Also, if the modal fails to render or getCropImage returns an unexpected value, behavior may be undefined.

Suggested improvement
- const cropImage = (file: File, aspectRatio: string | undefined) => {
-   return new Promise((resolve, reject) => {
+ const cropImage = (file: File, aspectRatio: string | undefined): Promise<string> => {
+   return new Promise<string>((resolve, reject) => {

398-426: Consider simplifying the nested Promise pattern.

The code awaits cropImage, then wraps the result processing in a new Promise. This can be simplified to avoid the anti-pattern of creating a Promise inside an async function when you already have the value.

Simplified approach
        if (
          attrs.crop &&
          !attrs.multiple &&
          originFileList[0] &&
          isImageFile(file)
        ) {
          file.status = 'removed';
          const base64 = await cropImage(originFileList[0], attrs.aspectRatio);
-         return new Promise((resolve, reject) => {
-           if (!base64) {
-             return reject(new Error($t('ui.crop.cancel')));
-           }
-           const blob = base64ToBlob(base64 as string);
-           if (!blob) {
-             return reject(new Error($t('ui.crop.errorTip')));
-           }
-           resolve(blob);
-         });
+         if (!base64) {
+           throw new Error($t('ui.crop.cancel'));
+         }
+         const blob = base64ToBlob(base64 as string);
+         if (!blob) {
+           throw new Error($t('ui.crop.errorTip'));
+         }
+         return blob;
        }
apps/web-antd/src/adapter/component/index.ts (1)

128-474: Significant code duplication with playground/src/adapter/component/index.ts.

The isImageFile, cropImage, base64ToBlob helpers and the entire withPreviewUpload factory are duplicated. Consider extracting these utilities to @vben/common-ui or @vben/utils to maintain a single source of truth and reduce maintenance burden.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3aab54a and c5a7274.

📒 Files selected for processing (3)
  • apps/web-antd/src/adapter/component/index.ts
  • playground/src/adapter/component/index.ts
  • playground/src/views/examples/form/basic.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • playground/src/views/examples/form/basic.vue
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-01-21T05:03:44.466Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5446
File: packages/effects/common-ui/src/components/icon-picker/icons.ts:0-0
Timestamp: 2025-01-21T05:03:44.466Z
Learning: In the vue-vben-admin project's icon-picker component, requests to the Iconify API should be deduplicated using a PENDING_REQUESTS cache to prevent multiple simultaneous requests for the same prefix, and should include a 10-second timeout using AbortController.

Applied to files:

  • apps/web-antd/src/adapter/component/index.ts
  • playground/src/adapter/component/index.ts
📚 Learning: 2024-12-09T04:41:58.914Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5075
File: packages/effects/common-ui/src/components/api-select/api-select.vue:61-62
Timestamp: 2024-12-09T04:41:58.914Z
Learning: 在文件 `packages/effects/common-ui/src/components/api-select/api-select.vue` 的组件 `ApiSelect` 中,`childrenField` 的默认值设置为空字符串是有意的,因为并非所有组件都使用树形数据。

Applied to files:

  • apps/web-antd/src/adapter/component/index.ts
🧬 Code graph analysis (1)
playground/src/adapter/component/index.ts (6)
packages/locales/src/index.ts (1)
  • $t (13-13)
apps/web-antd/src/locales/index.ts (1)
  • $t (102-102)
apps/web-ele/src/locales/index.ts (1)
  • $t (102-102)
apps/web-naive/src/locales/index.ts (1)
  • $t (38-38)
playground/src/locales/index.ts (1)
  • $t (102-102)
scripts/clean.mjs (1)
  • i (78-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: post-update (windows-latest)
  • GitHub Check: post-update (ubuntu-latest)
  • GitHub Check: Check (windows-latest)
  • GitHub Check: Check (ubuntu-latest)
  • GitHub Check: Lint (windows-latest)
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Lint (ubuntu-latest)
🔇 Additional comments (4)
playground/src/adapter/component/index.ts (2)

139-164: Image detection logic looks correct.

The isImageFile helper properly handles multiple scenarios: URL-based detection with fallback for URL parsing failures, MIME type detection, and filename extension fallback. The set of supported extensions is comprehensive.


529-542: ApiSelect and ApiTreeSelect refactored to use ApiComponent.

The registrations now consistently use the ApiComponent wrapper with appropriate configuration. This aligns with the pattern used for ApiCascader.

apps/web-antd/src/adapter/component/index.ts (2)

520-533: ApiSelect and ApiTreeSelect registrations updated consistently.

The component registrations now use the ApiComponent pattern matching ApiCascader, providing a consistent approach across API-driven select components.


389-420: Verify rejection behavior when cropping is canceled.

When the user cancels the crop modal, cropImage resolves with an empty string, which then causes handleBeforeUpload to reject with $t('ui.crop.cancel'). This rejection will be caught by ant-design-vue's Upload, but verify the UX is acceptable (e.g., no error toast shown to user for intentional cancel).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
playground/src/adapter/component/index.ts (3)

140-148: URL with query parameters may cause false negatives.

When parsing file.url, if the URL contains query parameters (e.g., https://example.com/image.jpg?token=abc), the pathname correctly strips them. However, in the catch block fallback (line 146), file.url.split('.').pop() would extract jpg?token=abc, failing the extension check.

Suggested fix
       } catch {
-        const ext = file.url?.split('.').pop()?.toLowerCase();
+        const urlWithoutQuery = file.url?.split('?')[0];
+        const ext = urlWithoutQuery?.split('.').pop()?.toLowerCase();
         return ext ? imageExtensions.has(ext) : false;
       }

360-375: Use charCodeAt instead of codePointAt for byte conversion.

codePointAt is designed for Unicode code points and can return undefined at the end of strings. For converting raw bytes from atob() output (which is always single-byte characters), charCodeAt is the correct method and never returns undefined.

Additionally, Base64URLString is a WebAuthn credential type, not a data URL type. Consider using string instead.

Suggested fix
-  const base64ToBlob = (base64: Base64URLString) => {
+  const base64ToBlob = (base64: string) => {
     try {
       const [typeStr, encodeStr] = base64.split(',');
       if (!typeStr || !encodeStr) return;
       const mime = typeStr.match(/:(.*?);/)?.[1];
       const raw = window.atob(encodeStr);
       const rawLength = raw.length;
       const uInt8Array = new Uint8Array(rawLength);
       for (let i = 0; i < rawLength; ++i) {
-        uInt8Array[i] = raw.codePointAt(i) as number;
+        uInt8Array[i] = raw.charCodeAt(i);
       }
       return new Blob([uInt8Array], { type: mime });
     } catch {
       return undefined;
     }
   };

402-422: Simplify error handling and avoid unnecessary nested Promise.

The nested new Promise() at lines 412-421 is unnecessary since handleBeforeUpload is already async. You can throw directly or return the blob.

Suggested simplification
         // 多选或者非图片不唤起裁剪框
         if (
           attrs.crop &&
           !attrs.multiple &&
           originFileList[0] &&
           isImageFile(file)
         ) {
           file.status = 'removed';
           // antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
           const base64 = await cropImage(originFileList[0], attrs.aspectRatio);
-          return new Promise((resolve, reject) => {
-            if (!base64) {
-              return reject(new Error($t('ui.crop.cancel')));
-            }
-            const blob = base64ToBlob(base64 as string);
-            if (!blob) {
-              return reject(new Error($t('ui.crop.errorTip')));
-            }
-            resolve(blob);
-          });
+          if (!base64) {
+            throw new Error($t('ui.crop.cancel'));
+          }
+          const blob = base64ToBlob(base64 as string);
+          if (!blob) {
+            throw new Error($t('ui.crop.errorTip'));
+          }
+          return blob;
         }
apps/web-antd/src/adapter/component/index.ts (1)

128-375: Extract shared cropping logic to reduce duplication.

The entire isImageFile, cropImage, and base64ToBlob implementations are duplicated between this file and playground/src/adapter/component/index.ts. This creates maintenance burden and risks inconsistencies.

Consider extracting these utilities to a shared module in @vben/common-ui or @vben/utils.

Note: The same issues flagged in the playground file apply here:

  • Use charCodeAt instead of codePointAt in base64ToBlob
  • Change Base64URLString type to string
  • Handle query parameters in URL fallback parsing in isImageFile
  • Simplify nested Promise in handleBeforeUpload
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5a7274 and e4de650.

📒 Files selected for processing (2)
  • apps/web-antd/src/adapter/component/index.ts
  • playground/src/adapter/component/index.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-01-21T05:03:44.466Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5446
File: packages/effects/common-ui/src/components/icon-picker/icons.ts:0-0
Timestamp: 2025-01-21T05:03:44.466Z
Learning: In the vue-vben-admin project's icon-picker component, requests to the Iconify API should be deduplicated using a PENDING_REQUESTS cache to prevent multiple simultaneous requests for the same prefix, and should include a 10-second timeout using AbortController.

Applied to files:

  • playground/src/adapter/component/index.ts
  • apps/web-antd/src/adapter/component/index.ts
📚 Learning: 2024-12-09T04:41:58.914Z
Learnt from: mynetfan
Repo: vbenjs/vue-vben-admin PR: 5075
File: packages/effects/common-ui/src/components/api-select/api-select.vue:61-62
Timestamp: 2024-12-09T04:41:58.914Z
Learning: 在文件 `packages/effects/common-ui/src/components/api-select/api-select.vue` 的组件 `ApiSelect` 中,`childrenField` 的默认值设置为空字符串是有意的,因为并非所有组件都使用树形数据。

Applied to files:

  • apps/web-antd/src/adapter/component/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: post-update (ubuntu-latest)
  • GitHub Check: post-update (windows-latest)
  • GitHub Check: Check (ubuntu-latest)
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Check (windows-latest)
🔇 Additional comments (3)
playground/src/adapter/component/index.ts (2)

525-538: LGTM on ApiSelect/ApiTreeSelect refactor.

The transition to ApiComponent-based registration with withDefaultPlaceholder is consistent with the ApiCascader pattern and properly configures the component props.


277-277: No action needed. The aspectRatio parameter type of string | undefined is correct—VCropper expects aspect ratios in string format (e.g., "16:9", "1:1") and parses them internally to numeric values.

Likely an incorrect or invalid review comment.

apps/web-antd/src/adapter/component/index.ts (1)

525-538: LGTM on ApiSelect/ApiTreeSelect refactor.

The ApiComponent-based registration is consistent with the existing ApiCascader pattern.

@jinmao88
jinmao88 merged commit 67da941 into vbenjs:main Jan 14, 2026
10 of 14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Feb 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants