diff --git a/package.json b/package.json index 2a30036..b322ae0 100644 --- a/package.json +++ b/package.json @@ -28,19 +28,19 @@ }, "dependencies": { "@ant-design/icons": "5.x", - "antd": "^5.24.7", - "antd-img-crop": "^4.24.0", - "html2canvas-pro": "^1.5.x", "@imgly/background-removal": "^1.6.0", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-slot": "^1.1.2", + "antd": "^5.24.7", + "antd-img-crop": "^4.24.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "exifreader": "^4.27.0", "gif.js": "^0.2.0", "gifuct-js": "^2.1.2", - "exifreader": "^4.27.0", + "html2canvas-pro": "^1.5.x", "jszip": "^3.10.1", "lucide-react": "^0.479.0", "next": "15.2.2", @@ -48,6 +48,7 @@ "pdf-lib": "^1.17.1", "pdfjs-dist": "4.8.69", "react": "^19.0.0", + "react-color": "^2.19.3", "react-dom": "^19.0.0", "react-pdf": "^9.2.1", "shadcn-ui": "^0.9.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c713a6..b2f37af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: react: specifier: ^19.0.0 version: 19.0.0 + react-color: + specifier: ^2.19.3 + version: 2.19.3(react@19.0.0) react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) @@ -164,7 +167,7 @@ importers: version: 4.3.4(vite@5.4.9(@types/node@22.13.10)(lightningcss@1.29.2)(terser@5.34.1)) '@vitest/coverage-v8': specifier: 3.0.8 - version: 3.0.8(vitest@3.0.8) + version: 3.0.8(vitest@3.0.8(@types/node@22.13.10)(@vitest/ui@3.0.8)(jsdom@26.0.0(canvas@3.1.0))(lightningcss@1.29.2)(terser@5.34.1)) '@vitest/ui': specifier: 3.0.8 version: 3.0.8(vitest@3.0.8) @@ -1280,6 +1283,11 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} + '@icons/material@0.2.4': + resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} + peerDependencies: + react: '*' + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1306,79 +1314,67 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -1470,28 +1466,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.2.2': resolution: {integrity: sha512-cgKWBuFMLlJ4TWcFHl1KOaVVUAF8vy4qEvX5KsNd0Yj5mhu989QFCq1WjuaEbv/tO1ZpsQI6h/0YR8bLwEi+nA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.2.2': resolution: {integrity: sha512-c3kWSOSsVL8rcNBBfOq1+/j2PKs2nsMwJUV4icUxRgGBwUOfppeh7YhN5s79enBQFU+8xRgVatFkhHU1QW7yUA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.2.2': resolution: {integrity: sha512-PXTW9PLTxdNlVYgPJ0equojcq1kNu5NtwcNjRjHAB+/sdoKZ+X8FBu70fdJFadkxFIGekQTyRvPMFF+SOJaQjw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.2.2': resolution: {integrity: sha512-nG644Es5llSGEcTaXhnGWR/aThM/hIaz0jx4MDg4gWC8GfTCp8eDBWZ77CVuv2ha/uL9Ce+nPTfYkSLG67/sHg==} @@ -1991,55 +1983,46 @@ packages: resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.24.0': resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.24.0': resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.24.0': resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.24.0': resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.24.0': resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.24.0': resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.24.0': resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.24.0': resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} @@ -2313,28 +2296,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.0.13': resolution: {integrity: sha512-GQj6TWevNxwsYw20FdT2r2d1f7uiRsF07iFvNYxPIvIyPEV74eZ0zgFEsAH1daK1OxPy+LXdZ4grV17P5tVzhQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.0.13': resolution: {integrity: sha512-sQRH09faifF9w9WS6TKDWr1oLi4hoPx0EIWXZHQK/jcjarDpXGQ2DbF0KnALJCwWBxOIP/1nrmU01fZwwMzY3g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.0.13': resolution: {integrity: sha512-Or1N8DIF3tP+LsloJp+UXLTIMMHMUcWXFhJLCsM4T7MzFzxkeReewRWXfk5mk137cdqVeUEH/R50xAhY1mOkTQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-win32-arm64-msvc@4.0.13': resolution: {integrity: sha512-u2mQyqCFrr9vVTP6sfDRfGE6bhOX3/7rInehzxNhHX1HYRIx09H3sDdXzTxnZWKOjIg3qjFTCrYFUZckva5PIg==} @@ -4731,28 +4710,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} @@ -4918,6 +4893,9 @@ packages: map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -5732,6 +5710,11 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-color@2.19.3: + resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} + peerDependencies: + react: '*' + react-confetti@6.1.0: resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} engines: {node: '>=10.18'} @@ -5818,6 +5801,11 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + reactcss@1.2.3: + resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} + peerDependencies: + react: '*' + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -6386,6 +6374,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} @@ -8212,6 +8203,10 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} + '@icons/material@0.2.4(react@19.0.0)': + dependencies: + react: 19.0.0 + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 @@ -9552,7 +9547,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.8(vitest@3.0.8)': + '@vitest/coverage-v8@3.0.8(vitest@3.0.8(@types/node@22.13.10)(@vitest/ui@3.0.8)(jsdom@26.0.0(canvas@3.1.0))(lightningcss@1.29.2)(terser@5.34.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -12362,6 +12357,8 @@ snapshots: map-or-similar@1.5.0: {} + material-colors@1.2.6: {} + math-intrinsics@1.1.0: {} md5.js@1.3.5: @@ -13314,6 +13311,17 @@ snapshots: strip-json-comments: 2.0.1 optional: true + react-color@2.19.3(react@19.0.0): + dependencies: + '@icons/material': 0.2.4(react@19.0.0) + lodash: 4.17.21 + lodash-es: 4.17.21 + material-colors: 1.2.6 + prop-types: 15.8.1 + react: 19.0.0 + reactcss: 1.2.3(react@19.0.0) + tinycolor2: 1.6.0 + react-confetti@6.1.0(react@19.0.0): dependencies: react: 19.0.0 @@ -13404,6 +13412,11 @@ snapshots: react@19.0.0: {} + reactcss@1.2.3(react@19.0.0): + dependencies: + lodash: 4.17.21 + react: 19.0.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -14090,6 +14103,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.0: {} tinyexec@0.3.2: {} diff --git a/src/app/image/id-photo/page.tsx b/src/app/image/id-photo/page.tsx new file mode 100644 index 0000000..251e944 --- /dev/null +++ b/src/app/image/id-photo/page.tsx @@ -0,0 +1,410 @@ +'use client'; + +import { RotateCw, Trash2, Upload, Download } from 'lucide-react'; +import { useState, useRef } from 'react'; +import { removeBackground } from '@imgly/background-removal'; +import JSZip from 'jszip'; + +//证件照制作 +const IDPhotoPage = () => { + // 支持的图片格式 + const SUPPORTED_FORMATS = [ + { + name: 'JPG', + mimeType: 'image/jpeg', + extension: '.jpg', + tips: '适合照片,有损压缩,不支持透明背景', + }, + { + name: 'PNG', + mimeType: 'image/png', + extension: '.png', + tips: '支持透明背景,无损压缩,文件较大', + }, + ]; + // 证件照标准尺寸配置 + const STANDARD_SIZES = [ + { label: '一寸', width: 25, height: 35 }, // 实际为 25mm×35mm + { label: '二寸', width: 35, height: 45 }, + { label: '小二寸', width: 33, height: 48 }, + ]; + //背景颜色配置 + const STANDARD_COLORS = [ + { label: '白色', color: '#ffffff' }, + { label: '黑色', color: '#000000' }, + { label: '红色', color: '#ff0000' }, + { label: '绿色', color: '#00ff00' }, + { label: '蓝色', color: '#0000ff' }, + { label: '紫色', color: '#800080' }, + { label: '黄色', color: '#ffff00' }, + ]; + const [files, setFiles] = useState([]); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + const [converting, setConverting] = useState(false); + const [output, setOutput] = useState<{ name: string; url: string }[]>([]); + const [selectedFormat, setSelectedFormat] = useState(STANDARD_SIZES[0]); + const [selectedColor, setSelectedColor] = useState(STANDARD_COLORS[0]); + const formatNameList = SUPPORTED_FORMATS.map((format) => format.name); + + const handleFileUpload = (e: React.ChangeEvent) => { + // console.log(e.target.files); + if (!e.target.files?.length) return; + + const newFiles = Array.from(e.target.files).filter((file) => file.type.startsWith('image/')); + setFiles((prev) => [...prev, ...newFiles]); + setOutput([]); + + // 清空文件输入,以便可以再次选择相同的文件 + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + // 处理文件拖拽 + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const droppedFiles = Array.from(e.dataTransfer.files).filter((file) => + file.type.startsWith('image/'), + ); + + if (droppedFiles.length > 0) { + setFiles((prev) => [...prev, ...droppedFiles]); + setOutput([]); + } + }; + // 清空所有文件 + const clearFiles = () => { + setFiles([]); + setOutput([]); + }; + // 删除选定的文件 + const removeFile = (index: number) => { + setFiles(files.filter((_, i) => i !== index)); + }; + const handleConvert = async () => { + if (!files.length) return; + setConverting(true); + + try { + const convertedFiles = await Promise.all( + files.map(async (file) => { + try { + // 1. 先进行背景移除 + const processedBlob = await removeBackground(file); + console.log(processedBlob); + + const processedUrl = URL.createObjectURL(processedBlob); + + // 创建处理后的图片对象 + const img = new Image(); + img.src = processedUrl; + await new Promise((resolve) => (img.onload = resolve)); + + // 创建画布并设置尺寸 + const canvas = document.createElement('canvas'); + const dpi = 300; // 标准证件照分辨率 + const mmToInch = 25.4; + canvas.width = (selectedFormat.width / mmToInch) * dpi; + canvas.height = (selectedFormat.height / mmToInch) * dpi; + + const ctx = canvas.getContext('2d')!; + + // 先填充背景色 + ctx.fillStyle = selectedColor.color; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 计算图片缩放和位置 + const scale = Math.max(canvas.width / img.width, canvas.height / img.height); + const x = (canvas.width - img.width * scale) / 2; + const y = (canvas.height - img.height * scale) / 2; + + // 绘制处理后的透明背景图片 + ctx.drawImage(img, x, y, img.width * scale, img.height * scale); + + // 转换为目标格式 + const format = SUPPORTED_FORMATS.find((f) => f.name === 'PNG')!; // 默认使用PNG保持透明度 + const blob = await new Promise((resolve) => + canvas.toBlob(resolve, format.mimeType), + ); + + return { + name: `${file.name.split('.')[0]}_${selectedFormat.label}${format.extension}`, + url: URL.createObjectURL(blob!), + }; + } catch (error) { + console.error(`处理文件 ${file.name} 失败:`, error); + + return null; + } + }), + ); + setOutput(convertedFiles.filter(Boolean) as { name: string; url: string }[]); + } catch (error) { + console.error('转换失败:', error); + } finally { + setConverting(false); + } + }; + const downloadFile = (file: { name: string; url: string }) => { + const a = document.createElement('a'); + a.href = file.url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }; + + const downloadAllFiles = async () => { + if (!output.length) return; + + if (output.length === 1) { + // 只有一个文件,直接下载 + downloadFile(output[0]); + + return; + } + + // 多个文件,创建zip压缩包 + const zip = new JSZip(); + + try { + for (const file of output) { + const response = await fetch(file.url); + const blob = await response.blob(); + zip.file(file.name, blob); + } + + const content = await zip.generateAsync({ type: 'blob' }); + const zipUrl = URL.createObjectURL(content); + + const a = document.createElement('a'); + a.href = zipUrl; + a.download = `证件照_${new Date().getTime()}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + URL.revokeObjectURL(zipUrl); + } catch (error) { + console.error('创建压缩包失败:', error); + alert('下载文件时出错,请重试'); + } + }; + + return ( +
+

证件照制作

+ {/* 上传区域 */} +
+ + +
+ {/* 已上传文件列表 */} + {files.length > 0 && ( +
+
+

已上传的文件 ({files.length})

+ +
+
+ {files.map((file, index) => ( +
+
+ {file.name} +
+
+

+ {file.name} +

+ +
+
+ ))} +
+
+ )} + {/* 证件照标准尺寸配置和转换按钮 */} + {files.length > 0 && ( +
+
+
+

证件照标准尺寸

+
+ {STANDARD_SIZES.map((format) => ( + + ))} +
+
+ {STANDARD_COLORS.map((color) => ( + + ))} +
+
+ +
+
+ )} + {/* 生成结果 */} + {output.length > 0 && ( +
+
+

转换结果

+ +
+
+ {output.map((file, index) => ( +
+
+ {file.name} +
+ +
+
+
+

+ {file.name} +

+ + {STANDARD_SIZES.find((format) => format.label === file.name)?.label} + +
+
+ ))} +
+
+ )} +
+ ); +}; + +export default IDPhotoPage;