update
This commit is contained in:
@@ -6,6 +6,22 @@ const nextConfig = {
|
||||
// 在构建过程中忽略ESLint错误
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// 处理PDF.js在SSR中的问题
|
||||
if (isServer) {
|
||||
config.externals.push({
|
||||
'pdfjs-dist': 'commonjs pdfjs-dist',
|
||||
});
|
||||
}
|
||||
|
||||
// 处理canvas模块
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
canvas: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
|
||||
Generated
+358
-2
@@ -14,18 +14,23 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@uiw/react-json-view": "^2.0.0-alpha.30",
|
||||
"compressorjs": "^1.2.1",
|
||||
"cron-parser": "^5.1.1",
|
||||
"cronstrue": "^2.59.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"fabric": "^6.6.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"marked": "^15.0.7",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "15.2.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.54",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-pdf": "^10.0.1",
|
||||
"react-qrcode-logo": "^3.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
"vanilla-jsoneditor": "^3.1.1"
|
||||
@@ -1009,6 +1014,191 @@
|
||||
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.74.tgz",
|
||||
"integrity": "sha512-pOIyzuS+5Bz1vAhD7tdhaw5/936mMJZUn4aVajojUdjYOGSWmfpDYSgt0nQLZPZVN5GLgWgutqXPOi7Jsm3k+Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.74",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.74",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.74",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.74",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.74",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.74",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.74",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.74",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.74",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.74"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.74.tgz",
|
||||
"integrity": "sha512-aq5ode+9Z/ZR0H485dI2jdRdttg/hl9Ob+iPCt0nj+QFiirpxDrbUHKeTZWQWEtkWyC7vI5R2dMTbDINBfl9eg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.74.tgz",
|
||||
"integrity": "sha512-eO5Miz+ef1dEQyUMWDdcbAb1Wr7yMyxD9/CL9d4frQxO4pTTaCiMBUWup8XDPLr/g7XkSkGCZLP47xiXiyXSpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.74.tgz",
|
||||
"integrity": "sha512-0EkO0IFkps7C3JpKC7lbM3IL+QDUYeUKagHLDbUry4PeQTghxp6JcgccpmU32ZbpFZgPnm7o0tTJO0J1d8S2rA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.74.tgz",
|
||||
"integrity": "sha512-qAVJEN2JqGayEI1kSpJy1Xr6ZmCFV9QhRyV35yWsS7e9X1jm+T4DAlCxI4PlKIlqVSzYMYhKrxchST20XBSzHg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.74.tgz",
|
||||
"integrity": "sha512-lOnop22qy6MYxI94GGunMMjo6D80I//2W/6pqKUfwXaDQtOfvHsTcVVzDu5cFXUTNrb9ZRfMCeol5YEd+9FJvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.74.tgz",
|
||||
"integrity": "sha512-tfFqLHGtSEabBigOnPUfZviSTGmW2xHv5tYZYPBWmgGiTkoNJ7lEWFUxHjwvV5HXGqLs8ok/O7g1enSpxO6lmQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.74.tgz",
|
||||
"integrity": "sha512-j6H9dHTMtr1y3tu/zGm1ythYIL9vTl4EEv9f6CMx0n3Zn2M+OruUUwh9ylCj4afzSNEK9T8cr6zMnmTPzkpBvQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.74.tgz",
|
||||
"integrity": "sha512-73DIV4E7Y9CpIJuUXVl9H6+MEQXyRy4VJQoUGA1tOlcKQiStxqhq6UErL4decI28NxjyQXBhtYZKj5q8AJEuOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.74.tgz",
|
||||
"integrity": "sha512-FgDMEFdGIJT3I2xejflRJ82/ZgDphyirS43RgtoLaIXI6zihLiZcQ7rczpqeWgAwlJNjR0He2EustsKe1SkUOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.74",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.74.tgz",
|
||||
"integrity": "sha512-x6bhwlhn0wU7dfiP46mt5Bi6PowSUH4CJ4PTzGj58LRQ1HVasEIJgoMx7MLC48F738eJpzbfg3WR/D8+e9CeTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz",
|
||||
@@ -1214,6 +1404,24 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/standard-fonts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/upng": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -1313,6 +1521,12 @@
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/file-saver": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -1348,7 +1562,7 @@
|
||||
"version": "19.0.12",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.0.12.tgz",
|
||||
"integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -2704,7 +2918,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@@ -2881,6 +3095,15 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
@@ -3766,6 +3989,12 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -5258,6 +5487,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-cancellable-promise": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
|
||||
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -5284,6 +5522,15 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/make-event-props": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/make-event-props/-/make-event-props-2.0.0.tgz",
|
||||
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.7.tgz",
|
||||
@@ -5312,6 +5559,23 @@
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-refs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/merge-refs/-/merge-refs-2.0.0.tgz",
|
||||
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -5929,6 +6193,12 @@
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -6009,6 +6279,36 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmmirror.com/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.4.54",
|
||||
"resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-5.4.54.tgz",
|
||||
"integrity": "sha512-TBAiTfQw89gU/Z4LW98Vahzd2/LoCFprVGvGbTgFt+QCB1F+woyOPmNNVgLa6djX9Z9GGTnj7qE1UzpOVJiINw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.16.0 || >=22.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.74"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6332,6 +6632,47 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-pdf": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-pdf/-/react-pdf-10.0.1.tgz",
|
||||
"integrity": "sha512-dblLZ5BqubeNFZwTzHkRi7Rexev2+Xb9z7sEvNYT+IOxmih1KgrByeqD4Hm0RnwR9nTWludXbiHQMe9d47DK2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"dequal": "^2.0.3",
|
||||
"make-cancellable-promise": "^2.0.0",
|
||||
"make-event-props": "^2.0.0",
|
||||
"merge-refs": "^2.0.0",
|
||||
"pdfjs-dist": "5.3.31",
|
||||
"tiny-invariant": "^1.0.0",
|
||||
"warning": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-pdf/node_modules/pdfjs-dist": {
|
||||
"version": "5.3.31",
|
||||
"resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-5.3.31.tgz",
|
||||
"integrity": "sha512-EhPdIjNX0fcdwYQO+e3BAAJPXt+XI29TZWC7COhIXs/K0JHcUt1Gdz1ITpebTwVMFiLsukdUZ3u0oTO7jij+VA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.16.0 || >=22.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.67"
|
||||
}
|
||||
},
|
||||
"node_modules/react-qrcode-logo": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-qrcode-logo/-/react-qrcode-logo-3.0.0.tgz",
|
||||
@@ -7477,6 +7818,12 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.12.tgz",
|
||||
@@ -7896,6 +8243,15 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
||||
@@ -15,18 +15,23 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@uiw/react-json-view": "^2.0.0-alpha.30",
|
||||
"compressorjs": "^1.2.1",
|
||||
"cron-parser": "^5.1.1",
|
||||
"cronstrue": "^2.59.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"fabric": "^6.6.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"marked": "^15.0.7",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "15.2.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.54",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-pdf": "^10.0.1",
|
||||
"react-qrcode-logo": "^3.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
"vanilla-jsoneditor": "^3.1.1"
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -681,7 +681,7 @@ export default function Home() {
|
||||
{/* 主内容区域 */}
|
||||
<main className="flex-1 container mx-auto">
|
||||
{/* 分类导航 - 独立导航栏 */}
|
||||
<div className="flex flex-wrap nav-bar gap-2 sm:gap-4 mb-6 sm:mb-8 mt-6">
|
||||
<div className="flex flex-wrap nav-bar gap-2 sm:gap-3 mb-6 sm:mb-8 mt-6">
|
||||
{allCategories.map((category, index) => (
|
||||
<button
|
||||
key={index}
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faUpload,
|
||||
faSpinner,
|
||||
faDownload,
|
||||
faCopy,
|
||||
faCheck,
|
||||
faTrash,
|
||||
faFileCode
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import ToolHeader from '@/components/ToolHeader';
|
||||
import BackToTop from '@/components/BackToTop';
|
||||
import tools from '@/config/tools';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
|
||||
// 转换结果接口
|
||||
interface ConversionResult {
|
||||
markdown_content: string;
|
||||
conversion_time_seconds: number;
|
||||
original_filename?: string;
|
||||
}
|
||||
|
||||
// CSS样式
|
||||
const styles = {
|
||||
container: "min-h-screen flex flex-col max-w-[1440px] mx-auto p-4 md:p-6",
|
||||
card: "card p-6",
|
||||
actionBar: "flex flex-wrap items-center justify-between gap-3 p-4 bg-block rounded-lg border border-purple-glow mb-6",
|
||||
uploadSection: "flex flex-col items-center p-6 bg-block rounded-lg border-2 border-dashed border-purple-glow/50 cursor-pointer hover:border-purple-glow transition-colors mb-6",
|
||||
uploadIcon: "text-purple text-3xl mb-4",
|
||||
uploadText: "text-center text-secondary mb-2",
|
||||
supportedFormats: "text-center text-sm text-tertiary",
|
||||
hiddenInput: "hidden",
|
||||
loading: "flex flex-col items-center justify-center p-6 bg-block rounded-lg mb-6",
|
||||
spinner: "animate-spin text-purple text-2xl mb-4",
|
||||
loadingText: "text-secondary",
|
||||
fileName: "text-sm text-secondary max-w-full truncate",
|
||||
fileInfo: "flex items-center p-4 bg-block-strong rounded-lg border border-purple-glow/30 mb-4",
|
||||
fileIcon: "text-purple mr-3 text-xl",
|
||||
fileDetails: "flex-1",
|
||||
fileActions: "flex gap-2",
|
||||
resultContainer: "w-full",
|
||||
resultHeader: "flex items-center justify-between mb-4",
|
||||
resultTitle: "text-lg font-medium text-primary",
|
||||
markdownOutput: "w-full h-[500px] p-4 bg-block-strong border border-purple-glow/30 rounded-lg text-primary font-mono text-sm resize-y overflow-auto focus:outline-none focus:border-purple focus:ring-1 focus:ring-purple",
|
||||
buttonGroup: "flex flex-wrap gap-3",
|
||||
actionButton: "btn-secondary flex items-center",
|
||||
convertButton: "btn-primary flex items-center",
|
||||
iconMargin: "mr-2",
|
||||
infoBox: "p-4 bg-block rounded-lg border border-purple-glow/30 mb-6",
|
||||
infoText: "text-tertiary text-sm",
|
||||
errorContainer: "p-4 bg-red-500/10 border border-red-500/30 rounded-lg text-red-500 mb-6",
|
||||
successContainer: "p-4 bg-green-500/10 border border-green-500/30 rounded-lg text-green-500 mb-6",
|
||||
warningText: "text-yellow-500 text-sm mt-2",
|
||||
};
|
||||
|
||||
// 支持的文件格式
|
||||
const SUPPORTED_FILE_FORMATS = [
|
||||
'.docx', '.pdf', '.pptx', '.xlsx', '.html', '.htm', '.rtf', '.txt', '.csv', '.json', '.xml', '.epub', '.md'
|
||||
];
|
||||
|
||||
// 文件大小限制(50MB)
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
export default function FileToMarkdownConverter() {
|
||||
// 使用语言上下文
|
||||
const { t } = useLanguage();
|
||||
|
||||
// 从工具配置中获取当前工具信息
|
||||
const toolConfig = tools.find(tool => tool.code === 'file_to_markdown_converter');
|
||||
|
||||
// 状态管理
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [markdown, setMarkdown] = useState<string>('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isFileTooLarge, setIsFileTooLarge] = useState(false);
|
||||
|
||||
// 引用
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// 提交文件进行转换
|
||||
const handleSubmit = async () => {
|
||||
if (!file) {
|
||||
setError(t('tools.file_to_markdown_converter.no_file_selected'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置加载状态
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setMarkdown('');
|
||||
|
||||
try {
|
||||
// 使用API客户端上传文件
|
||||
const result = await apiClient.uploadFile<ConversionResult>('/api/markdown-convert', file);
|
||||
|
||||
// 更新状态
|
||||
setMarkdown(result.markdown_content);
|
||||
setSuccess(t('tools.file_to_markdown_converter.conversion_success').replace('{time}', result.conversion_time_seconds.toFixed(2)));
|
||||
} catch (err) {
|
||||
console.error(t('tools.file_to_markdown_converter.conversion_error'), err);
|
||||
setError((err as Error).message || t('tools.file_to_markdown_converter.conversion_error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = event.target.files?.[0];
|
||||
if (!selectedFile) return;
|
||||
|
||||
// 检查文件格式
|
||||
const fileExtension = selectedFile.name.toLowerCase().split('.').pop();
|
||||
if (fileExtension === 'doc' || fileExtension === 'ppt' || fileExtension === 'xls') {
|
||||
setError(t('tools.file_to_markdown_converter.old_office_format').replace('{format}', fileExtension));
|
||||
setIsFileTooLarge(true); // 使用这个状态来禁用转换按钮
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (selectedFile.size > MAX_FILE_SIZE) {
|
||||
setIsFileTooLarge(true);
|
||||
setError(t('tools.file_to_markdown_converter.file_too_large').replace('{size}', (selectedFile.size / (1024 * 1024)).toFixed(2)));
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
setMarkdown('');
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setCopied(false);
|
||||
setIsFileTooLarge(false);
|
||||
|
||||
// 设置选中的文件
|
||||
setFile(selectedFile);
|
||||
};
|
||||
|
||||
// 打开文件选择器
|
||||
const handleSelectFile = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
// 处理拖放文件
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const droppedFile = event.dataTransfer.files[0];
|
||||
if (droppedFile) {
|
||||
// 检查文件格式
|
||||
const fileExtension = droppedFile.name.toLowerCase().split('.').pop();
|
||||
if (fileExtension === 'doc' || fileExtension === 'ppt' || fileExtension === 'xls') {
|
||||
setError(t('tools.file_to_markdown_converter.old_office_format').replace('{format}', fileExtension));
|
||||
setIsFileTooLarge(true); // 使用这个状态来禁用转换按钮
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (droppedFile.size > MAX_FILE_SIZE) {
|
||||
setIsFileTooLarge(true);
|
||||
setError(t('tools.file_to_markdown_converter.file_too_large').replace('{size}', (droppedFile.size / (1024 * 1024)).toFixed(2)));
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
setMarkdown('');
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setCopied(false);
|
||||
setIsFileTooLarge(false);
|
||||
|
||||
// 设置拖放的文件
|
||||
setFile(droppedFile);
|
||||
}
|
||||
};
|
||||
|
||||
// 防止默认拖放行为
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// 复制Markdown到剪贴板
|
||||
const copyToClipboard = () => {
|
||||
if (!markdown) return;
|
||||
|
||||
navigator.clipboard.writeText(markdown)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(t('tools.file_to_markdown_converter.copy_failed'), err);
|
||||
setError(t('tools.file_to_markdown_converter.copy_failed'));
|
||||
});
|
||||
};
|
||||
|
||||
// 下载Markdown文件
|
||||
const downloadMarkdown = () => {
|
||||
if (!markdown) return;
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
// 设置文件名,从原始文件名中提取基本名称
|
||||
const originalName = file?.name || 'document';
|
||||
const baseName = originalName.split('.').slice(0, -1).join('.') || originalName;
|
||||
link.download = `${baseName}.md`;
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// 清除选中的文件和结果
|
||||
const clearAll = () => {
|
||||
setFile(null);
|
||||
setMarkdown('');
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setCopied(false);
|
||||
setIsFileTooLarge(false);
|
||||
|
||||
// 重置文件输入
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* 工具头部 */}
|
||||
{toolConfig && (
|
||||
<ToolHeader
|
||||
title={toolConfig.title || ''}
|
||||
description={toolConfig.description || ''}
|
||||
icon={toolConfig.icon}
|
||||
toolCode="file_to_markdown_converter"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div className={styles.infoBox}>
|
||||
<p className={styles.infoText}>
|
||||
{t('tools.file_to_markdown_converter.description')}
|
||||
<strong className="block mt-2">
|
||||
{t('tools.file_to_markdown_converter.old_office_format').replace('{format}', 'doc/xls/ppt')}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className={styles.errorContainer}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 成功提示 */}
|
||||
{success && (
|
||||
<div className={styles.successContainer}>
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 始终显示上传区域 */}
|
||||
<div
|
||||
className={styles.uploadSection}
|
||||
onClick={handleSelectFile}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} className={styles.uploadIcon} />
|
||||
<p className={styles.uploadText}>
|
||||
{file ? t('tools.file_to_markdown_converter.select_file') : t('tools.file_to_markdown_converter.drop_file_here')}
|
||||
</p>
|
||||
<p className={styles.supportedFormats}>
|
||||
{t('tools.file_to_markdown_converter.supported_formats')}
|
||||
</p>
|
||||
<p className={styles.warningText}>{t('tools.file_to_markdown_converter.file_too_large').replace('{size}', '50')}</p>
|
||||
<p className={styles.warningText}>{t('tools.file_to_markdown_converter.old_office_format').replace('{format}', 'doc/xls/ppt')}</p>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className={styles.hiddenInput}
|
||||
onChange={handleFileChange}
|
||||
accept={SUPPORTED_FILE_FORMATS.join(',')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 显示选中的文件信息 */}
|
||||
{file && !isFileTooLarge && (
|
||||
<div className={styles.fileInfo}>
|
||||
<FontAwesomeIcon icon={faFileCode} className={styles.fileIcon} />
|
||||
<div className={styles.fileDetails}>
|
||||
<div className="font-medium">{file.name}</div>
|
||||
<div className="text-sm text-secondary">{t('tools.file_to_markdown_converter.file_size')}: {(file.size / 1024).toFixed(1)} KB</div>
|
||||
</div>
|
||||
<div className={styles.fileActions}>
|
||||
<button
|
||||
className={styles.convertButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faSpinner} spin className={styles.iconMargin} />
|
||||
{t('tools.file_to_markdown_converter.converting')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faFileCode} className={styles.iconMargin} />
|
||||
{t('tools.file_to_markdown_converter.convert')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.actionButton}
|
||||
onClick={clearAll}
|
||||
disabled={loading}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className={styles.iconMargin} />
|
||||
{t('tools.file_to_markdown_converter.clear')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<div className={styles.loading}>
|
||||
<FontAwesomeIcon icon={faSpinner} spin className={styles.spinner} />
|
||||
<p className={styles.loadingText}>{t('tools.file_to_markdown_converter.converting')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 转换结果 - 仅显示纯文本结果 */}
|
||||
{markdown && (
|
||||
<div className={styles.resultContainer}>
|
||||
<div className={styles.resultHeader}>
|
||||
<h3 className={styles.resultTitle}>{t('tools.file_to_markdown_converter.markdown_output')}</h3>
|
||||
<div className={styles.buttonGroup}>
|
||||
<button
|
||||
className={styles.actionButton}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={copied ? faCheck : faCopy}
|
||||
className={styles.iconMargin}
|
||||
/>
|
||||
{copied ? t('tools.file_to_markdown_converter.copied') : t('tools.file_to_markdown_converter.copy')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.actionButton}
|
||||
onClick={downloadMarkdown}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} className={styles.iconMargin} />
|
||||
{t('tools.file_to_markdown_converter.download')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.actionButton}
|
||||
onClick={clearAll}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className={styles.iconMargin} />
|
||||
{t('tools.file_to_markdown_converter.clear')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref={markdownTextAreaRef}
|
||||
className={styles.markdownOutput}
|
||||
value={markdown}
|
||||
onChange={(e) => setMarkdown(e.target.value)}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 回到顶部按钮 */}
|
||||
<BackToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export default function JsonFormatter() {
|
||||
|
||||
try {
|
||||
// 直接在原始输入字符串上移除转义斜杠
|
||||
const processed = jsonInput.replace(/\\\//g, '/');
|
||||
const processed = jsonInput.replace(/\\\/+/g, '/');
|
||||
|
||||
// 检查是否有变化
|
||||
if (processed === jsonInput) {
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import ToolHeader from '@/components/ToolHeader';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import ActionButton from '@/components/ActionButton';
|
||||
import ProgressBar from '@/components/ProgressBar';
|
||||
import { faCompress, faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface CompressionResult {
|
||||
url: string;
|
||||
filename: string;
|
||||
originalSize: number;
|
||||
compressedSize: number;
|
||||
compressionRatio: number;
|
||||
}
|
||||
|
||||
interface CompressionSettings {
|
||||
quality: 'high' | 'medium' | 'low';
|
||||
imageCompression: boolean;
|
||||
fontSubsetting: boolean;
|
||||
metadataRemoval: boolean;
|
||||
removeBookmarks: boolean;
|
||||
removeAnnotations: boolean;
|
||||
}
|
||||
|
||||
export default function PDFCompressorPage() {
|
||||
const { t } = useLanguage();
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isCompressing, setIsCompressing] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [results, setResults] = useState<CompressionResult[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pdfjsLib, setPdfjsLib] = useState<typeof import('pdfjs-dist') | null>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const resultsRef = useRef<HTMLDivElement>(null);
|
||||
const [settings, setSettings] = useState<CompressionSettings>({
|
||||
quality: 'medium',
|
||||
imageCompression: true,
|
||||
fontSubsetting: true,
|
||||
metadataRemoval: false,
|
||||
removeBookmarks: false,
|
||||
removeAnnotations: false,
|
||||
});
|
||||
|
||||
// 确保只在客户端运行
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
// 动态导入PDF.js
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
const loadPDFJS = async () => {
|
||||
try {
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/lib/pdfjs-dist/pdf.worker.min.mjs';
|
||||
setPdfjsLib(pdfjs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load PDF.js:', error);
|
||||
setError(t('tools.pdf_compressor.errors.library_failed'));
|
||||
}
|
||||
};
|
||||
|
||||
loadPDFJS();
|
||||
}, [isClient]);
|
||||
|
||||
const handleFileSelect = useCallback((selectedFiles: File[]) => {
|
||||
setFiles(selectedFiles);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const handleFileError = useCallback((errorMessage: string) => {
|
||||
setError(errorMessage);
|
||||
}, []);
|
||||
|
||||
// 压缩PDF文件
|
||||
const compressPDF = async (pdfFile: File): Promise<CompressionResult> => {
|
||||
if (!pdfjsLib) {
|
||||
throw new Error('PDF处理库未加载');
|
||||
}
|
||||
|
||||
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||
const originalSize = pdfFile.size;
|
||||
|
||||
try {
|
||||
// 使用PDF.js加载PDF
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
const numPages = pdf.numPages;
|
||||
|
||||
// 创建新的PDF文档
|
||||
const { PDFDocument } = await import('pdf-lib');
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
// 根据质量设置确定压缩参数
|
||||
const qualitySettings = {
|
||||
high: { imageQuality: 0.8, imageScale: 1.0 },
|
||||
medium: { imageQuality: 0.6, imageScale: 0.8 },
|
||||
low: { imageQuality: 0.4, imageScale: 0.6 }
|
||||
};
|
||||
|
||||
const currentQuality = qualitySettings[settings.quality];
|
||||
|
||||
// 处理每一页
|
||||
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||
setProgress((pageNum / numPages) * 80); // 80%用于页面处理
|
||||
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: currentQuality.imageScale });
|
||||
|
||||
// 创建canvas渲染页面
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) continue;
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
// 将canvas转换为图片
|
||||
const imageBlob = await new Promise<Blob>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob!);
|
||||
}, 'image/jpeg', currentQuality.imageQuality);
|
||||
});
|
||||
|
||||
// 将图片嵌入到新PDF中
|
||||
const imageBytes = await imageBlob.arrayBuffer();
|
||||
const image = await newPdfDoc.embedJpg(imageBytes);
|
||||
const pageWidth = viewport.width;
|
||||
const pageHeight = viewport.height;
|
||||
|
||||
const newPage = newPdfDoc.addPage([pageWidth, pageHeight]);
|
||||
newPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// 生成压缩后的PDF
|
||||
const compressedPdfBytes = await newPdfDoc.save({
|
||||
useObjectStreams: true,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: 20,
|
||||
updateFieldAppearances: false,
|
||||
});
|
||||
|
||||
setProgress(90);
|
||||
|
||||
// 创建Blob和URL
|
||||
const compressedBlob = new Blob([compressedPdfBytes], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(compressedBlob);
|
||||
const filename = pdfFile.name.replace('.pdf', '_compressed.pdf');
|
||||
const compressedSize = compressedBlob.size;
|
||||
const compressionRatio = ((originalSize - compressedSize) / originalSize) * 100;
|
||||
|
||||
setProgress(100);
|
||||
|
||||
return {
|
||||
url,
|
||||
filename,
|
||||
originalSize,
|
||||
compressedSize,
|
||||
compressionRatio,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('PDF压缩错误:', error);
|
||||
throw new Error('PDF压缩失败');
|
||||
}
|
||||
};
|
||||
|
||||
const startCompression = useCallback(async () => {
|
||||
if (files.length === 0) {
|
||||
setError(t('tools.pdf_compressor.errors.no_file'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pdfjsLib) {
|
||||
setError(t('tools.pdf_compressor.errors.library_failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 滚动到页面顶部,确保用户能看到进度
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
setIsCompressing(true);
|
||||
setProgress(0);
|
||||
setError(null);
|
||||
setResults([]);
|
||||
|
||||
// 开始压缩时滚动到进度条
|
||||
setTimeout(() => {
|
||||
const progressElement = document.querySelector('[data-progress]');
|
||||
if (progressElement) {
|
||||
progressElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
try {
|
||||
const compressionResults: CompressionResult[] = [];
|
||||
|
||||
// 只处理PDF文件
|
||||
const pdfFiles = files.filter(file => file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length === 0) {
|
||||
setError(t('tools.pdf_compressor.errors.invalid_format'));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
setProgress((i / pdfFiles.length) * 10); // 前10%用于文件准备
|
||||
const result = await compressPDF(pdfFiles[i]);
|
||||
compressionResults.push(result);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
setResults(compressionResults);
|
||||
|
||||
// 压缩完成后滚动到结果区域
|
||||
setTimeout(() => {
|
||||
if (resultsRef.current) {
|
||||
resultsRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.error('压缩错误:', err);
|
||||
setError(t('tools.pdf_compressor.errors.compression_failed'));
|
||||
} finally {
|
||||
setIsCompressing(false);
|
||||
}
|
||||
}, [files, settings, t, pdfjsLib]);
|
||||
|
||||
const downloadResult = useCallback((result: CompressionResult) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = result.url;
|
||||
link.download = result.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}, []);
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
// 清理之前的结果URL
|
||||
results.forEach(result => URL.revokeObjectURL(result.url));
|
||||
setFiles([]);
|
||||
setResults([]);
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
}, [results]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 如果不在客户端,显示加载状态
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500 mx-auto mb-4"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
<ToolHeader
|
||||
toolCode="pdf_compressor"
|
||||
title={t('tools.pdf_compressor.title')}
|
||||
description={t('tools.pdf_compressor.description')}
|
||||
icon={faCompress}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* 压缩设置 */}
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">{t('tools.pdf_compressor.compression_settings.title')}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t('tools.pdf_compressor.compression_settings.quality')}
|
||||
</label>
|
||||
<select
|
||||
value={settings.quality}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, quality: e.target.value as 'high' | 'medium' | 'low' }))}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"
|
||||
>
|
||||
<option value="high">{t('tools.pdf_compressor.compression_settings.quality_high')}</option>
|
||||
<option value="medium">{t('tools.pdf_compressor.compression_settings.quality_medium')}</option>
|
||||
<option value="low">{t('tools.pdf_compressor.compression_settings.quality_low')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="imageCompression"
|
||||
checked={settings.imageCompression}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, imageCompression: e.target.checked }))}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="imageCompression" className="text-sm">
|
||||
{t('tools.pdf_compressor.compression_settings.image_compression')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="fontSubsetting"
|
||||
checked={settings.fontSubsetting}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, fontSubsetting: e.target.checked }))}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="fontSubsetting" className="text-sm">
|
||||
{t('tools.pdf_compressor.compression_settings.font_subsetting')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="metadataRemoval"
|
||||
checked={settings.metadataRemoval}
|
||||
onChange={(e) => setSettings(prev => ({ ...prev, metadataRemoval: e.target.checked }))}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="metadataRemoval" className="text-sm">
|
||||
{t('tools.pdf_compressor.compression_settings.metadata_removal')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传 */}
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<FileUpload
|
||||
accept=".pdf"
|
||||
maxSize={100 * 1024 * 1024}
|
||||
multiple={true}
|
||||
onFileSelect={handleFileSelect}
|
||||
onError={handleFileError}
|
||||
title={t('tools.pdf_compressor.upload_area.title')}
|
||||
subtitle={t('tools.pdf_compressor.upload_area.subtitle')}
|
||||
buttonText={t('tools.pdf_compressor.upload_area.button')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="bg-red-600 text-white p-4 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 压缩进度 */}
|
||||
{isCompressing && (
|
||||
<div data-progress>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
status={t('tools.pdf_compressor.status.compressing')}
|
||||
onCancel={() => setIsCompressing(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{files.length > 0 && !isCompressing && (
|
||||
<div className="flex gap-4">
|
||||
<ActionButton
|
||||
onClick={startCompression}
|
||||
loading={isCompressing}
|
||||
disabled={files.length === 0 || !pdfjsLib}
|
||||
>
|
||||
{t('tools.pdf_compressor.actions.compress')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={clearAll}
|
||||
variant="secondary"
|
||||
>
|
||||
{t('tools.pdf_compressor.actions.clear')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 压缩结果 */}
|
||||
{results.length > 0 && (
|
||||
<div ref={resultsRef} className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">{t('tools.pdf_compressor.results.title')}</h3>
|
||||
<div className="space-y-3">
|
||||
{results.map((result, index) => (
|
||||
<div key={index} className="bg-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{result.filename}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{t('tools.pdf_compressor.results.original_size')}: {formatFileSize(result.originalSize)} |
|
||||
{t('tools.pdf_compressor.results.compressed_size')}: {formatFileSize(result.compressedSize)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className={`text-sm font-bold ${
|
||||
result.compressionRatio > 0 ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{result.compressionRatio > 0 ? '-' : '+'}{Math.abs(result.compressionRatio).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ActionButton
|
||||
onClick={() => downloadResult(result)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={faDownload}
|
||||
>
|
||||
{t('tools.pdf_compressor.actions.download')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import ToolHeader from '@/components/ToolHeader';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import ActionButton from '@/components/ActionButton';
|
||||
import ProgressBar from '@/components/ProgressBar';
|
||||
import { faDownload, faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface ConversionResult {
|
||||
url: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
type: 'image' | 'text';
|
||||
}
|
||||
|
||||
export default function PDFConverterPage() {
|
||||
const { t } = useLanguage();
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [conversionType, setConversionType] = useState<'pdf_to_image' | 'pdf_to_text'>('pdf_to_image');
|
||||
const [imageFormat, setImageFormat] = useState<'png' | 'jpeg' | 'webp'>('png');
|
||||
const [imageQuality, setImageQuality] = useState<'high' | 'medium' | 'low'>('high');
|
||||
const [isConverting, setIsConverting] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [results, setResults] = useState<ConversionResult[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pdfjsLib, setPdfjsLib] = useState<typeof import('pdfjs-dist') | null>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const resultsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 确保只在客户端运行
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
// 动态导入PDF.js
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
const loadPDFJS = async () => {
|
||||
try {
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/lib/pdfjs-dist/pdf.worker.min.mjs';
|
||||
setPdfjsLib(pdfjs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load PDF.js:', error);
|
||||
setError('PDF处理库加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
loadPDFJS();
|
||||
}, [isClient]);
|
||||
|
||||
const handleFileSelect = useCallback((selectedFiles: File[]) => {
|
||||
setFiles(selectedFiles);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const handleFileError = useCallback((errorMessage: string) => {
|
||||
setError(errorMessage);
|
||||
}, []);
|
||||
|
||||
// PDF转图片功能
|
||||
const convertPDFToImages = async (pdfFile: File): Promise<ConversionResult[]> => {
|
||||
if (!pdfjsLib) {
|
||||
throw new Error('PDF处理库未加载');
|
||||
}
|
||||
|
||||
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||
const results: ConversionResult[] = [];
|
||||
|
||||
try {
|
||||
// 使用本地PDF.js来加载和渲染PDF
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
const numPages = pdf.numPages;
|
||||
|
||||
for (let pageNum = 1; pageNum <= 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');
|
||||
if (!context) continue;
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
// 渲染页面到canvas
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
// 转换canvas为图片
|
||||
const quality = imageQuality === 'high' ? 1.0 : imageQuality === 'medium' ? 0.7 : 0.5;
|
||||
const mimeType = imageFormat === 'png' ? 'image/png' : imageFormat === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
||||
|
||||
// 等待canvas转换为blob
|
||||
const blob = await new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
}, mimeType, quality);
|
||||
});
|
||||
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const filename = `${pdfFile.name.replace('.pdf', '')}_page_${pageNum}.${imageFormat}`;
|
||||
results.push({
|
||||
url,
|
||||
filename,
|
||||
size: blob.size,
|
||||
type: 'image'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('PDF转换错误:', error);
|
||||
throw new Error('PDF转换失败');
|
||||
}
|
||||
};
|
||||
|
||||
const startConversion = useCallback(async () => {
|
||||
if (files.length === 0) {
|
||||
setError(t('tools.pdf_converter.errors.no_file'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pdfjsLib) {
|
||||
setError('PDF处理库未加载,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
|
||||
// 滚动到页面顶部,确保用户能看到进度
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
setIsConverting(true);
|
||||
setProgress(0);
|
||||
setError(null);
|
||||
setResults([]);
|
||||
|
||||
// 开始转换时滚动到进度条
|
||||
setTimeout(() => {
|
||||
const progressElement = document.querySelector('[data-progress]');
|
||||
if (progressElement) {
|
||||
progressElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
try {
|
||||
const conversionResults: ConversionResult[] = [];
|
||||
|
||||
if (conversionType === 'pdf_to_image') {
|
||||
// 只处理PDF文件
|
||||
const pdfFiles = files.filter(file => file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'));
|
||||
if (pdfFiles.length === 0) {
|
||||
setError('请选择PDF文件进行转换');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
setProgress((i / pdfFiles.length) * 100);
|
||||
const results = await convertPDFToImages(pdfFiles[i]);
|
||||
conversionResults.push(...results);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
} else if (conversionType === 'pdf_to_text') {
|
||||
// PDF转文本功能 - 直接跳转到文件转Markdown工具
|
||||
window.open('https://www.jisuxiang.com/tools/file_to_markdown_converter', '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
setProgress(100);
|
||||
setResults(conversionResults);
|
||||
|
||||
// 转换完成后滚动到结果区域
|
||||
setTimeout(() => {
|
||||
if (resultsRef.current) {
|
||||
resultsRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.error('转换错误:', err);
|
||||
setError(t('tools.pdf_converter.errors.conversion_failed'));
|
||||
} finally {
|
||||
setIsConverting(false);
|
||||
}
|
||||
}, [files, conversionType, imageFormat, imageQuality, t, pdfjsLib]);
|
||||
|
||||
const downloadResult = useCallback((result: ConversionResult) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = result.url;
|
||||
link.download = result.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}, []);
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
// 清理之前的结果URL
|
||||
results.forEach(result => URL.revokeObjectURL(result.url));
|
||||
setFiles([]);
|
||||
setResults([]);
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
}, [results]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 如果不在客户端,显示加载状态
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500 mx-auto mb-4"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
<ToolHeader
|
||||
toolCode="pdf_converter"
|
||||
title={t('tools.pdf_converter.title')}
|
||||
description={t('tools.pdf_converter.description')}
|
||||
icon={faExchangeAlt}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* 转换类型选择 */}
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">转换类型</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(['pdf_to_image', 'pdf_to_text'] as const).map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setConversionType(type)}
|
||||
className={`p-3 rounded-lg border transition-colors ${
|
||||
conversionType === type
|
||||
? 'border-purple-500 bg-purple-600 text-white'
|
||||
: 'border-gray-600 hover:border-purple-500'
|
||||
}`}
|
||||
>
|
||||
{type === 'pdf_to_image' ? 'PDF转图片' : 'PDF转文本'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PDF转文本提示 */}
|
||||
{conversionType === 'pdf_to_text' && (
|
||||
<div className="bg-blue-600 text-white p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium mb-2">推荐使用文件转Markdown工具</p>
|
||||
<p className="text-sm opacity-90">
|
||||
PDF转文本功能建议使用我们的文件转Markdown工具,支持更好的文本提取和格式转换。
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://www.jisuxiang.com/tools/file_to_markdown_converter"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-4 py-2 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
<i className="fas fa-external-link-alt"></i>
|
||||
打开工具
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 转换设置 */}
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">转换设置</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{conversionType === 'pdf_to_image' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
图片格式
|
||||
</label>
|
||||
<select
|
||||
value={imageFormat}
|
||||
onChange={(e) => setImageFormat(e.target.value as 'png' | 'jpeg' | 'webp')}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"
|
||||
>
|
||||
<option value="png">PNG</option>
|
||||
<option value="jpeg">JPEG</option>
|
||||
<option value="webp">WebP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
图片质量
|
||||
</label>
|
||||
<select
|
||||
value={imageQuality}
|
||||
onChange={(e) => setImageQuality(e.target.value as 'high' | 'medium' | 'low')}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"
|
||||
>
|
||||
<option value="high">高质量</option>
|
||||
<option value="medium">中等质量</option>
|
||||
<option value="low">低质量</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传 - 在PDF转文本时隐藏 */}
|
||||
{conversionType !== 'pdf_to_text' && (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<FileUpload
|
||||
accept=".pdf"
|
||||
maxSize={100 * 1024 * 1024}
|
||||
multiple={true}
|
||||
onFileSelect={handleFileSelect}
|
||||
onError={handleFileError}
|
||||
title="上传PDF文件"
|
||||
subtitle="支持拖拽上传,最大100MB"
|
||||
buttonText="选择文件"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="bg-red-600 text-white p-4 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 转换进度 */}
|
||||
{isConverting && (
|
||||
<div data-progress>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
status="正在转换..."
|
||||
onCancel={() => setIsConverting(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{files.length > 0 && !isConverting && conversionType !== 'pdf_to_text' && (
|
||||
<div className="flex gap-4">
|
||||
<ActionButton
|
||||
onClick={startConversion}
|
||||
loading={isConverting}
|
||||
disabled={files.length === 0 || !pdfjsLib}
|
||||
>
|
||||
开始转换
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={clearAll}
|
||||
variant="secondary"
|
||||
>
|
||||
清空
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 转换结果 */}
|
||||
{results.length > 0 && (
|
||||
<div ref={resultsRef} className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">转换结果</h3>
|
||||
<div className="space-y-3">
|
||||
{results.map((result, index) => (
|
||||
<div key={index} className="flex items-center justify-between bg-gray-700 rounded-lg p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{result.filename}</p>
|
||||
<p className="text-xs text-gray-400">{formatFileSize(result.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ActionButton
|
||||
onClick={() => downloadResult(result)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={faDownload}
|
||||
>
|
||||
下载
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import ToolHeader from '@/components/ToolHeader';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import ActionButton from '@/components/ActionButton';
|
||||
import ProgressBar from '@/components/ProgressBar';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faDownload, faTrash, faGripVertical, faObjectGroup,
|
||||
faScissors
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface PDFFile {
|
||||
file: File;
|
||||
url: string;
|
||||
size: number;
|
||||
pages?: number;
|
||||
}
|
||||
|
||||
interface SplitRange {
|
||||
start: number;
|
||||
end: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type OperationMode = 'merge' | 'split';
|
||||
|
||||
export default function PDFManagerPage() {
|
||||
const { t } = useLanguage();
|
||||
const [files, setFiles] = useState<PDFFile[]>([]);
|
||||
const [operationMode, setOperationMode] = useState<OperationMode>('merge');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [results, setResults] = useState<{ url: string; filename: string; size: number }[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pdfLib, setPdfLib] = useState<typeof import('pdf-lib') | null>(null);
|
||||
const resultsRef = useRef<HTMLDivElement>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
// 分割模式状态
|
||||
const [splitMethod, setSplitMethod] = useState<'single_pages' | 'custom_ranges' | 'equal_parts'>('single_pages');
|
||||
const [startPage, _setStartPage] = useState(1);
|
||||
const [endPage, _setEndPage] = useState(1);
|
||||
const [customRanges, setCustomRanges] = useState<SplitRange[]>([]);
|
||||
const [customRangeInput, setCustomRangeInput] = useState('');
|
||||
const [partsCount, setPartsCount] = useState(2);
|
||||
|
||||
// 合并模式状态
|
||||
const [outputName, setOutputName] = useState('merged_document.pdf');
|
||||
|
||||
|
||||
|
||||
// 确保只在客户端运行
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
// 动态导入pdf-lib
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
const loadPdfLib = async () => {
|
||||
try {
|
||||
const pdfLibModule = await import('pdf-lib');
|
||||
setPdfLib(pdfLibModule);
|
||||
} catch (error) {
|
||||
console.error('Failed to load pdf-lib:', error);
|
||||
setError('PDF处理库加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
loadPdfLib();
|
||||
}, [isClient]);
|
||||
|
||||
const handleFileSelect = useCallback((selectedFiles: File[]) => {
|
||||
const totalSize = selectedFiles.reduce((sum, file) => sum + file.size, 0);
|
||||
|
||||
if (totalSize > 500 * 1024 * 1024) {
|
||||
setError(t('tools.pdf_manager.errors.file_too_large'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfFiles: PDFFile[] = selectedFiles.map(file => ({
|
||||
file,
|
||||
url: URL.createObjectURL(file),
|
||||
size: file.size
|
||||
}));
|
||||
|
||||
setFiles(pdfFiles);
|
||||
setError(null);
|
||||
}, [t]);
|
||||
|
||||
const handleFileError = useCallback((errorMessage: string) => {
|
||||
setError(errorMessage);
|
||||
}, []);
|
||||
|
||||
const removeFile = useCallback((index: number) => {
|
||||
const newFiles = files.filter((_, i) => i !== index);
|
||||
setFiles(newFiles);
|
||||
}, [files]);
|
||||
|
||||
const _moveFile = useCallback((fromIndex: number, toIndex: number) => {
|
||||
const newFiles = [...files];
|
||||
const [movedFile] = newFiles.splice(fromIndex, 1);
|
||||
newFiles.splice(toIndex, 0, movedFile);
|
||||
setFiles(newFiles);
|
||||
}, [files]);
|
||||
|
||||
// 解析自定义范围输入
|
||||
const parseCustomRanges = useCallback((input: string): SplitRange[] => {
|
||||
const ranges: SplitRange[] = [];
|
||||
const parts = input.split(',').map(p => p.trim());
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.includes('-')) {
|
||||
const [start, end] = part.split('-').map(n => parseInt(n.trim()));
|
||||
if (!isNaN(start) && !isNaN(end) && start <= end) {
|
||||
ranges.push({
|
||||
start,
|
||||
end,
|
||||
id: `${start}-${end}`
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const page = parseInt(part);
|
||||
if (!isNaN(page)) {
|
||||
ranges.push({
|
||||
start: page,
|
||||
end: page,
|
||||
id: `${page}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}, []);
|
||||
|
||||
const addCustomRange = useCallback(() => {
|
||||
if (!customRangeInput.trim()) return;
|
||||
|
||||
const newRanges = parseCustomRanges(customRangeInput);
|
||||
setCustomRanges(prev => [...prev, ...newRanges]);
|
||||
setCustomRangeInput('');
|
||||
}, [customRangeInput, parseCustomRanges]);
|
||||
|
||||
const removeCustomRange = useCallback((id: string) => {
|
||||
setCustomRanges(prev => prev.filter(range => range.id !== id));
|
||||
}, []);
|
||||
|
||||
// 重置所有状态到初始值
|
||||
const resetState = useCallback(() => {
|
||||
setFiles([]);
|
||||
setResults([]);
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
setCustomRanges([]);
|
||||
setCustomRangeInput('');
|
||||
setOutputName('merged_document.pdf');
|
||||
setSplitMethod('single_pages');
|
||||
setPartsCount(2);
|
||||
}, []);
|
||||
|
||||
// 切换操作模式时重置状态
|
||||
const handleOperationModeChange = useCallback((mode: OperationMode) => {
|
||||
setOperationMode(mode);
|
||||
resetState();
|
||||
}, [resetState]);
|
||||
|
||||
const startOperation = useCallback(async () => {
|
||||
if (!pdfLib) {
|
||||
setError('PDF处理库未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
setError(t('tools.pdf_manager.errors.no_files'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 滚动到页面顶部,确保用户能看到进度
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
setIsProcessing(true);
|
||||
setProgress(0);
|
||||
setError(null);
|
||||
|
||||
// 开始处理时滚动到进度条
|
||||
setTimeout(() => {
|
||||
const progressElement = document.querySelector('[data-progress]');
|
||||
if (progressElement) {
|
||||
progressElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
try {
|
||||
if (operationMode === 'merge') {
|
||||
await performMerge();
|
||||
} else if (operationMode === 'split') {
|
||||
await performSplit();
|
||||
}
|
||||
|
||||
// 操作完成后自动滚动到结果区域
|
||||
setTimeout(() => {
|
||||
if (resultsRef.current) {
|
||||
resultsRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.error('PDF operation error:', err);
|
||||
setError(err instanceof Error ? err.message : t('tools.pdf_manager.errors.operation_failed'));
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [files, operationMode, splitMethod, startPage, endPage, customRanges, partsCount, outputName, t, pdfLib]);
|
||||
|
||||
const performMerge = useCallback(async () => {
|
||||
if (files.length < 2) {
|
||||
throw new Error(t('tools.pdf_manager.errors.need_at_least_two'));
|
||||
}
|
||||
|
||||
// 创建新的PDF文档
|
||||
const mergedPdf = await pdfLib!.PDFDocument.create();
|
||||
setProgress(20);
|
||||
|
||||
// 合并所有PDF文件
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
try {
|
||||
const arrayBuffer = await file.file.arrayBuffer();
|
||||
const pdf = await pdfLib!.PDFDocument.load(arrayBuffer);
|
||||
|
||||
// 复制所有页面到合并文档
|
||||
const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
||||
pages.forEach((page) => mergedPdf.addPage(page));
|
||||
|
||||
setProgress(20 + (i + 1) * (60 / files.length));
|
||||
} catch {
|
||||
throw new Error(`处理文件失败: ${file.file.name}。请确保是有效的PDF文件。`);
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(80);
|
||||
|
||||
// 生成合并后的PDF
|
||||
const mergedPdfBytes = await mergedPdf.save();
|
||||
setProgress(100);
|
||||
|
||||
// 创建Blob并生成下载链接
|
||||
const mergedPdfBlob = new Blob([mergedPdfBytes], { type: 'application/pdf' });
|
||||
|
||||
const result = {
|
||||
url: URL.createObjectURL(mergedPdfBlob),
|
||||
filename: outputName,
|
||||
size: mergedPdfBlob.size
|
||||
};
|
||||
|
||||
setResults([result]);
|
||||
}, [files, outputName, t, pdfLib]);
|
||||
|
||||
const performSplit = useCallback(async () => {
|
||||
if (files.length === 0) {
|
||||
throw new Error(t('tools.pdf_manager.errors.no_files'));
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
const arrayBuffer = await file.file.arrayBuffer();
|
||||
const pdf = await pdfLib!.PDFDocument.load(arrayBuffer);
|
||||
const pageCount = pdf.getPageCount();
|
||||
|
||||
setProgress(20);
|
||||
|
||||
const splitResults: { url: string; filename: string; size: number }[] = [];
|
||||
|
||||
if (splitMethod === 'single_pages') {
|
||||
// 单页分割
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
const newPdf = await pdfLib!.PDFDocument.create();
|
||||
const [page] = await newPdf.copyPages(pdf, [i]);
|
||||
newPdf.addPage(page);
|
||||
|
||||
const pdfBytes = await newPdf.save();
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
splitResults.push({
|
||||
url: URL.createObjectURL(blob),
|
||||
filename: `${file.file.name.replace('.pdf', '')}_page_${i + 1}.pdf`,
|
||||
size: blob.size
|
||||
});
|
||||
|
||||
setProgress(20 + (i + 1) * (60 / pageCount));
|
||||
}
|
||||
} else if (splitMethod === 'custom_ranges') {
|
||||
// 自定义范围分割
|
||||
const ranges = customRanges.length > 0 ? customRanges : [{ start: startPage, end: endPage, id: 'default' }];
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (range.start < 1 || range.end > pageCount || range.start > range.end) {
|
||||
throw new Error(t('tools.pdf_manager.errors.range_out_of_bounds'));
|
||||
}
|
||||
|
||||
const newPdf = await pdfLib!.PDFDocument.create();
|
||||
const pageIndices = Array.from({ length: range.end - range.start + 1 }, (_, j) => range.start - 1 + j);
|
||||
const pages = await newPdf.copyPages(pdf, pageIndices);
|
||||
pages.forEach(page => newPdf.addPage(page));
|
||||
|
||||
const pdfBytes = await newPdf.save();
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
splitResults.push({
|
||||
url: URL.createObjectURL(blob),
|
||||
filename: `${file.file.name.replace('.pdf', '')}_${range.start}-${range.end}.pdf`,
|
||||
size: blob.size
|
||||
});
|
||||
|
||||
setProgress(20 + (i + 1) * (60 / ranges.length));
|
||||
}
|
||||
} else if (splitMethod === 'equal_parts') {
|
||||
// 等分分割
|
||||
if (partsCount < 2) {
|
||||
throw new Error(t('tools.pdf_manager.errors.invalid_parts_count'));
|
||||
}
|
||||
|
||||
const pagesPerPart = Math.ceil(pageCount / partsCount);
|
||||
|
||||
for (let i = 0; i < partsCount; i++) {
|
||||
const startPage = i * pagesPerPart + 1;
|
||||
const endPage = Math.min((i + 1) * pagesPerPart, pageCount);
|
||||
|
||||
if (startPage > pageCount) break;
|
||||
|
||||
const newPdf = await pdfLib!.PDFDocument.create();
|
||||
const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, j) => startPage - 1 + j);
|
||||
const pages = await newPdf.copyPages(pdf, pageIndices);
|
||||
pages.forEach(page => newPdf.addPage(page));
|
||||
|
||||
const pdfBytes = await newPdf.save();
|
||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
|
||||
splitResults.push({
|
||||
url: URL.createObjectURL(blob),
|
||||
filename: `${file.file.name.replace('.pdf', '')}_part_${i + 1}.pdf`,
|
||||
size: blob.size
|
||||
});
|
||||
|
||||
setProgress(20 + (i + 1) * (60 / partsCount));
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(100);
|
||||
setResults(splitResults);
|
||||
}, [files, splitMethod, startPage, endPage, customRanges, partsCount, t, pdfLib]);
|
||||
|
||||
const downloadResult = useCallback((result: { url: string; filename: string; size: number }) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = result.url;
|
||||
link.download = result.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}, []);
|
||||
|
||||
const downloadAllResults = useCallback(() => {
|
||||
results.forEach(result => downloadResult(result));
|
||||
}, [results, downloadResult]);
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
files.forEach(file => URL.revokeObjectURL(file.url));
|
||||
results.forEach(result => URL.revokeObjectURL(result.url));
|
||||
setFiles([]);
|
||||
setResults([]);
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
setCustomRanges([]);
|
||||
setCustomRangeInput('');
|
||||
}, [files, results]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
<ToolHeader
|
||||
toolCode="pdf_manager"
|
||||
title={t('tools.pdf_manager.title')}
|
||||
description={t('tools.pdf_manager.description')}
|
||||
icon={faObjectGroup}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
{/* 操作模式选择 */}
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">{t('tools.pdf_manager.operation_mode.title')}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => handleOperationModeChange('merge')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
operationMode === 'merge'
|
||||
? 'border-indigo-500 bg-indigo-500/20 text-indigo-300'
|
||||
: 'border-gray-600 bg-gray-700 hover:border-gray-500'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faObjectGroup} className="text-2xl mb-2" />
|
||||
<div className="font-medium">{t('tools.pdf_manager.operation_mode.merge')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleOperationModeChange('split')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
operationMode === 'split'
|
||||
? 'border-indigo-500 bg-indigo-500/20 text-indigo-300'
|
||||
: 'border-gray-600 bg-gray-700 hover:border-gray-500'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faScissors} className="text-2xl mb-2" />
|
||||
<div className="font-medium">{t('tools.pdf_manager.operation_mode.split')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件上传 */}
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<FileUpload
|
||||
accept=".pdf"
|
||||
maxSize={operationMode === 'merge' ? 500 * 1024 * 1024 : 100 * 1024 * 1024}
|
||||
multiple={operationMode === 'merge'}
|
||||
onFileSelect={handleFileSelect}
|
||||
onError={handleFileError}
|
||||
title={t('tools.pdf_manager.upload_area.title')}
|
||||
subtitle={t('tools.pdf_manager.upload_area.subtitle')}
|
||||
buttonText={t('tools.pdf_manager.upload_area.button')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="bg-red-600 text-white p-4 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文件列表 */}
|
||||
{files.length > 0 && (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">{t('tools.pdf_manager.file_list.title')}</h3>
|
||||
<div className="text-sm text-gray-400">
|
||||
{t('tools.pdf_manager.file_list.total_size')}: {formatFileSize(totalSize)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-gray-700 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FontAwesomeIcon icon={faGripVertical} className="text-gray-400 cursor-move" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">{file.file.name}</p>
|
||||
<p className="text-xs text-gray-400">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="text-gray-400 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
{t('tools.pdf_manager.file_list.drag_hint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作设置 */}
|
||||
{files.length > 0 && (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
{operationMode === 'merge' && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">{t('tools.pdf_manager.merge_mode.title')}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t('tools.pdf_manager.merge_mode.output_name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={outputName}
|
||||
onChange={(e) => setOutputName(e.target.value)}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"
|
||||
placeholder="merged_document.pdf"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{operationMode === 'split' && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold">{t('tools.pdf_manager.split_mode.title')}</h3>
|
||||
|
||||
{/* 分割方式选择 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t('tools.pdf_manager.split_mode.split_method')}
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{(['single_pages', 'custom_ranges', 'equal_parts'] as const).map((method) => (
|
||||
<button
|
||||
key={method}
|
||||
onClick={() => setSplitMethod(method)}
|
||||
className={`p-3 rounded-lg border-2 transition-all ${
|
||||
splitMethod === method
|
||||
? 'border-indigo-500 bg-indigo-500/20 text-indigo-300'
|
||||
: 'border-gray-600 bg-gray-700 hover:border-gray-500'
|
||||
}`}
|
||||
>
|
||||
{t(`tools.pdf_manager.split_methods.${method}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 单页分割设置 */}
|
||||
{splitMethod === 'single_pages' && (
|
||||
<div className="bg-gray-700 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-300">
|
||||
{t('tools.pdf_manager.split_methods.single_pages')} - {t('tools.pdf_manager.split_methods.single_pages_description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 自定义范围设置 */}
|
||||
{splitMethod === 'custom_ranges' && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-700 rounded-lg p-4">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t('tools.pdf_manager.custom_ranges.title')}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={customRangeInput}
|
||||
onChange={(e) => setCustomRangeInput(e.target.value)}
|
||||
className="flex-1 bg-gray-600 border border-gray-500 rounded-lg px-3 py-2"
|
||||
placeholder={t('tools.pdf_manager.custom_ranges.placeholder')}
|
||||
/>
|
||||
<button
|
||||
onClick={addCustomRange}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors"
|
||||
>
|
||||
{t('tools.pdf_manager.custom_ranges.add_range')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
{t('tools.pdf_manager.custom_ranges.range_format')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{customRanges.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{customRanges.map((range) => (
|
||||
<div key={range.id} className="flex items-center justify-between bg-gray-700 rounded-lg p-3">
|
||||
<span className="text-sm">
|
||||
{range.start === range.end
|
||||
? t('tools.pdf_manager.custom_ranges.single_page').replace('{page}', range.start.toString())
|
||||
: t('tools.pdf_manager.custom_ranges.page_range').replace('{start}', range.start.toString()).replace('{end}', range.end.toString())
|
||||
}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeCustomRange(range.id)}
|
||||
className="text-red-400 hover:text-red-300 transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 等分分割设置 */}
|
||||
{splitMethod === 'equal_parts' && (
|
||||
<div className="bg-gray-700 rounded-lg p-4">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t('tools.pdf_manager.equal_parts.parts_count')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="2"
|
||||
value={partsCount}
|
||||
onChange={(e) => setPartsCount(parseInt(e.target.value) || 2)}
|
||||
className="w-full bg-gray-600 border border-gray-500 rounded-lg px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 处理进度 */}
|
||||
{isProcessing && (
|
||||
<div data-progress>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
status={t(`tools.pdf_manager.status.${operationMode === 'merge' ? 'merging' : 'splitting'}`)}
|
||||
onCancel={() => setIsProcessing(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{files.length > 0 && !isProcessing && (
|
||||
<div className="flex gap-4">
|
||||
<ActionButton
|
||||
onClick={startOperation}
|
||||
loading={isProcessing}
|
||||
disabled={files.length === 0}
|
||||
>
|
||||
{t(`tools.pdf_manager.actions.${operationMode === 'merge' ? 'merge' : 'split'}`)}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={clearAll}
|
||||
variant="secondary"
|
||||
>
|
||||
{t('tools.pdf_manager.actions.clear')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作结果 */}
|
||||
{results.length > 0 && (
|
||||
<div ref={resultsRef} className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">{t('tools.pdf_manager.results.title')}</h3>
|
||||
<div className="text-sm text-gray-400">
|
||||
{t('tools.pdf_manager.results.files_count')}: {results.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{results.map((result, index) => (
|
||||
<div key={index} className="flex items-center justify-between bg-gray-700 rounded-lg p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{result.filename}</p>
|
||||
<p className="text-xs text-gray-400">{formatFileSize(result.size)}</p>
|
||||
</div>
|
||||
<ActionButton
|
||||
onClick={() => downloadResult(result)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={faDownload}
|
||||
>
|
||||
{t('tools.pdf_manager.actions.download')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{results.length > 1 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-600">
|
||||
<ActionButton
|
||||
onClick={downloadAllResults}
|
||||
variant="primary"
|
||||
icon={faDownload}
|
||||
>
|
||||
{t('tools.pdf_manager.results.download_all')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -187,13 +187,6 @@ export default function TimestampConverter() {
|
||||
dateTimeToTimestamp(value);
|
||||
};
|
||||
|
||||
// 使用常用时间戳
|
||||
const handleUseCommonTimestamp = (ts: number) => {
|
||||
const tsStr = ts.toString();
|
||||
setTimestamp(tsStr);
|
||||
timestampToDateTime(tsStr);
|
||||
};
|
||||
|
||||
// 刷新计算,使用当前时间
|
||||
const refreshWithCurrentTime = () => {
|
||||
const now = new Date();
|
||||
@@ -202,23 +195,11 @@ export default function TimestampConverter() {
|
||||
timestampToDateTime(currentTimestamp.toString());
|
||||
};
|
||||
|
||||
// 使用当前时间更新日期时间输入
|
||||
const setCurrentDateTime = () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
const nowDateTime = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
||||
setDateTime(nowDateTime);
|
||||
setFormattedDateTime(`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`);
|
||||
|
||||
// 转换为时间戳
|
||||
const timestamp = Math.floor(now.getTime() / 1000);
|
||||
setTimestamp(timestamp.toString());
|
||||
// 使用常用时间戳
|
||||
const handleUseCommonTimestamp = (ts: number) => {
|
||||
const tsStr = ts.toString();
|
||||
setTimestamp(tsStr);
|
||||
timestampToDateTime(tsStr);
|
||||
};
|
||||
|
||||
// 复制时间戳到剪贴板
|
||||
@@ -260,6 +241,25 @@ export default function TimestampConverter() {
|
||||
setSwapped(!swapped);
|
||||
};
|
||||
|
||||
// 使用当前时间更新日期时间输入
|
||||
const setCurrentDateTime = () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
const nowDateTime = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
||||
setDateTime(nowDateTime);
|
||||
setFormattedDateTime(`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`);
|
||||
|
||||
// 转换为时间戳
|
||||
const timestamp = Math.floor(now.getTime() / 1000);
|
||||
setTimestamp(timestamp.toString());
|
||||
};
|
||||
|
||||
// 渲染时间戳区域
|
||||
const renderTimestampSection = () => (
|
||||
<div id="timestamp-section" className="flex flex-col gap-4 transition-all duration-300">
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
interface ActionButtonProps {
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
icon?: IconDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ActionButton({
|
||||
onClick,
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
icon,
|
||||
className = ''
|
||||
}: ActionButtonProps) {
|
||||
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-purple-600 hover:bg-purple-700 text-white focus:ring-purple-500 disabled:bg-gray-600 disabled:cursor-not-allowed',
|
||||
secondary: 'bg-gray-700 hover:bg-gray-600 text-white border border-gray-600 focus:ring-gray-500 disabled:bg-gray-800 disabled:cursor-not-allowed',
|
||||
danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500 disabled:bg-gray-600 disabled:cursor-not-allowed'
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base'
|
||||
};
|
||||
|
||||
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
className={classes}
|
||||
>
|
||||
{loading && (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||
)}
|
||||
{icon && !loading && (
|
||||
<FontAwesomeIcon icon={icon} className="mr-2" />
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCloudUploadAlt, faFile, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface FileUploadProps {
|
||||
accept?: string;
|
||||
maxSize?: number; // 以字节为单位
|
||||
multiple?: boolean;
|
||||
onFileSelect: (files: File[]) => void;
|
||||
onError?: (error: string) => void;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function FileUpload({
|
||||
accept = '*',
|
||||
maxSize = 100 * 1024 * 1024, // 默认100MB
|
||||
multiple = false,
|
||||
onFileSelect,
|
||||
onError,
|
||||
title = '拖拽文件到此处或点击上传',
|
||||
subtitle = '支持多种格式',
|
||||
buttonText = '选择文件',
|
||||
className = ''
|
||||
}: FileUploadProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
|
||||
const validateFiles = useCallback((files: FileList): File[] => {
|
||||
const validFiles: File[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
// 检查文件大小
|
||||
if (file.size > maxSize) {
|
||||
errors.push(`文件 ${file.name} 过大,最大支持 ${Math.round(maxSize / 1024 / 1024)}MB`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (accept !== '*' && !file.type.match(new RegExp(accept.replace(/\*/g, '.*')))) {
|
||||
errors.push(`文件 ${file.name} 格式不支持`);
|
||||
return;
|
||||
}
|
||||
|
||||
validFiles.push(file);
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
onError?.(errors.join('\n'));
|
||||
return [];
|
||||
}
|
||||
|
||||
return validFiles;
|
||||
}, [accept, maxSize, onError]);
|
||||
|
||||
const handleFileSelect = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const validFiles = validateFiles(files);
|
||||
if (validFiles.length > 0) {
|
||||
setSelectedFiles(validFiles);
|
||||
onFileSelect(validFiles);
|
||||
}
|
||||
}, [validateFiles, onFileSelect]);
|
||||
|
||||
const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (!files) return;
|
||||
|
||||
const validFiles = validateFiles(files);
|
||||
if (validFiles.length > 0) {
|
||||
setSelectedFiles(validFiles);
|
||||
onFileSelect(validFiles);
|
||||
}
|
||||
}, [validateFiles, onFileSelect]);
|
||||
|
||||
const handleDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const removeFile = useCallback((index: number) => {
|
||||
const newFiles = selectedFiles.filter((_, i) => i !== index);
|
||||
setSelectedFiles(newFiles);
|
||||
onFileSelect(newFiles);
|
||||
}, [selectedFiles, onFileSelect]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* 文件上传区域 */}
|
||||
<div
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer
|
||||
${isDragOver
|
||||
? 'border-purple-400 bg-purple-50 dark:bg-purple-900/20'
|
||||
: 'border-purple-500 hover:border-purple-400'
|
||||
}
|
||||
`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => document.getElementById('file-upload')?.click()}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCloudUploadAlt}
|
||||
className="text-4xl text-purple-500 mb-4"
|
||||
/>
|
||||
<h3 className="text-xl font-semibold mb-2 text-gray-100">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-gray-400 mb-4">
|
||||
{subtitle}
|
||||
</p>
|
||||
<button className="bg-purple-600 hover:bg-purple-700 text-white px-6 py-2 rounded-lg transition-colors">
|
||||
{buttonText}
|
||||
</button>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 已选择的文件列表 */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-300">已选择的文件:</h4>
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={`${file.name}-${index}`}
|
||||
className="flex items-center justify-between bg-gray-800 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FontAwesomeIcon icon={faFile} className="text-purple-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-100">{file.name}</p>
|
||||
<p className="text-xs text-gray-400">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="text-gray-400 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface ProgressBarProps {
|
||||
progress: number; // 0-100
|
||||
status: string;
|
||||
onCancel?: () => void;
|
||||
showCancel?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProgressBar({
|
||||
progress,
|
||||
status,
|
||||
onCancel,
|
||||
showCancel = true,
|
||||
className = ''
|
||||
}: ProgressBarProps) {
|
||||
return (
|
||||
<div className={`bg-gray-800 rounded-lg p-4 ${className}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-300">{status}</span>
|
||||
<span className="text-sm text-gray-400">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-700 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-purple-600 h-2 rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showCancel && onCancel && (
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="text-gray-400 hover:text-red-400 transition-colors text-sm flex items-center gap-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const categories: Category[] = [
|
||||
{ code: "text", active: false },
|
||||
{ code: "image", active: false },
|
||||
{ code: "frontend", active: false },
|
||||
{ code: "pdf", active: false },
|
||||
];
|
||||
|
||||
export default categories;
|
||||
@@ -36,6 +36,7 @@ export const commonEn = {
|
||||
text: 'Text Processing',
|
||||
image: 'Image Tools',
|
||||
frontend: 'Frontend Dev',
|
||||
pdf: 'PDF Tools',
|
||||
favorites: 'My Favorites'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ export const commonZh = {
|
||||
text: '文本处理',
|
||||
image: '图像工具',
|
||||
frontend: '前端开发',
|
||||
pdf: 'PDF工具',
|
||||
favorites: '我的收藏'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +32,11 @@ export const en = {
|
||||
image_watermark: tools.image_watermark.en,
|
||||
image_to_ico: tools.image_to_ico.en,
|
||||
cron_generator: tools.cron_generator.en,
|
||||
icon_designer: tools.icon_designer.en
|
||||
icon_designer: tools.icon_designer.en,
|
||||
|
||||
pdf_converter: tools.pdf_converter.en,
|
||||
pdf_manager: tools.pdf_manager.en,
|
||||
pdf_compressor: tools.pdf_compressor.en,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ import imageWatermark from './image_watermark';
|
||||
import imageToIco from './image_to_ico';
|
||||
import cronGenerator from './cron_generator';
|
||||
import { iconDesigner } from './icon_designer';
|
||||
import { pdfConverterI18n } from './pdf_converter';
|
||||
|
||||
import { pdfManager } from './pdf_manager';
|
||||
import { pdfCompressor } from './pdf_compressor';
|
||||
export const tools = {
|
||||
json_formatter: jsonFormatter,
|
||||
regex_tester: regexTester,
|
||||
@@ -57,7 +60,13 @@ export const tools = {
|
||||
image_watermark: imageWatermark,
|
||||
image_to_ico: imageToIco,
|
||||
cron_generator: cronGenerator,
|
||||
icon_designer: iconDesigner
|
||||
icon_designer: iconDesigner,
|
||||
|
||||
pdf_converter: pdfConverterI18n,
|
||||
|
||||
pdf_manager: pdfManager,
|
||||
pdf_compressor: pdfCompressor,
|
||||
|
||||
};
|
||||
|
||||
export default tools;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export const pdfCompressorEn = {
|
||||
title: 'PDF Compressor',
|
||||
description: 'Efficiently compress PDF files to reduce file size while maintaining document quality',
|
||||
compression_settings: {
|
||||
title: 'Compression Settings',
|
||||
quality: 'Compression Quality',
|
||||
quality_high: 'High Quality (Larger File)',
|
||||
quality_medium: 'Medium Quality (Recommended)',
|
||||
quality_low: 'Low Quality (Smallest File)',
|
||||
image_compression: 'Compress Images',
|
||||
font_subsetting: 'Font Subsetting',
|
||||
metadata_removal: 'Remove Metadata',
|
||||
remove_bookmarks: 'Remove Bookmarks',
|
||||
remove_annotations: 'Remove Annotations'
|
||||
},
|
||||
upload_area: {
|
||||
title: 'Upload PDF Files',
|
||||
subtitle: 'Drag and drop supported, max 100MB',
|
||||
button: 'Choose Files'
|
||||
},
|
||||
actions: {
|
||||
compress: 'Start Compression',
|
||||
download: 'Download',
|
||||
clear: 'Clear'
|
||||
},
|
||||
status: {
|
||||
compressing: 'Compressing...',
|
||||
completed: 'Compression Complete',
|
||||
failed: 'Compression Failed',
|
||||
ready: 'Ready'
|
||||
},
|
||||
results: {
|
||||
title: 'Compression Results',
|
||||
original_size: 'Original Size',
|
||||
compressed_size: 'Compressed Size',
|
||||
compression_ratio: 'Compression Ratio'
|
||||
},
|
||||
errors: {
|
||||
no_file: 'Please select PDF files first',
|
||||
file_too_large: 'File too large, please select files smaller than 100MB',
|
||||
invalid_format: 'Unsupported file format, please select PDF files',
|
||||
compression_failed: 'Compression failed, please check the file',
|
||||
library_failed: 'PDF processing library failed to load'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pdfCompressorZh } from './zh';
|
||||
import { pdfCompressorEn } from './en';
|
||||
|
||||
export const pdfCompressor = {
|
||||
zh: pdfCompressorZh,
|
||||
en: pdfCompressorEn,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
export const pdfCompressorZh = {
|
||||
title: 'PDF压缩器',
|
||||
description: '高效压缩PDF文件,减小文件大小,保持文档质量',
|
||||
compression_settings: {
|
||||
title: '压缩设置',
|
||||
quality: '压缩质量',
|
||||
quality_high: '高质量 (文件较大)',
|
||||
quality_medium: '中等质量 (推荐)',
|
||||
quality_low: '低质量 (文件最小)',
|
||||
image_compression: '压缩图片',
|
||||
font_subsetting: '字体子集化',
|
||||
metadata_removal: '移除元数据',
|
||||
remove_bookmarks: '移除书签',
|
||||
remove_annotations: '移除注释'
|
||||
},
|
||||
upload_area: {
|
||||
title: '上传PDF文件',
|
||||
subtitle: '支持拖拽上传,最大100MB',
|
||||
button: '选择文件'
|
||||
},
|
||||
actions: {
|
||||
compress: '开始压缩',
|
||||
download: '下载',
|
||||
clear: '清空'
|
||||
},
|
||||
status: {
|
||||
compressing: '正在压缩...',
|
||||
completed: '压缩完成',
|
||||
failed: '压缩失败',
|
||||
ready: '准备就绪'
|
||||
},
|
||||
results: {
|
||||
title: '压缩结果',
|
||||
original_size: '原始大小',
|
||||
compressed_size: '压缩后',
|
||||
compression_ratio: '压缩率'
|
||||
},
|
||||
errors: {
|
||||
no_file: '请先选择PDF文件',
|
||||
file_too_large: '文件过大,请选择小于100MB的文件',
|
||||
invalid_format: '不支持的文件格式,请选择PDF文件',
|
||||
compression_failed: '压缩失败,请检查文件',
|
||||
library_failed: 'PDF处理库加载失败'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
export const pdfConverterEn = {
|
||||
title: 'PDF Converter',
|
||||
description: 'PDF to image and text extraction tool with batch processing support',
|
||||
upload_area: {
|
||||
title: 'Drag PDF files here or click to upload',
|
||||
subtitle: 'Supports PDF format, max 100MB',
|
||||
button: 'Choose Files'
|
||||
},
|
||||
conversion_types: {
|
||||
pdf_to_image: 'PDF to Image',
|
||||
pdf_to_text: 'PDF to Text'
|
||||
},
|
||||
settings: {
|
||||
image_format: 'Image Format',
|
||||
image_quality: 'Image Quality',
|
||||
page_range: 'Page Range',
|
||||
output_format: 'Output Format',
|
||||
compression: 'Compression Settings'
|
||||
},
|
||||
options: {
|
||||
formats: {
|
||||
png: 'PNG',
|
||||
jpeg: 'JPEG',
|
||||
webp: 'WebP'
|
||||
},
|
||||
quality: {
|
||||
high: 'High Quality',
|
||||
medium: 'Medium Quality',
|
||||
low: 'Low Quality'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
convert: 'Start Conversion',
|
||||
download: 'Download',
|
||||
preview: 'Preview',
|
||||
clear: 'Clear'
|
||||
},
|
||||
status: {
|
||||
converting: 'Converting...',
|
||||
completed: 'Conversion Completed',
|
||||
failed: 'Conversion Failed',
|
||||
ready: 'Ready'
|
||||
},
|
||||
errors: {
|
||||
file_too_large: 'File too large, please select files smaller than 100MB',
|
||||
invalid_format: 'Unsupported file format',
|
||||
conversion_failed: 'Conversion failed, please check the file',
|
||||
no_file: 'Please select files first'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pdfConverterZh } from './zh';
|
||||
import { pdfConverterEn } from './en';
|
||||
|
||||
export const pdfConverterI18n = {
|
||||
zh: pdfConverterZh,
|
||||
en: pdfConverterEn,
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
export const pdfConverterZh = {
|
||||
title: 'PDF转换器',
|
||||
description: 'PDF转图片和文本提取工具,支持批量转换',
|
||||
upload_area: {
|
||||
title: '拖拽PDF文件到此处或点击上传',
|
||||
subtitle: '支持PDF格式,最大100MB',
|
||||
button: '选择文件'
|
||||
},
|
||||
conversion_types: {
|
||||
pdf_to_image: 'PDF转图片',
|
||||
pdf_to_text: 'PDF转文本'
|
||||
},
|
||||
settings: {
|
||||
image_format: '图片格式',
|
||||
image_quality: '图片质量',
|
||||
page_range: '页面范围',
|
||||
output_format: '输出格式',
|
||||
compression: '压缩设置'
|
||||
},
|
||||
options: {
|
||||
formats: {
|
||||
png: 'PNG',
|
||||
jpeg: 'JPEG',
|
||||
webp: 'WebP'
|
||||
},
|
||||
quality: {
|
||||
high: '高质量',
|
||||
medium: '中等质量',
|
||||
low: '低质量'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
convert: '开始转换',
|
||||
download: '下载',
|
||||
preview: '预览',
|
||||
clear: '清空'
|
||||
},
|
||||
status: {
|
||||
converting: '转换中...',
|
||||
completed: '转换完成',
|
||||
failed: '转换失败',
|
||||
ready: '准备就绪'
|
||||
},
|
||||
errors: {
|
||||
file_too_large: '文件过大,请选择小于100MB的文件',
|
||||
invalid_format: '不支持的文件格式',
|
||||
conversion_failed: '转换失败,请检查文件',
|
||||
no_file: '请先选择文件'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
export const pdfManagerEn = {
|
||||
title: 'PDF Merge & Split',
|
||||
description: 'Merge, split from PDF files ',
|
||||
upload_area: {
|
||||
title: 'Drag and drop PDF files here or click to upload',
|
||||
subtitle: 'Supports multiple PDF files, single file up to 100MB, total size up to 500MB',
|
||||
button: 'Choose Files'
|
||||
},
|
||||
operation_mode: {
|
||||
title: 'Operation Mode',
|
||||
merge: 'Merge Mode',
|
||||
split: 'Split Mode',
|
||||
extract: 'Page Extract'
|
||||
},
|
||||
merge_mode: {
|
||||
title: 'Merge Settings',
|
||||
file_order: 'File Order',
|
||||
output_name: 'Output Filename',
|
||||
compression: 'Compression'
|
||||
},
|
||||
split_mode: {
|
||||
title: 'Split Settings',
|
||||
split_method: 'Split Method',
|
||||
page_range: 'Page Range',
|
||||
custom_ranges: 'Custom Ranges',
|
||||
equal_parts: 'Equal Parts'
|
||||
},
|
||||
split_methods: {
|
||||
single_pages: 'Single Pages',
|
||||
single_pages_description: 'Split each page into a separate PDF file',
|
||||
custom_ranges: 'Custom Ranges',
|
||||
equal_parts: 'Equal Parts',
|
||||
by_range: 'By Range'
|
||||
},
|
||||
page_range: {
|
||||
title: 'Page Range',
|
||||
start_page: 'Start Page',
|
||||
end_page: 'End Page',
|
||||
total_pages: 'Total Pages',
|
||||
preview: 'Preview'
|
||||
},
|
||||
custom_ranges: {
|
||||
title: 'Custom Ranges',
|
||||
add_range: 'Add Range',
|
||||
remove_range: 'Remove Range',
|
||||
range_format: 'Format: 1-5,7-10,15',
|
||||
placeholder: 'Enter page ranges, e.g.: 1-5,7-10,15',
|
||||
single_page: 'Page {page}',
|
||||
page_range: 'Pages {start}-{end}'
|
||||
},
|
||||
equal_parts: {
|
||||
title: 'Equal Parts',
|
||||
parts_count: 'Number of Parts',
|
||||
pages_per_part: 'Pages per Part'
|
||||
},
|
||||
extract_mode: {
|
||||
title: 'Page Extract',
|
||||
extract_pages: 'Extract Pages',
|
||||
page_numbers: 'Page Numbers',
|
||||
page_numbers_hint: 'Enter page numbers separated by commas, support ranges like: 1,3,5-8,10',
|
||||
output_format: 'Output Format'
|
||||
},
|
||||
file_list: {
|
||||
title: 'File List',
|
||||
drag_hint: 'Drag files to reorder',
|
||||
total_size: 'Total Size',
|
||||
page_count: 'Total Pages',
|
||||
remove_file: 'Remove File'
|
||||
},
|
||||
output_settings: {
|
||||
title: 'Output Settings',
|
||||
filename_prefix: 'Filename Prefix',
|
||||
include_page_numbers: 'Include Page Numbers',
|
||||
compression: 'Compression'
|
||||
},
|
||||
actions: {
|
||||
merge: 'Start Merge',
|
||||
split: 'Start Split',
|
||||
extract: 'Start Extract',
|
||||
preview: 'Preview',
|
||||
download: 'Download',
|
||||
download_all: 'Download All',
|
||||
download_selected: 'Download Selected',
|
||||
clear: 'Clear'
|
||||
},
|
||||
status: {
|
||||
merging: 'Merging...',
|
||||
splitting: 'Splitting...',
|
||||
extracting: 'Extracting...',
|
||||
completed: 'Operation completed',
|
||||
failed: 'Operation failed',
|
||||
ready: 'Ready'
|
||||
},
|
||||
results: {
|
||||
title: 'Operation Results',
|
||||
files_count: 'File Count',
|
||||
total_size: 'Total Size',
|
||||
download_all: 'Download All Files',
|
||||
download_selected: 'Download Selected Files'
|
||||
},
|
||||
errors: {
|
||||
file_too_large: 'Files too large, single file cannot exceed 100MB, total size cannot exceed 500MB',
|
||||
invalid_format: 'Unsupported file format, please select PDF files',
|
||||
operation_failed: 'Operation failed, please check the files',
|
||||
no_files: 'Please select files first',
|
||||
need_at_least_two: 'Merge mode requires at least two PDF files',
|
||||
invalid_range: 'Invalid page range',
|
||||
range_out_of_bounds: 'Page range exceeds file page count',
|
||||
invalid_parts_count: 'Number of parts must be greater than 1',
|
||||
no_ranges: 'Please add at least one split range',
|
||||
invalid_page_numbers: 'Invalid page numbers'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { pdfManagerZh } from './zh';
|
||||
import { pdfManagerEn } from './en';
|
||||
|
||||
export const pdfManager = {
|
||||
zh: pdfManagerZh,
|
||||
en: pdfManagerEn
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
export const pdfManagerZh = {
|
||||
title: 'PDF合并与切割',
|
||||
description: 'PDF文件合并、切割',
|
||||
upload_area: {
|
||||
title: '拖拽PDF文件到此处或点击上传',
|
||||
subtitle: '支持多个PDF文件,单个文件不超过100MB,总大小不超过500MB',
|
||||
button: '选择文件'
|
||||
},
|
||||
operation_mode: {
|
||||
title: '操作模式',
|
||||
merge: '合并模式',
|
||||
split: '分割模式',
|
||||
extract: '页面提取'
|
||||
},
|
||||
merge_mode: {
|
||||
title: '合并设置',
|
||||
file_order: '文件顺序',
|
||||
output_name: '输出文件名',
|
||||
compression: '压缩设置'
|
||||
},
|
||||
split_mode: {
|
||||
title: '分割设置',
|
||||
split_method: '分割方式',
|
||||
page_range: '页面范围',
|
||||
custom_ranges: '自定义范围',
|
||||
equal_parts: '等分分割'
|
||||
},
|
||||
split_methods: {
|
||||
single_pages: '单页分割',
|
||||
single_pages_description: '将每个页面分割为单独的PDF文件',
|
||||
custom_ranges: '自定义范围',
|
||||
equal_parts: '等分分割',
|
||||
by_range: '按范围分割'
|
||||
},
|
||||
page_range: {
|
||||
title: '页面范围',
|
||||
start_page: '起始页',
|
||||
end_page: '结束页',
|
||||
total_pages: '总页数',
|
||||
preview: '预览'
|
||||
},
|
||||
custom_ranges: {
|
||||
title: '自定义范围',
|
||||
add_range: '添加范围',
|
||||
remove_range: '删除范围',
|
||||
range_format: '格式:1-5,7-10,15',
|
||||
placeholder: '输入页面范围,如:1-5,7-10,15',
|
||||
single_page: '第 {page} 页',
|
||||
page_range: '第 {start}-{end} 页'
|
||||
},
|
||||
equal_parts: {
|
||||
title: '等分分割',
|
||||
parts_count: '分割份数',
|
||||
pages_per_part: '每份页数'
|
||||
},
|
||||
extract_mode: {
|
||||
title: '页面提取',
|
||||
extract_pages: '提取页面',
|
||||
page_numbers: '页面编号',
|
||||
page_numbers_hint: '输入页面编号,用逗号分隔,支持范围如:1,3,5-8,10',
|
||||
output_format: '输出格式'
|
||||
},
|
||||
file_list: {
|
||||
title: '文件列表',
|
||||
drag_hint: '拖拽文件调整顺序',
|
||||
total_size: '总大小',
|
||||
page_count: '总页数',
|
||||
remove_file: '删除文件'
|
||||
},
|
||||
output_settings: {
|
||||
title: '输出设置',
|
||||
filename_prefix: '文件名前缀',
|
||||
include_page_numbers: '包含页码',
|
||||
compression: '压缩设置'
|
||||
},
|
||||
actions: {
|
||||
merge: '开始合并',
|
||||
split: '开始分割',
|
||||
extract: '开始提取',
|
||||
preview: '预览',
|
||||
download: '下载',
|
||||
download_all: '下载全部',
|
||||
download_selected: '下载选中',
|
||||
clear: '清空'
|
||||
},
|
||||
status: {
|
||||
merging: '合并中...',
|
||||
splitting: '分割中...',
|
||||
extracting: '提取中...',
|
||||
completed: '操作完成',
|
||||
failed: '操作失败',
|
||||
ready: '准备就绪'
|
||||
},
|
||||
results: {
|
||||
title: '操作结果',
|
||||
files_count: '文件数量',
|
||||
total_size: '总大小',
|
||||
download_all: '下载全部文件',
|
||||
download_selected: '下载选中文件'
|
||||
},
|
||||
errors: {
|
||||
file_too_large: '文件过大,单个文件不能超过100MB,总大小不能超过500MB',
|
||||
invalid_format: '不支持的文件格式,请选择PDF文件',
|
||||
operation_failed: '操作失败,请检查文件',
|
||||
no_files: '请先选择文件',
|
||||
need_at_least_two: '合并模式至少需要选择两个PDF文件',
|
||||
invalid_range: '无效的页面范围',
|
||||
range_out_of_bounds: '页面范围超出文件页数',
|
||||
invalid_parts_count: '分割份数必须大于1',
|
||||
no_ranges: '请至少添加一个分割范围',
|
||||
invalid_page_numbers: '无效的页面编号'
|
||||
}
|
||||
};
|
||||
@@ -32,7 +32,12 @@ export const zh = {
|
||||
image_watermark: tools.image_watermark.zh,
|
||||
image_to_ico: tools.image_to_ico.zh,
|
||||
cron_generator: tools.cron_generator.zh,
|
||||
icon_designer: tools.icon_designer.zh
|
||||
icon_designer: tools.icon_designer.zh,
|
||||
|
||||
pdf_converter: tools.pdf_converter.zh,
|
||||
pdf_manager: tools.pdf_manager.zh,
|
||||
pdf_compressor: tools.pdf_compressor.zh,
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+26
-2
@@ -2,7 +2,7 @@ import {
|
||||
faCode, faExchangeAlt, faClock, faGlobe, faLink, faLock,
|
||||
faImage, faCogs, faFileCode, faKey, faFont,
|
||||
faCalendarAlt, faPalette, faEdit, faRuler, faNetworkWired,
|
||||
faEraser, faCalculator, faFileAlt, faCube
|
||||
faEraser, faCalculator, faFileAlt, faCube,faObjectGroup, faCompress
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { Tool } from '@/types/tools';
|
||||
|
||||
@@ -177,7 +177,31 @@ const tools: Tool[] = [
|
||||
icon: faCube,
|
||||
category: ['common', 'image'],
|
||||
keywords: ['图标设计', '图标生成', 'icon设计', '图标制作', 'app图标', 'logo设计', 'favicon制作', '图标工具', 'icon designer', 'icon generator', 'tubiao', 'tb', 'sheji', 'sj', 'zhizuo', 'zz']
|
||||
}
|
||||
},
|
||||
|
||||
// PDF工具
|
||||
|
||||
|
||||
{
|
||||
code: 'pdf_converter',
|
||||
icon: faExchangeAlt,
|
||||
category: ['pdf'],
|
||||
keywords: ['pdf转换', 'pdf转图片', '图片转pdf', 'pdf转文本', 'pdf转换器', 'pdf converter', 'pdf转换工具', 'pdf', 'zhuanhuan', 'zh', 'zhuanhuanqi', 'zhq']
|
||||
},
|
||||
{
|
||||
code: 'pdf_manager',
|
||||
icon: faObjectGroup,
|
||||
category: ['pdf'],
|
||||
keywords: ['pdf合并', 'pdf分割', 'pdf合并器', 'pdf分割器', '合并pdf', '分割pdf', 'pdf合并工具', 'pdf分割工具', 'pdf merger', 'pdf splitter', 'pdf合并软件', 'pdf分割软件', 'pdf', 'hebing', 'fenge', 'hb', 'fg', 'hebingqi', 'fengeqi', 'hbq', 'fgq']
|
||||
},
|
||||
{
|
||||
code: 'pdf_compressor',
|
||||
icon: faCompress,
|
||||
category: ['pdf'],
|
||||
keywords: ['pdf压缩', 'pdf压缩器', '压缩pdf', 'pdf压缩工具', 'pdf compressor', 'pdf压缩软件', 'pdf', 'yasuo', 'ys', 'yasuoqi', 'ysq']
|
||||
},
|
||||
|
||||
|
||||
] as Tool[];
|
||||
|
||||
export default tools;
|
||||
Reference in New Issue
Block a user