From f3854b9c9befcfe08c24300f47e9ae09a774d230 Mon Sep 17 00:00:00 2001 From: leermao <447552708@qq.com> Date: Mon, 22 Dec 2025 11:52:48 +0800 Subject: [PATCH 1/4] feat(uploader): add headless API for programmatic control --- packages/uploader/src/react/uploader.tsx | 94 ++++++++++++++++++++++-- packages/uploader/src/types.ts | 36 ++++++++- 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/packages/uploader/src/react/uploader.tsx b/packages/uploader/src/react/uploader.tsx index 6a53aca..ada40a2 100644 --- a/packages/uploader/src/react/uploader.tsx +++ b/packages/uploader/src/react/uploader.tsx @@ -1,4 +1,4 @@ -import { UploaderProps } from '../types'; +import { UploaderProps, UploaderRef } from '../types'; import keyBy from 'lodash/keyBy'; import { useReactive, useRequest } from 'ahooks'; import { createRoot } from 'react-dom/client'; @@ -914,6 +914,81 @@ export function Uploader({ }, 500); } + const addFilesToUppy = (files: File[], source: string, autoUpload: boolean) => { + files.forEach((file) => { + state.uppy.addFile({ + name: file.name, + type: file.type, + data: file, + source, + }); + }); + if (autoUpload) { + state.uppy.upload(); + } + }; + + const triggerFileInput = (options?: { accept?: string; multiple?: boolean; autoUpload?: boolean }) => { + const input = document.createElement('input'); + input.type = 'file'; + input.style.display = 'none'; + input.accept = options?.accept ?? state.restrictions?.allowedFileTypes?.join(',') ?? ''; + input.multiple = options?.multiple ?? state.restrictions?.maxNumberOfFiles !== 1; + + input.onchange = (e: Event) => { + const target = e.target as HTMLInputElement; + const files = Array.from(target.files || []); + addFilesToUppy(files, 'local', options?.autoUpload !== false); + document.body.removeChild(input); + }; + + document.body.appendChild(input); + input.click(); + }; + + const getDropzoneProps = (options?: { autoUpload?: boolean; noClick?: boolean }) => { + const autoUpload = options?.autoUpload !== false; + let dragCounter = 0; + + return { + onDragEnter: (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter++; + if (e.dataTransfer?.items?.length) { + e.currentTarget.setAttribute('data-dragging', 'true'); + } + }, + onDragLeave: (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter--; + if (dragCounter === 0) { + e.currentTarget.removeAttribute('data-dragging'); + } + }, + onDragOver: (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, + onDrop: (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.removeAttribute('data-dragging'); + dragCounter = 0; + const files = Array.from(e.dataTransfer?.files || []); + if (files.length) { + addFilesToUppy(files, 'drop', autoUpload); + } + }, + onClick: options?.noClick + ? undefined + : () => { + triggerFileInput({ autoUpload }); + }, + }; + }; + useImperativeHandle( ref, () => @@ -921,11 +996,18 @@ export function Uploader({ getUploader: () => state.uppy, open, close, - } as { - getUploader: Function; - open: Function; - close: Function; - }) + // Headless API + triggerFileInput, + getDropzoneProps, + addFiles: (files: File[], options?: { autoUpload?: boolean }) => { + addFilesToUppy(files, 'local', options?.autoUpload !== false); + }, + upload: () => state.uppy.upload(), + getProgress: () => state.uppy.getState().totalProgress, + getFiles: () => state.uppy.getFiles(), + removeFile: (fileId: string) => state.uppy.removeFile(fileId), + cancelAll: () => state.uppy.cancelAll(), + } as UploaderRef) ); const Wrapper = popup ? Modal : Fragment; diff --git a/packages/uploader/src/types.ts b/packages/uploader/src/types.ts index c33d3d5..6a7e866 100644 --- a/packages/uploader/src/types.ts +++ b/packages/uploader/src/types.ts @@ -1,4 +1,5 @@ -import type { UppyOptions } from '@uppy/core'; +import type Uppy from '@uppy/core'; +import type { UppyOptions, UppyFile } from '@uppy/core'; import type { DashboardOptions } from '@uppy/dashboard'; import type { TusOptions } from '@uppy/tus'; import type { ImageEditorOptions } from '@uppy/image-editor'; @@ -6,6 +7,39 @@ import type DropTarget from '@uppy/drop-target'; import type { HTMLAttributes } from 'react'; import type { SxProps, Theme } from '@mui/material/styles'; +export interface DropzoneProps { + onDragEnter: (e: React.DragEvent) => void; + onDragLeave: (e: React.DragEvent) => void; + onDragOver: (e: React.DragEvent) => void; + onDrop: (e: React.DragEvent) => void; + onClick?: () => void; +} + +export type UploaderRef = { + /** 获取底层 Uppy 实例 */ + getUploader: () => Uppy; + /** 打开上传器 (Dashboard) */ + open: (pluginName?: string) => void; + /** 关闭上传器 */ + close: () => void; + /** 触发系统文件选择器 */ + triggerFileInput: (options?: { accept?: string; multiple?: boolean; autoUpload?: boolean }) => void; + /** 获取拖拽区域 props,绑定到元素即可支持拖拽+点击上传 */ + getDropzoneProps: (options?: { autoUpload?: boolean; noClick?: boolean }) => DropzoneProps; + /** 批量添加文件 */ + addFiles: (files: File[], options?: { autoUpload?: boolean }) => void; + /** 开始上传 */ + upload: () => Promise; + /** 获取当前总进度 0-100 */ + getProgress: () => number; + /** 获取当前文件列表 */ + getFiles: () => UppyFile[]; + /** 移除指定文件 */ + removeFile: (fileId: string) => void; + /** 取消所有上传 */ + cancelAll: () => void; +}; + export type UploaderProps = { id?: string; popup?: boolean; From 4f6c5845aca7bb2ea1e7bdc01faa7f7ab1a84eee Mon Sep 17 00:00:00 2001 From: leermao <447552708@qq.com> Date: Mon, 22 Dec 2025 11:53:37 +0800 Subject: [PATCH 2/4] chore: bump version --- blocklets/image-bin/CHANGELOG.md | 4 ++++ blocklets/image-bin/blocklet.yml | 2 +- blocklets/image-bin/package.json | 2 +- blocklets/image-bin/version | 2 +- packages/uploader-server/CHANGELOG.md | 4 ++++ packages/uploader-server/package.json | 2 +- packages/uploader-server/version | 2 +- packages/uploader/CHANGELOG.md | 4 ++++ packages/uploader/package.json | 2 +- packages/uploader/version | 2 +- packages/xss/CHANGELOG.md | 4 ++++ packages/xss/package.json | 2 +- packages/xss/version | 2 +- 13 files changed, 25 insertions(+), 9 deletions(-) diff --git a/blocklets/image-bin/CHANGELOG.md b/blocklets/image-bin/CHANGELOG.md index 6bf1fd0..487d99e 100644 --- a/blocklets/image-bin/CHANGELOG.md +++ b/blocklets/image-bin/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.14.18 (December 22, 2025) + +- feat(uploader): add headless API for programmatic control + ## 0.14.17 (December 20, 2025) - feat: support app shell in dashboard diff --git a/blocklets/image-bin/blocklet.yml b/blocklets/image-bin/blocklet.yml index 197b682..5ad9f1a 100644 --- a/blocklets/image-bin/blocklet.yml +++ b/blocklets/image-bin/blocklet.yml @@ -1,5 +1,5 @@ name: image-bin -version: 0.14.17 +version: 0.14.18 title: Media Kit description: Self-hosted media management that replaces expensive cloud services while keeping you in complete control of your digital assets. diff --git a/blocklets/image-bin/package.json b/blocklets/image-bin/package.json index c92f1a3..62b5b2a 100644 --- a/blocklets/image-bin/package.json +++ b/blocklets/image-bin/package.json @@ -1,6 +1,6 @@ { "name": "image-bin", - "version": "0.14.17", + "version": "0.14.18", "private": true, "scripts": { "dev": "blocklet dev", diff --git a/blocklets/image-bin/version b/blocklets/image-bin/version index 4af45e0..1f94e60 100644 --- a/blocklets/image-bin/version +++ b/blocklets/image-bin/version @@ -1 +1 @@ -0.14.17 \ No newline at end of file +0.14.18 \ No newline at end of file diff --git a/packages/uploader-server/CHANGELOG.md b/packages/uploader-server/CHANGELOG.md index 8e9bc02..5286d79 100644 --- a/packages/uploader-server/CHANGELOG.md +++ b/packages/uploader-server/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.17 (December 22, 2025) + +- feat(uploader): add headless API for programmatic control + ## 0.3.16 (December 17, 2025) - fix(uploader): improve runtime path handling and EXIF removal logic diff --git a/packages/uploader-server/package.json b/packages/uploader-server/package.json index a86ff44..d5ee812 100644 --- a/packages/uploader-server/package.json +++ b/packages/uploader-server/package.json @@ -1,6 +1,6 @@ { "name": "@blocklet/uploader-server", - "version": "0.3.16", + "version": "0.3.17", "description": "blocklet upload server", "publishConfig": { "access": "public" diff --git a/packages/uploader-server/version b/packages/uploader-server/version index b88c14d..6e46293 100644 --- a/packages/uploader-server/version +++ b/packages/uploader-server/version @@ -1 +1 @@ -0.3.16 \ No newline at end of file +0.3.17 \ No newline at end of file diff --git a/packages/uploader/CHANGELOG.md b/packages/uploader/CHANGELOG.md index 35e9427..698e4e8 100644 --- a/packages/uploader/CHANGELOG.md +++ b/packages/uploader/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.17 (December 22, 2025) + +- feat(uploader): add headless API for programmatic control + ## 0.3.16 (December 17, 2025) - fix(uploader): improve runtime path handling and EXIF removal logic diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 0e0a736..23410a1 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@blocklet/uploader", - "version": "0.3.16", + "version": "0.3.17", "description": "blocklet upload component", "publishConfig": { "access": "public" diff --git a/packages/uploader/version b/packages/uploader/version index b88c14d..6e46293 100644 --- a/packages/uploader/version +++ b/packages/uploader/version @@ -1 +1 @@ -0.3.16 \ No newline at end of file +0.3.17 \ No newline at end of file diff --git a/packages/xss/CHANGELOG.md b/packages/xss/CHANGELOG.md index dc407f9..e325912 100644 --- a/packages/xss/CHANGELOG.md +++ b/packages/xss/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.15 (December 22, 2025) + +- feat(uploader): add headless API for programmatic control + ## 0.3.14 (December 17, 2025) - fix(uploader): improve runtime path handling and EXIF removal logic diff --git a/packages/xss/package.json b/packages/xss/package.json index 47ad338..d94bff9 100644 --- a/packages/xss/package.json +++ b/packages/xss/package.json @@ -1,6 +1,6 @@ { "name": "@blocklet/xss", - "version": "0.3.14", + "version": "0.3.15", "description": "blocklet prevent xss attack", "publishConfig": { "access": "public" diff --git a/packages/xss/version b/packages/xss/version index 0010ffe..50ac577 100644 --- a/packages/xss/version +++ b/packages/xss/version @@ -1 +1 @@ -0.3.14 \ No newline at end of file +0.3.15 \ No newline at end of file From 836eebe13cdf91c5e645778913e5455fc9f484ab Mon Sep 17 00:00:00 2001 From: leermao <447552708@qq.com> Date: Mon, 22 Dec 2025 15:14:52 +0800 Subject: [PATCH 3/4] chore: bump version --- packages/uploader/src/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/uploader/src/types.ts b/packages/uploader/src/types.ts index 6a7e866..1b826ef 100644 --- a/packages/uploader/src/types.ts +++ b/packages/uploader/src/types.ts @@ -4,14 +4,14 @@ import type { DashboardOptions } from '@uppy/dashboard'; import type { TusOptions } from '@uppy/tus'; import type { ImageEditorOptions } from '@uppy/image-editor'; import type DropTarget from '@uppy/drop-target'; -import type { HTMLAttributes } from 'react'; +import type { HTMLAttributes, DragEvent } from 'react'; import type { SxProps, Theme } from '@mui/material/styles'; export interface DropzoneProps { - onDragEnter: (e: React.DragEvent) => void; - onDragLeave: (e: React.DragEvent) => void; - onDragOver: (e: React.DragEvent) => void; - onDrop: (e: React.DragEvent) => void; + onDragEnter: (e: DragEvent) => void; + onDragLeave: (e: DragEvent) => void; + onDragOver: (e: DragEvent) => void; + onDrop: (e: DragEvent) => void; onClick?: () => void; } From 030b45719d50c74d950da51e0c45c4dd6caf3ace Mon Sep 17 00:00:00 2001 From: leermao <447552708@qq.com> Date: Mon, 22 Dec 2025 20:51:39 +0800 Subject: [PATCH 4/4] fix: lint --- packages/uploader/src/react/uploader.tsx | 11 +++-------- packages/uploader/src/types.ts | 18 +----------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/uploader/src/react/uploader.tsx b/packages/uploader/src/react/uploader.tsx index ada40a2..b6be5e1 100644 --- a/packages/uploader/src/react/uploader.tsx +++ b/packages/uploader/src/react/uploader.tsx @@ -923,6 +923,7 @@ export function Uploader({ source, }); }); + if (autoUpload) { state.uppy.upload(); } @@ -996,17 +997,11 @@ export function Uploader({ getUploader: () => state.uppy, open, close, + // Headless API triggerFileInput, getDropzoneProps, - addFiles: (files: File[], options?: { autoUpload?: boolean }) => { - addFilesToUppy(files, 'local', options?.autoUpload !== false); - }, - upload: () => state.uppy.upload(), - getProgress: () => state.uppy.getState().totalProgress, - getFiles: () => state.uppy.getFiles(), - removeFile: (fileId: string) => state.uppy.removeFile(fileId), - cancelAll: () => state.uppy.cancelAll(), + addFilesToUppy, } as UploaderRef) ); diff --git a/packages/uploader/src/types.ts b/packages/uploader/src/types.ts index 1b826ef..beb393f 100644 --- a/packages/uploader/src/types.ts +++ b/packages/uploader/src/types.ts @@ -16,28 +16,12 @@ export interface DropzoneProps { } export type UploaderRef = { - /** 获取底层 Uppy 实例 */ getUploader: () => Uppy; - /** 打开上传器 (Dashboard) */ open: (pluginName?: string) => void; - /** 关闭上传器 */ close: () => void; - /** 触发系统文件选择器 */ triggerFileInput: (options?: { accept?: string; multiple?: boolean; autoUpload?: boolean }) => void; - /** 获取拖拽区域 props,绑定到元素即可支持拖拽+点击上传 */ getDropzoneProps: (options?: { autoUpload?: boolean; noClick?: boolean }) => DropzoneProps; - /** 批量添加文件 */ - addFiles: (files: File[], options?: { autoUpload?: boolean }) => void; - /** 开始上传 */ - upload: () => Promise; - /** 获取当前总进度 0-100 */ - getProgress: () => number; - /** 获取当前文件列表 */ - getFiles: () => UppyFile[]; - /** 移除指定文件 */ - removeFile: (fileId: string) => void; - /** 取消所有上传 */ - cancelAll: () => void; + addFilesToUppy: (files: File[], source: string, autoUpload: boolean) => void; }; export type UploaderProps = {