更新 PDF 转换工具,添加文件上传和下载功能,优化路由组件样式,新增 AGENTS.md 文档

This commit is contained in:
2026-02-04 19:22:15 +08:00
parent 263218b481
commit 87d48e6fc3
6 changed files with 484 additions and 7 deletions

View File

@@ -4,7 +4,7 @@ include:
.common_env: &common_env
env:
TO_REPO: template/vite-react-template
TO_REPO: kevisual/pdf
TO_URL: git.xiongxiao.me
imports:
- https://cnb.cool/kevisual/env/-/blob/main/.env.development

4
AGENTS.md Normal file
View File

@@ -0,0 +1,4 @@
# 代码
- @tanstack/react-router"
- shadcn/ui

View File

@@ -1,15 +1,15 @@
{
"name": "vite-react",
"name": "@kevisual/pdf",
"private": true,
"version": "0.0.1",
"type": "module",
"basename": "/",
"basename": "/root/pdf",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"ui": "pnpm dlx shadcn@latest add ",
"pub": "envision deploy ./dist -k vite-react -v 0.0.1"
"pub": "envision deploy ./dist -k pdf -v 0.0.1 -y y -u"
},
"files": [
"dist"
@@ -20,14 +20,18 @@
"@kevisual/router": "0.0.70",
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-router": "^1.158.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"es-toolkit": "^1.44.0",
"lucide-react": "^0.563.0",
"nanoid": "^5.1.6",
"pdfjs-dist": "^5.4.624",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-dropzone": "^14.4.0",
"react-hook-form": "^7.71.1",
"react-pdf": "^10.3.0",
"zustand": "^5.0.11"
},
"publishConfig": {
@@ -40,6 +44,7 @@
"@tanstack/react-router-devtools": "^1.158.0",
"@tanstack/router-plugin": "^1.158.0",
"@types/node": "^25.2.0",
"@types/pdfjs-dist": "^2.10.378",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.3",

263
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
'@tanstack/react-router':
specifier: ^1.158.0
version: 1.158.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -32,15 +35,24 @@ importers:
nanoid:
specifier: ^5.1.6
version: 5.1.6
pdfjs-dist:
specifier: ^5.4.624
version: 5.4.624
react:
specifier: ^19.2.4
version: 19.2.4
react-dom:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
react-dropzone:
specifier: ^14.4.0
version: 14.4.0(react@19.2.4)
react-hook-form:
specifier: ^7.71.1
version: 7.71.1(react@19.2.4)
react-pdf:
specifier: ^10.3.0
version: 10.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
zustand:
specifier: ^5.0.11
version: 5.0.11(@types/react@19.2.10)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
@@ -63,6 +75,9 @@ importers:
'@types/node':
specifier: ^25.2.0
version: 25.2.0
'@types/pdfjs-dist':
specifier: ^2.10.378
version: 2.10.378
'@types/react':
specifier: ^19.2.10
version: 19.2.10
@@ -622,6 +637,81 @@ packages:
'@types/react':
optional: true
'@napi-rs/canvas-android-arm64@0.1.89':
resolution: {integrity: sha512-CXxQTXsjtQqKGENS8Ejv9pZOFJhOPIl2goenS+aU8dY4DygvkyagDhy/I07D1YLqrDtPvLEX5zZHt8qUdnuIpQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.89':
resolution: {integrity: sha512-k29cR/Zl20WLYM7M8YePevRu2VQRaKcRedYr1V/8FFHkyIQ8kShEV+MPoPGi+znvmd17Eqjy2Pk2F2kpM2umVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.89':
resolution: {integrity: sha512-iUragqhBrA5FqU13pkhYBDbUD1WEAIlT8R2+fj6xHICY2nemzwMUI8OENDhRh7zuL06YDcRwENbjAVxOmaX9jg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.89':
resolution: {integrity: sha512-y3SM9sfDWasY58ftoaI09YBFm35Ig8tosZqgahLJ2WGqawCusGNPV9P0/4PsrLOCZqGg629WxexQMY25n7zcvA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.89':
resolution: {integrity: sha512-NEoF9y8xq5fX8HG8aZunBom1ILdTwt7ayBzSBIwrmitk7snj4W6Fz/yN/ZOmlM1iyzHDNX5Xn0n+VgWCF8BEdA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.89':
resolution: {integrity: sha512-UQQkIEzV12/l60j1ziMjZ+mtodICNUbrd205uAhbyTw0t60CrC/EsKb5/aJWGq1wM0agvcgZV72JJCKfLS6+4w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.89':
resolution: {integrity: sha512-1/VmEoFaIO6ONeeEMGoWF17wOYZOl5hxDC1ios2Bkz/oQjbJJ8DY/X22vWTmvuUKWWhBVlo63pxLGZbjJU/heA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.89':
resolution: {integrity: sha512-ebLuqkCuaPIkKgKH9q4+pqWi1tkPOfiTk5PM1LKR1tB9iO9sFNVSIgwEp+SJreTSbA2DK5rW8lQXiN78SjtcvA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.89':
resolution: {integrity: sha512-w+5qxHzplvA4BkHhCaizNMLLXiI+CfP84YhpHm/PqMub4u8J0uOAv+aaGv40rYEYra5hHRWr9LUd6cfW32o9/A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.89':
resolution: {integrity: sha512-DmyXa5lJHcjOsDC78BM3bnEECqbK3xASVMrKfvtT/7S7Z8NGQOugvu+L7b41V6cexCd34mBWgMOsjoEBceeB1Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.89':
resolution: {integrity: sha512-WMej0LZrIqIncQcx0JHaMXlnAG7sncwJh7obs/GBgp0xF9qABjwoRwIooMWCZkSansapKGNUHhamY6qEnFN7gA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.89':
resolution: {integrity: sha512-7GjmkMirJHejeALCqUnZY3QwID7bbumOiLrqq2LKgxrdjdmxWQBTc6rcASa2u8wuWrH7qo4/4n/VNrOwCoKlKg==}
engines: {node: '>= 10'}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
@@ -1121,6 +1211,10 @@ packages:
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
'@types/pdfjs-dist@2.10.378':
resolution: {integrity: sha512-TRdIPqdsvKmPla44kVy4jv5Nt5vjMfVjbIEke1CRULIrwKNRC4lIiZvNYDJvbUMNCFPNIUcOKhXTyMJrX18IMA==}
deprecated: This is a stub types definition. pdfjs-dist provides its own type definitions, so you do not need this installed.
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
@@ -1166,6 +1260,10 @@ packages:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
attr-accept@2.2.5:
resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
engines: {node: '>=4'}
babel-dead-code-elimination@1.0.12:
resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
@@ -1197,6 +1295,9 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
@@ -1243,6 +1344,10 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
@@ -1313,6 +1418,10 @@ packages:
picomatch:
optional: true
file-selector@2.1.2:
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
engines: {node: '>= 12'}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -1527,6 +1636,20 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
make-cancellable-promise@2.0.0:
resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==}
make-event-props@2.0.0:
resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==}
merge-refs@2.0.0:
resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1540,6 +1663,9 @@ packages:
engines: {node: ^18 || >=20}
hasBin: true
node-readable-to-web-readable-stream@0.4.2:
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
@@ -1581,6 +1707,14 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pdfjs-dist@5.4.296:
resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
engines: {node: '>=20.16.0 || >=22.3.0'}
pdfjs-dist@5.4.624:
resolution: {integrity: sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==}
engines: {node: '>=20.16.0 || >=22.3.0'}
picocolors@1.1.0:
resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==}
@@ -1633,6 +1767,12 @@ packages:
react: '>= 16.3.0'
react-dom: '>= 16.3.0'
react-dropzone@14.4.0:
resolution: {integrity: sha512-8VvsHqg9WGAr+wAnP0oVErK5HOwAoTOzRsxLPzbBXrtXtFfukkxMyuvdI/lJ+5OxtsrzmvWE5Eoo3Y4hMsaxpA==}
engines: {node: '>= 10.13'}
peerDependencies:
react: '>= 16.8 || 18.0.0'
react-hook-form@7.56.4:
resolution: {integrity: sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw==}
engines: {node: '>=18.0.0'}
@@ -1667,6 +1807,16 @@ packages:
react-is@19.1.0:
resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==}
react-pdf@10.3.0:
resolution: {integrity: sha512-2LQzC9IgNVAX8gM+6F+1t/70a9/5RWThYxc+CWAmT2LW/BRmnj+35x1os5j/nR2oldyf8L+hCAMBmVKU8wrYFA==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@@ -1878,6 +2028,9 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
@@ -2430,6 +2583,54 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.6
'@napi-rs/canvas-android-arm64@0.1.89':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.89':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.89':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.89':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.89':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.89':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.89':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.89':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.89':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.89':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.89':
optional: true
'@napi-rs/canvas@0.1.89':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.89
'@napi-rs/canvas-darwin-arm64': 0.1.89
'@napi-rs/canvas-darwin-x64': 0.1.89
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.89
'@napi-rs/canvas-linux-arm64-gnu': 0.1.89
'@napi-rs/canvas-linux-arm64-musl': 0.1.89
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.89
'@napi-rs/canvas-linux-x64-gnu': 0.1.89
'@napi-rs/canvas-linux-x64-musl': 0.1.89
'@napi-rs/canvas-win32-arm64-msvc': 0.1.89
'@napi-rs/canvas-win32-x64-msvc': 0.1.89
optional: true
'@popperjs/core@2.11.8': {}
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.10)(react@19.2.4)':
@@ -2821,6 +3022,10 @@ snapshots:
'@types/parse-json@4.0.2': {}
'@types/pdfjs-dist@2.10.378':
dependencies:
pdfjs-dist: 5.4.624
'@types/prop-types@15.7.14': {}
'@types/react-dom@19.2.3(@types/react@19.2.10)':
@@ -2866,6 +3071,8 @@ snapshots:
dependencies:
tslib: 2.8.1
attr-accept@2.2.5: {}
babel-dead-code-elimination@1.0.12:
dependencies:
'@babel/core': 7.29.0
@@ -2910,6 +3117,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
clsx@1.2.1: {}
clsx@2.1.1: {}
@@ -2942,6 +3153,8 @@ snapshots:
deepmerge@4.3.1: {}
dequal@2.0.3: {}
detect-libc@2.0.4: {}
diff@8.0.3: {}
@@ -3013,6 +3226,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
file-selector@2.1.2:
dependencies:
tslib: 2.8.1
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -3177,12 +3394,23 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
make-cancellable-promise@2.0.0: {}
make-event-props@2.0.0: {}
merge-refs@2.0.0(@types/react@19.2.10):
optionalDependencies:
'@types/react': 19.2.10
ms@2.1.3: {}
nanoid@3.3.11: {}
nanoid@5.1.6: {}
node-readable-to-web-readable-stream@0.4.2:
optional: true
node-releases@2.0.18: {}
normalize-path@3.0.0: {}
@@ -3210,6 +3438,15 @@ snapshots:
pathe@2.0.3: {}
pdfjs-dist@5.4.296:
optionalDependencies:
'@napi-rs/canvas': 0.1.89
pdfjs-dist@5.4.624:
optionalDependencies:
'@napi-rs/canvas': 0.1.89
node-readable-to-web-readable-stream: 0.4.2
picocolors@1.1.0: {}
picocolors@1.1.1: {}
@@ -3256,6 +3493,13 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-dropzone@14.4.0(react@19.2.4):
dependencies:
attr-accept: 2.2.5
file-selector: 2.1.2
prop-types: 15.8.1
react: 19.2.4
react-hook-form@7.56.4(react@19.1.0):
dependencies:
react: 19.1.0
@@ -3278,6 +3522,21 @@ snapshots:
react-is@19.1.0: {}
react-pdf@10.3.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
clsx: 2.1.1
dequal: 2.0.3
make-cancellable-promise: 2.0.0
make-event-props: 2.0.0
merge-refs: 2.0.0(@types/react@19.2.10)
pdfjs-dist: 5.4.296
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
tiny-invariant: 1.3.3
warning: 4.0.3
optionalDependencies:
'@types/react': 19.2.10
react-refresh@0.18.0: {}
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
@@ -3479,6 +3738,10 @@ snapshots:
void-elements@3.1.0: {}
warning@4.0.3:
dependencies:
loose-envify: 1.4.0
webpack-virtual-modules@0.6.2: {}
ws@8.18.1:

View File

@@ -1,3 +1,206 @@
import { useState, useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
import * as pdfjsLib from 'pdfjs-dist'
import { Button } from '@/components/ui/button'
import { Upload, Download, FileText, Loader2 } from 'lucide-react'
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
// 设置 PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
export const Home = () => {
return <div>Home Page</div>
const [pdfFile, setPdfFile] = useState<File | null>(null)
const [pdfName, setPdfName] = useState<string>('')
const [pageCount, setPageCount] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(false)
const [converting, setConverting] = useState<boolean>(false)
const onDrop = useCallback(async (acceptedFiles: File[]) => {
const file = acceptedFiles[0]
if (file && file.type === 'application/pdf') {
setPdfFile(file)
setPdfName(file.name.replace('.pdf', ''))
// 读取 PDF 获取页数
setLoading(true)
try {
const arrayBuffer = await file.arrayBuffer()
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
setPageCount(pdf.numPages)
} catch (error) {
console.error('Error loading PDF:', error)
alert('加载 PDF 失败,请重试')
} finally {
setLoading(false)
}
}
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'application/pdf': ['.pdf']
},
multiple: false
})
const handleDownload = async () => {
if (!pdfFile) return
setConverting(true)
try {
const arrayBuffer = await pdfFile.arrayBuffer()
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
// 遍历每一页
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum)
// 设置缩放比例以获得高质量图片
const scale = 2.0
const viewport = page.getViewport({ scale })
// 创建 canvas
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.height = viewport.height
canvas.width = viewport.width
if (!context) continue
// 渲染 PDF 页面到 canvas
await page.render({
canvasContext: context,
viewport: viewport,
canvas: canvas
}).promise
// 将 canvas 转换为 blob 并下载
canvas.toBlob((blob) => {
if (blob) {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${pdfName}_${pageNum}.png`
link.click()
URL.revokeObjectURL(url)
}
}, 'image/png')
// 添加延迟避免浏览器下载限制
await new Promise(resolve => setTimeout(resolve, 200))
}
alert(`成功转换并下载 ${pdf.numPages} 张图片!`)
} catch (error) {
console.error('Error converting PDF:', error)
alert('转换 PDF 失败,请重试')
} finally {
setConverting(false)
}
}
return (
<div className="h-full bg-linear-to-br from-blue-50 to-indigo-100 p-8 overflow-auto">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-800 mb-2">
PDF
</h1>
<p className="text-gray-600">
PDF
</p>
</div>
{/* 上传区域 */}
<div
{...getRootProps()}
className={`
border-2 border-dashed rounded-lg p-12 mb-8 text-center cursor-pointer
transition-all duration-200 ease-in-out
${isDragActive
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 bg-white hover:border-blue-400 hover:bg-blue-50/50'
}
`}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center gap-4">
<Upload className={`w-16 h-16 ${isDragActive ? 'text-blue-500' : 'text-gray-400'}`} />
{isDragActive ? (
<p className="text-lg text-blue-600 font-medium">
...
</p>
) : (
<>
<p className="text-lg text-gray-700 font-medium">
PDF
</p>
<p className="text-sm text-gray-500">
PDF
</p>
</>
)}
</div>
</div>
{/* PDF 信息卡片 */}
{pdfFile && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<FileText className="w-12 h-12 text-blue-500 shrink-0" />
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-800 truncate">
{pdfFile.name}
</h3>
<div className="mt-2 space-y-1 text-sm text-gray-600">
<p>
: {(pdfFile.size / 1024 / 1024).toFixed(2)} MB
</p>
{loading ? (
<p className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
PDF...
</p>
) : (
<p>: {pageCount} </p>
)}
</div>
</div>
</div>
<Button
onClick={handleDownload}
disabled={loading || converting || pageCount === 0}
className="flex items-center gap-2"
>
{converting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Download className="w-4 h-4" />
</>
)}
</Button>
</div>
</div>
)}
{/* 说明 */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="font-semibold text-gray-800 mb-3">使</h3>
<ol className="list-decimal list-inside space-y-2 text-gray-600 text-sm">
<li> PDF </li>
<li></li>
<li>"下载图片" PNG </li>
<li>_1.png, _2.png...</li>
</ol>
</div>
</div>
</div>
)
}

View File

@@ -8,7 +8,7 @@ export const Route = createRootRoute({
function RootComponent() {
return (
<>
<div className="p-2 flex gap-2 text-lg">
<div className="p-2 flex gap-2 text-lg h-16">
<Link
to="/"
activeProps={{
@@ -20,7 +20,9 @@ function RootComponent() {
</Link>
</div>
<hr />
<Outlet />
<main className='h-[calc(100vh-64px)] overflow-hidden'>
<Outlet />
</main>
<TanStackRouterDevtools position="bottom-right" />
</>
)