Repository: nanxiaobei/antd-img-crop Branch: main Commit: 9ed87c0c7b5f Files: 16 Total size: 35.7 KB Directory structure: gitextract_qm1z0c07/ ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── README.zh-CN.md ├── eslint.config.mjs ├── package.json ├── postcss.config.js ├── rollup.config.ts ├── src/ │ ├── EasyCrop.tsx │ ├── ImgCrop.css │ ├── ImgCrop.tsx │ ├── constants.ts │ └── types.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules dist example index.html vite.config.mts ================================================ FILE: .prettierignore ================================================ dist ================================================ FILE: .prettierrc.js ================================================ module.exports = { singleQuote: true, plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], }; ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 nanxiaobei Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Link in bio to **widgets**, your online **home screen**. ➫ [🔗 kee.so](https://kee.so/)
--- # antd-img-crop An image cropper for Ant Design [Upload](https://ant.design/components/upload/) [![npm](https://img.shields.io/npm/v/antd-img-crop.svg?style=flat-square)](https://www.npmjs.com/package/antd-img-crop) [![npm](https://img.shields.io/npm/dt/antd-img-crop?style=flat-square)](https://www.npmtrends.com/antd-img-crop) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/antd-img-crop?style=flat-square)](https://bundlephobia.com/result?p=antd-img-crop) [![GitHub](https://img.shields.io/github/license/nanxiaobei/antd-img-crop?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) [![npm type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/src/types.ts) English | [简体中文](./README.zh-CN.md) ## Demo [![Edit antd-img-crop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/antd-img-crop-5x4j3r) ## Install ```sh pnpm add antd-img-crop # or yarn add antd-img-crop # or npm i antd-img-crop ``` ## Usage ```jsx harmony import { Upload } from 'antd'; import ImgCrop from 'antd-img-crop'; const Demo = () => ( + Add image ); ``` ## Props | Prop | Type | Default | Description | | -------------- | ---------- | -------------- | -------------------------------------------------------------------------------- | | quality | `number` | `0.4` | Cropped image quality, `0` to `1` | | fillColor | `string` | `'white'` | Fill color for cropped image | | zoomSlider | `boolean` | `true` | Enable zoom | | rotationSlider | `boolean` | `false` | Enable rotation | | aspectSlider | `boolean` | `false` | Enable aspect | | showReset | `boolean` | `false` | Show reset button to reset zoom & rotation & aspect | | resetText | `string` | `Reset` | Reset button text | | aspect | `number` | `1 / 1` | Aspect of crop area , `width / height` | | minZoom | `number` | `1` | Minimum zoom | | maxZoom | `number` | `3` | Maximum zoom | | minAspect | `number` | `0.5` | Minimum aspect | | maxAspect | `number` | `2` | Maximum aspect | | cropShape | `string` | `'rect'` | Shape of crop area, `'rect'` or `'round'` | | showGrid | `boolean` | `false` | Show grid of crop area (third-lines) | | cropperProps | `object` | - | [react-easy-crop] props (\* existing props cannot be overridden) | | modalClassName | `string` | `''` | Modal classname | | modalTitle | `string` | `'Edit image'` | Modal title | | modalWidth | `number` | `string` | Modal width | | modalOk | `string` | | Ok button text | | modalCancel | `string` | | Cancel button text | | onModalOk | `function` | - | Callback of click ok button | | onModalCancel | `function` | - | Callback of click cancel button (or modal mask & top right "x") | | modalProps | `object` | | [Ant Design Modal] props (\* existing props cannot be overridden) | | beforeCrop | `function` | - | Callback before crop modal, if return `false` or `reject()`, modal will not open | ## FAQ ### `ConfigProvider` not work? Try to set `libraryDirectory`(`'es'` or `'lib'`) to `babel-plugin-import` config, see which one will work. ```js module.exports = { plugins: [ ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }], ], }; ``` ### No style? (only `antd<=v4`) If you use `antd<=v4` + `babel-plugin-import`, and no `Modal` or `Slider` were imported, please import these styles manually: ```js import 'antd/es/modal/style'; import 'antd/es/slider/style'; ``` ## License [MIT License](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/) [react-easy-crop]: https://github.com/ValentinH/react-easy-crop#props [Ant Design Modal]: https://ant.design/components/modal#api ================================================ FILE: README.zh-CN.md ================================================
Link in bio to **widgets**, your online **home screen**. ➫ [🔗 kee.so](https://kee.so/)
--- # antd-img-crop 图片裁切工具,用于 Ant Design [Upload](https://ant.design/components/upload-cn/) 组件 [![npm](https://img.shields.io/npm/v/antd-img-crop.svg?style=flat-square)](https://www.npmjs.com/package/antd-img-crop) [![npm](https://img.shields.io/npm/dt/antd-img-crop?style=flat-square)](https://www.npmtrends.com/antd-img-crop) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/antd-img-crop?style=flat-square)](https://bundlephobia.com/result?p=antd-img-crop) [![GitHub](https://img.shields.io/github/license/nanxiaobei/antd-img-crop?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) [![npm type definitions](https://img.shields.io/npm/types/typescript?style=flat-square)](https://github.com/nanxiaobei/antd-img-crop/blob/main/src/types.ts) [English](./README.md) | 简体中文 ## 示例 [![Edit antd-img-crop](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/antd-img-crop-5x4j3r) ## 安装 ```sh pnpm add antd-img-crop # or yarn add antd-img-crop # or npm i antd-img-crop ``` ## 使用 ```jsx harmony import { Upload } from 'antd'; import ImgCrop from 'antd-img-crop'; const Demo = () => ( + Add image ); ``` ## Props | 属性 | 类型 | 默认 | 说明 | | -------------- | -------------------- | ------------ | ---------------------------------------------------------------- | | quality | `number` | `0.4` | 裁切图片质量,`0` 到 `1` 之间 | | fillColor | `string` | `'white'` | 裁切图像填充色 | | zoomSlider | `boolean` | `true` | 允许缩放 | | rotationSlider | `boolean` | `false` | 允许旋转 | | aspectSlider | `boolean` | `false` | 允许调整裁切比 | | showReset | `boolean` | `false` | 显示重置按钮,重置缩放 & 旋转 & 裁切比 | | resetText | `string` | `重置` | 重置按钮文字 | | aspect | `number` | `1 / 1` | 裁切区域宽高比,`width / height` | | minZoom | `number` | `1` | 最小缩放 | | maxZoom | `number` | `3` | 最大缩放 | | minAspect | `number` | `0.5` | 最小裁切比 | | maxAspect | `number` | `2` | 最大裁切比 | | cropShape | `string` | `'rect'` | 裁切区域形状,`'rect'` 或 `'round'` | | showGrid | `boolean` | `false` | 显示裁切区域网格(九宫格) | | cropperProps | `object` | - | [react-easy-crop] 的 props(\* 已有 props 无法重写) | | modalClassName | `string` | `''` | 弹窗 className | | modalTitle | `string` | `'编辑图片'` | 弹窗标题 | | modalWidth | `number` \| `string` | | 弹窗宽度 | | modalOk | `string` | | 确定按钮文字 | | modalCancel | `string` | | 取消按钮文字 | | onModalOK | `function` | - | 点击确定按钮的回调 | | onModalCancel | `function` | - | 点击取消按钮、遮罩层、或右上角 'x' 的回调 | | modalProps | `object` | | [Ant Design Modal] 的 props(\* 已有 props 无法重写) | | beforeCrop | `function` | - | 裁切弹窗打开前的回调,若返回 `false` 或 `reject`,弹窗将不会打开 | ## FAQ ### `ConfigProvider` 无效? 尝试设置 `libraryDirectory`(`'es'` 或 `'lib'`)到 `babel-plugin-import` 的配置项,看看哪个会生效。 ```js module.exports = { plugins: [ ['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }], ], }; ``` ### 没有样式?(仅 `antd<=v4`) 若使用 `antd<=v4` + `babel-plugin-import`,且未引入 `Modal` 或 `Slider`,请手动引入这些样式: ```js import 'antd/es/modal/style'; import 'antd/es/slider/style'; ``` ## 协议 [MIT License](https://github.com/nanxiaobei/antd-img-crop/blob/main/LICENSE) (c) [nanxiaobei](https://lee.so/) [react-easy-crop]: https://github.com/ValentinH/react-easy-crop#props [Ant Design Modal]: https://ant.design/components/modal-cn#api ================================================ FILE: eslint.config.mjs ================================================ import { fixupConfigRules } from '@eslint/compat'; import { FlatCompat } from '@eslint/eslintrc'; import js from '@eslint/js'; import { defineConfig, globalIgnores } from 'eslint/config'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all, }); export default defineConfig([ globalIgnores(['**/dist']), { extends: fixupConfigRules(compat.extends('react-app', 'prettier')), }, ]); ================================================ FILE: package.json ================================================ { "name": "antd-img-crop", "version": "4.30.0", "description": "An image cropper for Ant Design Upload", "keywords": [ "react", "antd", "ant-design", "upload", "image", "crop", "cropper" ], "license": "MIT", "author": "nanxiaobei (https://github.com/nanxiaobei)", "homepage": "https://github.com/nanxiaobei/antd-img-crop", "repository": "github:nanxiaobei/antd-img-crop", "bugs": "https://github.com/nanxiaobei/antd-img-crop/issues", "types": "./dist/antd-img-crop.d.ts", "module": "./dist/antd-img-crop.esm.js", "main": "./dist/antd-img-crop.cjs.js", "exports": { ".": { "types": "./dist/antd-img-crop.d.ts", "import": "./dist/antd-img-crop.esm.js", "require": "./dist/antd-img-crop.cjs.js" } }, "files": [ "dist" ], "scripts": { "build": "rm -rf dist && rollup -c --configPlugin @rollup/plugin-typescript --bundleConfigAsCjs" }, "peerDependencies": { "antd": ">=4.0.0", "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "dependencies": { "react-easy-crop": "^5.5.6", "tslib": "^2.8.1" }, "devDependencies": { "@eslint/compat": "^2.0.3", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-typescript": "^12.3.0", "@tailwindcss/postcss": "^4.2.1", "@types/node": "^25.5.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "^8.57.0", "@vitejs/plugin-react": "^6.0.0", "antd": "^6.3.2", "autoprefixer": "^10.4.27", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "postcss": "^8.5.8", "prettier": "^3.8.1", "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-tailwindcss": "^0.7.2", "react": "19.2.4", "react-dom": "19.2.4", "rollup": "4.59.0", "rollup-plugin-dts": "^6.4.0", "rollup-plugin-postcss": "^4.0.2", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "vite": "^8.0.0" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { '@tailwindcss/postcss': {}, }, }; ================================================ FILE: rollup.config.ts ================================================ import nodeResolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import typescript from '@rollup/plugin-typescript'; import type { RollupOptions } from 'rollup'; import dts from 'rollup-plugin-dts'; import postcss from 'rollup-plugin-postcss'; import pkg from './package.json'; const input = 'src/ImgCrop.tsx'; const cjsOutput = { file: pkg.main, format: 'cjs', exports: 'auto' } as const; const esmOutput = { file: pkg.module, format: 'es' } as const; const dtsOutput = { file: pkg.types, format: 'es' } as const; const nodePlugin = nodeResolve(); const tsPlugin = typescript({ compilerOptions: { module: 'esnext' } }); const postcssPlugin = postcss({ minimize: true, inject: (cssVariableName) => ` (function() { if (typeof document === 'undefined') return; const style = document.createElement('style'); const meta = document.querySelector('meta[name="csp-nonce"]'); if (meta && meta.content) style.setAttribute('nonce', meta.content); style.textContent = ${cssVariableName}; document.head.appendChild(style); })(); `, }); const replacePlugin = replace({ preventAssignment: true, '/es/': '/lib/' }); const cjsPlugins = [nodePlugin, tsPlugin, postcssPlugin, replacePlugin]; const esmPlugins = [nodePlugin, tsPlugin, postcssPlugin]; const external = [ ...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }), /^react($|\/)/, /^antd($|\/)/, ]; const config: RollupOptions[] = [ { input, output: cjsOutput, plugins: cjsPlugins, external }, { input, output: esmOutput, plugins: esmPlugins, external }, { input, output: dtsOutput, plugins: [dts()], external: [/\.css$/] }, ]; export default config; ================================================ FILE: src/EasyCrop.tsx ================================================ import AntButton from 'antd/es/button'; import AntSlider from 'antd/es/slider'; import { forwardRef, memo, useCallback, useImperativeHandle, useRef, useState, } from 'react'; import Cropper from 'react-easy-crop'; import type { Area, Point } from 'react-easy-crop'; import { ASPECT_STEP, PREFIX, ROTATION_INITIAL, ROTATION_MAX, ROTATION_MIN, ROTATION_STEP, ZOOM_INITIAL, ZOOM_STEP, } from './constants'; import type { EasyCropProps, EasyCropRef } from './types'; const EasyCrop = forwardRef((props, ref) => { const { cropperRef, zoomSlider, rotationSlider, aspectSlider, showReset, resetBtnText, modalImage, aspect: propAspect, minZoom, maxZoom, minAspect, maxAspect, cropShape, showGrid, cropperProps, } = props; const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(ZOOM_INITIAL); const [rotation, setRotation] = useState(ROTATION_INITIAL); const [aspect, setAspect] = useState(propAspect); const cropPixelsRef = useRef({ width: 0, height: 0, x: 0, y: 0 }); const onCropComplete = useCallback((_: Area, croppedAreaPixels: Area) => { cropPixelsRef.current = croppedAreaPixels; }, []); const prevPropAspect = useRef(propAspect); if (prevPropAspect.current !== propAspect) { prevPropAspect.current = propAspect; setAspect(propAspect); } const isResetActive = zoom !== ZOOM_INITIAL || rotation !== ROTATION_INITIAL || aspect !== propAspect; const onReset = () => { setZoom(ZOOM_INITIAL); setRotation(ROTATION_INITIAL); setAspect(propAspect); }; useImperativeHandle(ref, () => ({ rotation, cropPixelsRef, onReset, })); const wrapperClass = '[display:flex] [align-items:center] [width:60%] [margin-inline:auto]'; const buttonClass = '[display:flex] [align-items:center] [justify-content:center] [height:32px] [width:32px] [background:transparent] [border:0] [font-family:inherit] [font-size:18px] [cursor:pointer] disabled:[opacity:20%] disabled:[cursor:default]'; const sliderClass = '[flex:1]'; return ( <> {zoomSlider && (
)} {rotationSlider && (
)} {aspectSlider && (
)} {showReset && (zoomSlider || rotationSlider || aspectSlider) && ( {resetBtnText} )} ); }); export default memo(EasyCrop); ================================================ FILE: src/ImgCrop.css ================================================ @tailwind utilities; ================================================ FILE: src/ImgCrop.tsx ================================================ import type { ModalProps } from 'antd'; import { version } from 'antd'; import AntModal from 'antd/es/modal'; import AntUpload from 'antd/es/upload'; import type { RcFile, UploadFile } from 'antd/es/upload/interface'; import type { MouseEvent, ReactNode } from 'react'; import { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; import type CropperRef from 'react-easy-crop'; import EasyCrop from './EasyCrop'; import './ImgCrop.css'; import { PREFIX, ROTATION_INITIAL } from './constants'; import type { BeforeUpload, BeforeUploadReturnType, EasyCropRef, ImgCropProps, } from './types'; // v1 >= v2 const isGeThan = (v1: string, v2: string): boolean => { const arr1 = v1.split('.').map(Number); const arr2 = v2.split('.').map(Number); const len = Math.max(arr1.length, arr2.length); for (let i = 0; i < len; i++) { const a = arr1[i] ?? 0; const b = arr2[i] ?? 0; if (a > b) return true; if (a < b) return false; } return true; }; export type { ImgCropProps } from './types'; const ImgCrop = forwardRef((props, cropperRef) => { const { quality = 0.4, fillColor = 'white', zoomSlider = true, rotationSlider = false, aspectSlider = false, showReset = false, resetText, aspect = 1, minZoom = 1, maxZoom = 3, minAspect = 0.5, maxAspect = 2, cropShape = 'rect', showGrid = false, cropperProps, modalClassName, modalTitle, modalWidth, modalOk, modalCancel, onModalOk, onModalCancel, modalProps, beforeCrop, children, } = props; const cb = useRef< Pick >({}); cb.current.onModalOk = onModalOk; cb.current.onModalCancel = onModalCancel; cb.current.beforeCrop = beforeCrop; /** * crop */ const easyCropRef = useRef(null); const getCropCanvas = useCallback( (target: ShadowRoot) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; const context = (target?.getRootNode?.() as ShadowRoot) || document; type ImgSource = CanvasImageSource & { naturalWidth: number; naturalHeight: number; }; const imgSource = context.querySelector(`.${PREFIX}-media`) as ImgSource; const { width: cropWidth, height: cropHeight, x: cropX, y: cropY, } = easyCropRef.current!.cropPixelsRef.current; if ( rotationSlider && easyCropRef.current!.rotation !== ROTATION_INITIAL ) { const { naturalWidth: imgWidth, naturalHeight: imgHeight } = imgSource; const angle = easyCropRef.current!.rotation * (Math.PI / 180); // get container for rotated image const sine = Math.abs(Math.sin(angle)); const cosine = Math.abs(Math.cos(angle)); const squareWidth = imgWidth * cosine + imgHeight * sine; const squareHeight = imgHeight * cosine + imgWidth * sine; canvas.width = squareWidth; canvas.height = squareHeight; ctx.fillStyle = fillColor; ctx.fillRect(0, 0, squareWidth, squareHeight); // rotate container const squareHalfWidth = squareWidth / 2; const squareHalfHeight = squareHeight / 2; ctx.translate(squareHalfWidth, squareHalfHeight); ctx.rotate(angle); ctx.translate(-squareHalfWidth, -squareHalfHeight); // draw rotated image const imgX = (squareWidth - imgWidth) / 2; const imgY = (squareHeight - imgHeight) / 2; ctx.drawImage( imgSource, 0, 0, imgWidth, imgHeight, imgX, imgY, imgWidth, imgHeight, ); // crop rotated image const imgData = ctx.getImageData(0, 0, squareWidth, squareHeight); canvas.width = cropWidth; canvas.height = cropHeight; ctx.putImageData(imgData, -cropX, -cropY); } else { canvas.width = cropWidth; canvas.height = cropHeight; ctx.fillStyle = fillColor; ctx.fillRect(0, 0, cropWidth, cropHeight); ctx.drawImage( imgSource, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight, ); } return canvas; }, [fillColor, rotationSlider], ); /** * upload */ const [modalOpen, setModalOpen] = useState(false); const [modalImage, setModalImage] = useState(''); const onCancel = useRef(undefined); const onOk = useRef(undefined); const runBeforeUpload = useCallback( async ({ beforeUpload, file, resolve, reject, }: { beforeUpload: BeforeUpload; file: RcFile; resolve: (parsedFile: BeforeUploadReturnType) => void; reject: (rejectErr: BeforeUploadReturnType) => void; }) => { const rawFile = file as unknown as File; if (typeof beforeUpload !== 'function') { resolve(rawFile); return; } try { // https://ant.design/components/upload-cn#api // https://github.com/ant-design/ant-design/blob/master/components/upload/Upload.tsx#L152-L178 const result = await beforeUpload(file, [file]); if (result === false) { resolve(false); } else { resolve((result !== true && result) || rawFile); } } catch (err) { reject(err as BeforeUploadReturnType); } }, [], ); const getNewBeforeUpload = useCallback( (beforeUpload: BeforeUpload) => { return ((file, fileList) => { return new Promise(async (resolve, reject) => { let processedFile = file; if (typeof cb.current.beforeCrop === 'function') { try { const result = await cb.current.beforeCrop(file, fileList); if (result === false) { return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal } if (result !== true) { processedFile = (result as unknown as RcFile) || file; // will open modal } } catch (err) { return runBeforeUpload({ beforeUpload, file, resolve, reject }); // not open modal } } // read file const reader = new FileReader(); reader.addEventListener('load', () => { if (typeof reader.result === 'string') { setModalOpen(true); setTimeout(() => { setModalImage(reader.result as string); }, 10); } }); reader.readAsDataURL(processedFile as unknown as Blob); // on modal cancel onCancel.current = () => { setModalOpen(false); setModalImage(''); easyCropRef.current!.onReset(); let hasResolveCalled = false; cb.current.onModalCancel?.((LIST_IGNORE) => { resolve(LIST_IGNORE); hasResolveCalled = true; }); if (!hasResolveCalled) { resolve(AntUpload.LIST_IGNORE); } }; // on modal confirm onOk.current = async (event: MouseEvent) => { setModalOpen(false); setModalImage(''); easyCropRef.current!.onReset(); const canvas = getCropCanvas(event.target as ShadowRoot); const { type, name, uid } = processedFile as UploadFile; canvas.toBlob( async (blob) => { const newFile = new File([blob as BlobPart], name, { type }); Object.assign(newFile, { uid }); runBeforeUpload({ beforeUpload, file: newFile as unknown as RcFile, resolve: (file) => { resolve(file); cb.current.onModalOk?.(file); }, reject: (err) => { reject(err); cb.current.onModalOk?.(err); }, }); }, type, quality, ); }; }); }) as BeforeUpload; }, [getCropCanvas, quality, runBeforeUpload], ); const getNewUpload = useCallback( (children: ReactNode) => { const upload = Array.isArray(children) ? children[0] : children; const { beforeUpload, accept, ...restUploadProps } = upload.props; return { ...upload, props: { ...restUploadProps, accept: accept || 'image/*', beforeUpload: getNewBeforeUpload(beforeUpload), }, }; }, [getNewBeforeUpload], ); /** * modal */ const modalBaseProps = useMemo(() => { const obj: Pick = {}; if (modalWidth !== undefined) obj.width = modalWidth; if (modalOk !== undefined) obj.okText = modalOk; if (modalCancel !== undefined) obj.cancelText = modalCancel; return obj; }, [modalCancel, modalOk, modalWidth]); const wrapClassName = `${PREFIX}-modal${ modalClassName ? ` ${modalClassName}` : '' }`; const lang = typeof window === 'undefined' ? '' : window.navigator.language; const isCN = lang === 'zh-CN'; const title = modalTitle || (isCN ? '编辑图片' : 'Edit image'); const resetBtnText = resetText || (isCN ? '重置' : 'Reset'); return ( <> {getNewUpload(children)} ); }); export default ImgCrop; ================================================ FILE: src/constants.ts ================================================ export const PREFIX = 'img-crop'; export const ZOOM_INITIAL = 1; export const ZOOM_STEP = 0.1; export const ROTATION_INITIAL = 0; export const ROTATION_MIN = -180; export const ROTATION_MAX = 180; export const ROTATION_STEP = 1; export const ASPECT_STEP = 0.01; ================================================ FILE: src/types.ts ================================================ import type { ModalProps, UploadProps } from 'antd'; import { ForwardedRef, JSX, MutableRefObject } from 'react'; import type { default as Cropper, CropperProps } from 'react-easy-crop'; import type { Area } from 'react-easy-crop'; export type BeforeUpload = Exclude; export type BeforeUploadReturnType = ReturnType; export type ImgCropProps = { quality?: number; fillColor?: string; zoomSlider?: boolean; rotationSlider?: boolean; aspectSlider?: boolean; showReset?: boolean; resetText?: string; aspect?: number; minZoom?: number; maxZoom?: number; minAspect?: number; maxAspect?: number; cropShape?: 'rect' | 'round'; showGrid?: boolean; cropperProps?: Omit< CropperProps, | 'image' | 'crop' | 'zoom' | 'rotation' | 'aspect' | 'minZoom' | 'maxZoom' | 'minAspect' | 'maxAspect' | 'zoomWithScroll' | 'cropShape' | 'showGrid' | 'onCropChange' | 'onZoomChange' | 'onRotationChange' | 'onCropComplete' | 'classes' | 'keyboardStep' > & Partial>; modalClassName?: string; modalTitle?: string; modalWidth?: number | string; modalOk?: string; modalCancel?: string; onModalOk?: (value: BeforeUploadReturnType) => void; onModalCancel?: (resolve: (value: BeforeUploadReturnType) => void) => void; modalProps?: Omit< ModalProps, | 'className' | 'title' | 'width' | 'okText' | 'cancelText' | 'onOk' | 'onCancel' | 'open' | 'visible' | 'wrapClassName' | 'maskClosable' | 'destroyOnHidden' >; beforeCrop?: BeforeUpload; children: JSX.Element; }; export type EasyCropRef = { rotation: number; cropPixelsRef: MutableRefObject; onReset: () => void; }; export type EasyCropProps = { cropperRef: ForwardedRef; modalImage: string; } & Required< Pick< ImgCropProps, | 'zoomSlider' | 'rotationSlider' | 'aspectSlider' | 'showReset' | 'aspect' | 'minZoom' | 'maxZoom' | 'minAspect' | 'maxAspect' | 'cropShape' | 'showGrid' > > & Pick & { resetBtnText: string }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "alwaysStrict": true, "noFallthroughCasesInSwitch": true, "noImplicitAny": true, "noImplicitOverride": true, "noImplicitReturns": false, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "strict": true, "strictBindCallApply": true, "strictFunctionTypes": true, "strictNullChecks": true, "skipLibCheck": true, "strictPropertyInitialization": true, "module": "nodenext", "moduleResolution": "nodenext", "resolveJsonModule": true, "outDir": "dist", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "jsx": "react-jsx", "jsxImportSource": "react", "lib": ["esnext", "dom", "dom.iterable"], "target": "es6" }, "exclude": ["node_modules", "dist"] }